This commit is contained in:
2026-01-07 22:27:52 +03:00
parent 481e532dce
commit ff1174671a
55 changed files with 11201 additions and 858 deletions
+521
View File
@@ -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>
);
};