525 lines
15 KiB
React
525 lines
15 KiB
React
import { useAtom } from "jotai";
|
|
import { AnimatePresence, motion } from "motion/react";
|
|
import {
|
|
BiCheckCircle,
|
|
BiCopy,
|
|
BiLeftArrowAlt,
|
|
BiLink,
|
|
BiPlus,
|
|
BiPlusCircle,
|
|
BiTrash,
|
|
BiX,
|
|
} from "react-icons/bi";
|
|
import Logo from "../assets/logo.svg";
|
|
import LogoAlternative from "../assets/fully_alternative_logo.svg";
|
|
import {
|
|
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={pickmeMode ? LogoAlternative : Logo}
|
|
alt="logo"
|
|
width={100}
|
|
draggable={false}
|
|
/>
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|