dx4 v2
This commit is contained in:
@@ -0,0 +1,521 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import {
|
||||
BiCheck,
|
||||
BiCheckCircle,
|
||||
BiCopy,
|
||||
BiLeftArrowAlt,
|
||||
BiLink,
|
||||
BiPlus,
|
||||
BiPlusCircle,
|
||||
BiTrash,
|
||||
BiX,
|
||||
BiXCircle,
|
||||
} from "react-icons/bi";
|
||||
import Logo from "../assets/logo.svg";
|
||||
import {
|
||||
AdminReq,
|
||||
AdminUpdateRequest,
|
||||
ClassUpdateRequest,
|
||||
getClassProfile,
|
||||
} from "../trash/req_mgr";
|
||||
import { API_BASE_URL } from "../App";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
DengiButton,
|
||||
DengiEditable,
|
||||
DengiYesOrNoDialog,
|
||||
LoadingSpinner,
|
||||
TextPreloader,
|
||||
} from "../trash/trash-components";
|
||||
import { b64e, copyDXLink, randomString } from "../trash/helpers";
|
||||
import { AvailableAdminSymbols } from "../trash/dengi";
|
||||
|
||||
export default function AdminProfilePage({
|
||||
classIDAtom,
|
||||
adminAtom,
|
||||
setsetRoute,
|
||||
routeAtom,
|
||||
pickmeModeAtom,
|
||||
}) {
|
||||
const [pickmeMode, _] = useAtom(pickmeModeAtom);
|
||||
const [_route, setRoute] = useAtom(routeAtom);
|
||||
const [adminId, _setAdminId] = useAtom(adminAtom);
|
||||
const [classId, _setClassId] = useAtom(classIDAtom);
|
||||
const [error, setError] = useState("");
|
||||
const [classData, setClassData] = useState(false);
|
||||
const [adminsData, setAdminsData] = useState(false);
|
||||
const [dataId, setDataId] = useState(0);
|
||||
if (classId === "") {
|
||||
setsetRoute(setRoute, "/");
|
||||
}
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
const a = (await getClassProfile(API_BASE_URL, adminId)) || false;
|
||||
if (!cancelled && a[0] === 1) {
|
||||
if (a[1]["class"]) {
|
||||
setClassData(a[1]["class"]);
|
||||
setAdminsData(a[1]["admins"]);
|
||||
} else {
|
||||
setError(a[1]);
|
||||
}
|
||||
} else {
|
||||
setError(a[1]);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [adminId, dataId]);
|
||||
return (
|
||||
<>
|
||||
<motion.div className="w-full h-screen flex flex-col items-center justify-center gap-5 overflow-x-hidden z-2">
|
||||
<motion.div
|
||||
className="w-screen h-screen flex flex-col items-center sm:max-w-[500px] sm:max-h-[90vh] gap-3"
|
||||
initial={{ scale: 0.99, opacity: 0, y: 5 }}
|
||||
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||
>
|
||||
<Header {...{ setsetRoute, setRoute, pickmeMode }} />
|
||||
<span className="text-xl font-bold text-center">
|
||||
управление классом
|
||||
</span>
|
||||
{error && typeof error == "string" && (
|
||||
<span className="text-red-500 text-2xl">{error}</span>
|
||||
)}
|
||||
<ClassProfileEditor
|
||||
{...{
|
||||
classId,
|
||||
loaded: classData !== false,
|
||||
classFr: classData.userclassname,
|
||||
pickmeMode,
|
||||
adminId,
|
||||
adminsData,
|
||||
update: () => {
|
||||
setDataId(dataId + 1);
|
||||
},
|
||||
setAdminsData,
|
||||
setClassFr: (v) => {
|
||||
setClassData((prev) => ({ ...prev, userclassname: v }));
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<CoolBg {...{ pickmeMode }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ClassProfileEditor({
|
||||
classId,
|
||||
classFr,
|
||||
pickmeMode,
|
||||
loaded,
|
||||
adminId,
|
||||
adminsData,
|
||||
setClassFr,
|
||||
update,
|
||||
setAdminsData,
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col items-center bg-grain gap-2 bg-zinc-800/50 p-5 rounded-2xl not-md:w-[90%] relative md:min-w-[520px]">
|
||||
<table className="">
|
||||
<tbody className="*:*:min-w-20 *:*:h-10">
|
||||
<tr>
|
||||
<td className="">classId</td>
|
||||
<td className="font-mono text-center select-all selectable">
|
||||
{classId}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="">название</td>
|
||||
<td className="w-40">
|
||||
<DengiEditable
|
||||
value={loaded ? classFr : false}
|
||||
pink={pickmeMode}
|
||||
onSubmit={async (e) => {
|
||||
e.setIdle(true);
|
||||
let r = await ClassUpdateRequest(API_BASE_URL, adminId, {
|
||||
action: "update_name",
|
||||
value: e.value.trim(),
|
||||
});
|
||||
if (r[1] == true) setClassFr(e.value.trim());
|
||||
e.setIdle(false);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="w-50 mt-5">
|
||||
<CopyClassLink {...{ classId }} />
|
||||
</div>
|
||||
<span className="h-[1px] w-[80%] bg-zinc-700 block my-5 "></span>
|
||||
<span className="text-xl font-bold block">админы</span>
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="*:px-2">
|
||||
<th>имя</th>
|
||||
<th>id</th>
|
||||
<th>уров.</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{adminsData &&
|
||||
adminsData.map((i) => (
|
||||
<AdminProfileRow
|
||||
key={i.adminId}
|
||||
{...{
|
||||
adminId: i.adminId,
|
||||
editorAdminId: adminId,
|
||||
adminName: i.adminName,
|
||||
adminLevel: i.adminPrems,
|
||||
classId,
|
||||
update,
|
||||
pickmeMode,
|
||||
setAdminFrId: (v) => {
|
||||
setAdminsData((prev) =>
|
||||
prev.map((a) =>
|
||||
a.adminId === i.adminId ? { ...a, adminName: v } : a
|
||||
)
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{!loaded && (
|
||||
<tr>
|
||||
<td>
|
||||
<span className="flex w-full justify-center">
|
||||
<TextPreloader />
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="flex w-full justify-center">
|
||||
<TextPreloader />
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="flex w-full justify-center">
|
||||
<TextPreloader />
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<LoadingSpinner size={20} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{loaded && <CreateAdmin {...{ editorAdminId: adminId, update }} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateAdmin({ editorAdminId, update }) {
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [adminId, setAdminId] = useState("");
|
||||
const [adminName, setAdminName] = useState("");
|
||||
const [locked, setLocked] = useState(false);
|
||||
const form = useRef(null);
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-full gap-1">
|
||||
<span className="w-full" />
|
||||
<AnimatePresence>
|
||||
{creating && (
|
||||
<motion.form
|
||||
className="flex gap-2"
|
||||
variants={{
|
||||
off: { opacity: 0, x: 20 },
|
||||
on: { opacity: 1, x: 0 },
|
||||
}}
|
||||
initial="off"
|
||||
animate="on"
|
||||
exit="off"
|
||||
ref={form}
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
if (locked || adminId.length < 5) return;
|
||||
setLocked(true);
|
||||
const c = await ClassUpdateRequest(
|
||||
API_BASE_URL,
|
||||
editorAdminId,
|
||||
{
|
||||
action: "create_admin",
|
||||
value: `${adminId};${b64e(adminName)}`,
|
||||
}
|
||||
);
|
||||
if (c[1] == true) {
|
||||
update();
|
||||
setCreating(false);
|
||||
}
|
||||
setLocked(false);
|
||||
}}
|
||||
>
|
||||
<input
|
||||
className="bg-zinc-900 rounded-2xl border border-zinc-500/50 px-2"
|
||||
placeholder="имя"
|
||||
required
|
||||
value={adminName}
|
||||
disabled={locked}
|
||||
maxLength={50}
|
||||
onInput={(e) => setAdminName(e.target.value.trimStart())}
|
||||
/>
|
||||
<input
|
||||
className="bg-zinc-900 rounded-2xl border border-zinc-500/50 px-2"
|
||||
placeholder="adminId"
|
||||
value={adminId}
|
||||
required
|
||||
disabled={locked}
|
||||
maxLength={20}
|
||||
minLength={5}
|
||||
min={5}
|
||||
onInput={(e) => {
|
||||
let val = e.target.value;
|
||||
let nval = "";
|
||||
val.split("").forEach((i) => {
|
||||
if (AvailableAdminSymbols.includes(i)) {
|
||||
nval = nval + i;
|
||||
}
|
||||
});
|
||||
e.target.value = nval;
|
||||
setAdminId(nval);
|
||||
}}
|
||||
/>
|
||||
<button>
|
||||
<DengiButton>
|
||||
{locked ? <LoadingSpinner /> : <BiPlus />}
|
||||
</DengiButton>
|
||||
</button>
|
||||
</motion.form>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<DengiButton
|
||||
disabled={locked}
|
||||
onClick={() => {
|
||||
setCreating(!creating);
|
||||
setAdminName("");
|
||||
setAdminId(randomString(8, AvailableAdminSymbols));
|
||||
}}
|
||||
>
|
||||
<span className={`${creating ? "rotate-45" : ""} transition`}>
|
||||
<BiPlusCircle />
|
||||
</span>
|
||||
</DengiButton>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminProfileRow({
|
||||
adminId,
|
||||
editorAdminId,
|
||||
adminName,
|
||||
classId,
|
||||
pickmeMode,
|
||||
setAdminFrId,
|
||||
adminLevel,
|
||||
update,
|
||||
}) {
|
||||
return (
|
||||
<tr className="h-12">
|
||||
<td className="px-2">
|
||||
<DengiEditable
|
||||
value={adminName}
|
||||
pink={pickmeMode}
|
||||
onSubmit={async (e) => {
|
||||
e.setIdle(true);
|
||||
const r = await AdminUpdateRequest(API_BASE_URL, editorAdminId, {
|
||||
action: "change_name",
|
||||
value: e.value.trim(),
|
||||
person: adminId,
|
||||
});
|
||||
if (r[1] == true) setAdminFrId(e.value);
|
||||
e.setIdle(false);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2">
|
||||
<DengiEditable
|
||||
value={adminId}
|
||||
password={true}
|
||||
pink={pickmeMode}
|
||||
disabled
|
||||
/>
|
||||
</td>
|
||||
<td className="text-center">{adminLevel}</td>
|
||||
<td className="inline-flex gap-1 items-center justify-center">
|
||||
<CopyAdminLink {...{ classId, adminId }} />
|
||||
<RemoveAdminButton
|
||||
{...{ adminId, editorAdminId, adminName, update }}
|
||||
disabled={[-1, 2].includes(adminLevel)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const RemoveAdminButton = ({
|
||||
adminId,
|
||||
editorAdminId,
|
||||
adminName,
|
||||
update,
|
||||
disabled,
|
||||
}) => {
|
||||
const [showDiag, setShowDiag] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DengiYesOrNoDialog
|
||||
show={showDiag}
|
||||
content={{
|
||||
message: (
|
||||
<p>
|
||||
вы точно хотите удалить админа <b>{adminName}</b>?
|
||||
</p>
|
||||
),
|
||||
yesBtn: "да",
|
||||
noBtn: "нет",
|
||||
yesIcon: <BiTrash />,
|
||||
noIcon: <BiX size={20} />,
|
||||
bgDissmis: true,
|
||||
yesBtnStyle: "bg-red-500! text-white! md:hover:bg-red-600!",
|
||||
}}
|
||||
onAnswer={async (answ) => {
|
||||
setShowDiag(false);
|
||||
if (answ) {
|
||||
setDeleting(true);
|
||||
const c = await ClassUpdateRequest(API_BASE_URL, editorAdminId, {
|
||||
action: "delete_admin",
|
||||
value: adminId,
|
||||
});
|
||||
if (c[1] == true) {
|
||||
update();
|
||||
setDeleting(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<DengiButton
|
||||
disabled={adminId === editorAdminId || disabled}
|
||||
onClick={() => {
|
||||
if (deleting) return;
|
||||
setShowDiag(true);
|
||||
}}
|
||||
>
|
||||
{deleting ? <LoadingSpinner size={20} /> : <BiTrash />}
|
||||
</DengiButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CopyClassLink = ({ classId }) => {
|
||||
const [linkCopied, setLinkCopied] = useState(false);
|
||||
|
||||
return (
|
||||
<DengiButton
|
||||
onClick={() => {
|
||||
copyDXLink(classId).then((a) => {
|
||||
if (a) {
|
||||
setLinkCopied(true);
|
||||
setTimeout(() => {
|
||||
setLinkCopied(false);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<BiLink />
|
||||
<span>{linkCopied ? "скопировано" : "коп ссылку"}</span>
|
||||
</DengiButton>
|
||||
);
|
||||
};
|
||||
const CopyAdminLink = ({ classId, adminId }) => {
|
||||
const [showDiag, setShowDiag] = useState(false);
|
||||
const [seenDiag, setSeenDiag] = useState(false);
|
||||
const [linkCopied, setLinkCopied] = useState(false);
|
||||
const copy = () => {
|
||||
copyDXLink(classId, adminId).then((a) => {
|
||||
if (a) {
|
||||
setLinkCopied(true);
|
||||
setTimeout(() => {
|
||||
setLinkCopied(false);
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DengiYesOrNoDialog
|
||||
show={showDiag}
|
||||
content={{
|
||||
message: (
|
||||
<p>
|
||||
<b>любой</b>, кто получит ссылку,{" "}
|
||||
<b>будет иметь доступ к админке</b>.<br /> вы желаете продолжить?
|
||||
</p>
|
||||
),
|
||||
yesBtn: "да",
|
||||
noBtn: "нет",
|
||||
yesIcon: <BiCopy />,
|
||||
noIcon: <BiX size={20} />,
|
||||
bgDissmis: true,
|
||||
yesBtnStyle: "bg-orange-500! text-white! md:hover:bg-orange-600!",
|
||||
}}
|
||||
onAnswer={(answ) => {
|
||||
if (answ) {
|
||||
setSeenDiag(true);
|
||||
copy();
|
||||
}
|
||||
setShowDiag(false);
|
||||
}}
|
||||
/>
|
||||
<DengiButton onClick={() => (seenDiag ? copy() : setShowDiag(true))}>
|
||||
{linkCopied ? <BiCheckCircle /> : <BiLink />}
|
||||
</DengiButton>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const CoolBg = ({ pickmeMode }) => {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-full h-full -z-5 overflow-hidden bg-gradient-to-tl to-black to-50% bg-fixed ${
|
||||
pickmeMode ? "from-pink-400/10" : "from-white/2"
|
||||
}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Header = ({ setsetRoute, setRoute, pickmeMode }) => {
|
||||
return (
|
||||
<div className={"flex items-center w-full justify-between px-5"}>
|
||||
<span className="flex gap-2 items-center">
|
||||
<BiLeftArrowAlt
|
||||
size={35}
|
||||
onClick={() => {
|
||||
setsetRoute(setRoute, "/app");
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
className={
|
||||
"rounded-full " +
|
||||
(pickmeMode
|
||||
? "lg:hover:bg-pink-400"
|
||||
: "lg:hover:bg-white lg:hover:fill-black") +
|
||||
" lg:hover:scale-105 transition cursor-pointer"
|
||||
}
|
||||
/>
|
||||
<img src={Logo} alt="logo" width={100} draggable={false} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user