dx4 v2
This commit is contained in:
+6
-2
@@ -1,7 +1,7 @@
|
||||
// CHANGE TO YOUR API DESTINATION
|
||||
export const API_BASE_URL = "http://localhost:8321/api.php";
|
||||
// export const API_BASE_URL = "http://192.168.3.10:8321/api.php";
|
||||
// export const API_BASE_URL = "http://localhost:8321/api.php"
|
||||
export const API_BASE_URL = "http://x90620bx.beget.tech/dx4back/dengidengi.php"
|
||||
// export const API_BASE_URL = "http://x90620bx.beget.tech/dx4back/api.php"
|
||||
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
@@ -19,6 +19,8 @@ const pickmeModeAtom = atomWithStorage("pickmeMode", false);
|
||||
|
||||
const EditorPage = lazy(() => import("./pages/editor"));
|
||||
const TestPage = lazy(() => import("./pages/test"));
|
||||
const AdminProfilePage = lazy(() => import("./pages/AdminProfile"));
|
||||
const ClassProfilePage = lazy(() => import("./pages/ClassProfile"));
|
||||
|
||||
const dengi_cash = new DengiCaching();
|
||||
|
||||
@@ -61,6 +63,8 @@ const pages = {
|
||||
"/exit": <ExitPage {...basePageProps} />,
|
||||
"/editor": <EditorPage {...basePageProps} />,
|
||||
"/test": <TestPage />,
|
||||
"/adminprofile": <AdminProfilePage {...basePageProps} />,
|
||||
"/classprofile": <ClassProfilePage {...basePageProps} />,
|
||||
};
|
||||
|
||||
let changeAnchor = true;
|
||||
|
||||
@@ -33,3 +33,7 @@ input {
|
||||
.bg-grain {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cfilter id='noise' x='0' y='0'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.8 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.45'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
body > *:not(#root):not(vite-error-overlay) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { motion } from "motion/react";
|
||||
import { BiCopy, BiError, BiLeftArrowAlt, BiLink, BiX } from "react-icons/bi";
|
||||
import Logo from "../assets/logo.svg";
|
||||
import { AdminReq, AdminUpdateRequest } from "../trash/req_mgr";
|
||||
import { API_BASE_URL } from "../App";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
DengiButton,
|
||||
DengiEditable,
|
||||
DengiYesOrNoDialog,
|
||||
TextPreloader,
|
||||
} from "../trash/trash-components";
|
||||
import { copyDXLink } from "../trash/helpers";
|
||||
|
||||
export default function AdminProfilePage({
|
||||
adminAtom,
|
||||
setsetRoute,
|
||||
routeAtom,
|
||||
pickmeModeAtom,
|
||||
dengi_cash,
|
||||
classIDAtom,
|
||||
}) {
|
||||
const [pickmeMode, _] = useAtom(pickmeModeAtom);
|
||||
const [_route, setRoute] = useAtom(routeAtom);
|
||||
const [adminId, _setAdminId] = useAtom(adminAtom);
|
||||
const [classId, _setClassId] = useAtom(classIDAtom);
|
||||
const [adminFrId, setAdminFrId] = useState(false);
|
||||
const [adminData, setAdminData] = useState(false);
|
||||
if (classId === "") {
|
||||
setsetRoute(setRoute, "/");
|
||||
}
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
const a = (await AdminReq(API_BASE_URL, adminId)) || false;
|
||||
if (!cancelled && a[0] === 1) {
|
||||
setAdminFrId(a[1].adminName);
|
||||
setAdminData({ class: a[1].adminClass, adminPerms: a[1].adminPerms });
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [adminId]);
|
||||
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">профиль админа</span>
|
||||
<AdminProfileEditor
|
||||
{...{
|
||||
adminId,
|
||||
classId: adminData.class ? adminData.class : false,
|
||||
adminFrId,
|
||||
pickmeMode,
|
||||
setAdminFrId: (v) => {
|
||||
setAdminFrId(v);
|
||||
dengi_cash.delete(`adminfr-${adminId}`);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
<CoolBg {...{ pickmeMode }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function AdminProfileEditor({
|
||||
adminFrId,
|
||||
setAdminFrId,
|
||||
adminId,
|
||||
classId,
|
||||
pickmeMode = false,
|
||||
}) {
|
||||
return (
|
||||
<div className=" bg-grain bg-zinc-800/50 p-5 rounded-2xl not-md:w-[90%]">
|
||||
<table className="">
|
||||
<tbody className="*:*:w-10 *:*:h-10">
|
||||
<tr>
|
||||
<td className="">имя</td>
|
||||
<td className="w-50!">
|
||||
<DengiEditable
|
||||
value={adminFrId}
|
||||
pink={pickmeMode}
|
||||
onSubmit={async (e) => {
|
||||
e.setIdle(true);
|
||||
const r = await AdminUpdateRequest(API_BASE_URL, adminId, {
|
||||
action: "change_name",
|
||||
value: e.value.trim(),
|
||||
});
|
||||
if (r[1] == true) setAdminFrId(e.value);
|
||||
e.setIdle(false);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>id</td>
|
||||
<td>
|
||||
<DengiEditable
|
||||
value={adminId}
|
||||
password={true}
|
||||
pink={pickmeMode}
|
||||
disabled
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>class</td>
|
||||
<td className="font-mono text-center select-all selectable">
|
||||
{classId ? classId : <TextPreloader />}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<CopyLink {...{ adminId, classId }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const CopyLink = ({ 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 (
|
||||
<div className="flex justify-end mt-2">
|
||||
<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))}>
|
||||
<BiLink /> <BiError color={"orange"} />
|
||||
<span>{linkCopied ? "скопировано" : "коп ссылку на админку"}</span>
|
||||
</DengiButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
export default function Panel() {
|
||||
return <>
|
||||
<div>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
+174
-96
@@ -1,20 +1,22 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Fragment, memo, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
BiDotsHorizontalRounded,
|
||||
BiExit,
|
||||
BiSolidUser,
|
||||
BiStar,
|
||||
BiPencil,
|
||||
BiLink,
|
||||
BiWrench,
|
||||
} from "react-icons/bi";
|
||||
import Logo from "../assets/logo.svg";
|
||||
import {
|
||||
Switch,
|
||||
LoadingSpinner,
|
||||
DatePicker,
|
||||
TextPreloader,
|
||||
// CoolMouseBg,
|
||||
} from "../trash/trash-components";
|
||||
import {
|
||||
AdminReq,
|
||||
AdminUserFRequest,
|
||||
classUserFRequest,
|
||||
RaspGet,
|
||||
@@ -24,7 +26,7 @@ import { AnimatePresence, LayoutGroup, motion } from "motion/react";
|
||||
import { apiFormatDate, isTimeBeforeNow } from "../trash/dengi";
|
||||
import { useAtom } from "jotai";
|
||||
import { API_BASE_URL } from "../App";
|
||||
import { copyToClipboard, ifstyle } from "../trash/helpers";
|
||||
import { copyDXLink, ifstyle } from "../trash/helpers";
|
||||
|
||||
export function DX4App({
|
||||
classIDAtom,
|
||||
@@ -41,6 +43,7 @@ export function DX4App({
|
||||
const [pickmeMode, setPickmeMode] = useAtom(pickmeModeAtom);
|
||||
const [rasp, setRasp] = useState([false, {}, 0]);
|
||||
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||
const [managing, setManaging] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -49,14 +52,14 @@ export function DX4App({
|
||||
if (!dengi_cash.wasCached(cacheName)) {
|
||||
RaspGet(API_BASE_URL, classId, selectedDate).then(
|
||||
(d) => {
|
||||
if (cancelled) return;
|
||||
if (d[0] == 1) {
|
||||
try {
|
||||
setRasp([
|
||||
true,
|
||||
d[1]["data"] && d[1]["data"][0] ? d[1]["data"] : -322,
|
||||
d[1]["type"] ? d[1]["type"] : -1,
|
||||
]);
|
||||
if (!cancelled)
|
||||
setRasp([
|
||||
true,
|
||||
d[1]["data"] && d[1]["data"][0] ? d[1]["data"] : -322,
|
||||
d[1]["type"] ? d[1]["type"] : -1,
|
||||
]);
|
||||
//console.log(d, d[1] ? d[1] : false);
|
||||
dengi_cash.cache(cacheName, [
|
||||
d[1]["data"] && d[1]["data"][0] ? d[1]["data"] : -322,
|
||||
@@ -64,18 +67,20 @@ export function DX4App({
|
||||
]);
|
||||
} catch (exception) {
|
||||
//console.error(exception);
|
||||
setRasp([true, exception.toString()]);
|
||||
if (!cancelled) setRasp([true, exception.toString()]);
|
||||
}
|
||||
} else {
|
||||
setRasp([true, "ошибка: " + d[1].toString()]);
|
||||
if (!cancelled) setRasp([true, "ошибка: " + d[1].toString()]);
|
||||
//console.log(d[1]);
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (cancelled) return;
|
||||
setRasp([true, "ошибка: " + err]);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (cancelled) return;
|
||||
setRasp([true, ...dengi_cash.get(cacheName)]);
|
||||
}
|
||||
|
||||
@@ -133,8 +138,8 @@ export function DX4App({
|
||||
else promises.push(Promise.resolve([0]));
|
||||
if (adminId !== "")
|
||||
promises.push(
|
||||
dengi_cash.withCacheAsync(`adminfr-${adminId}`, async () =>
|
||||
AdminUserFRequest(API_BASE_URL, adminId)
|
||||
dengi_cash.withCacheAsync(`admindata-${adminId}`, async () =>
|
||||
AdminReq(API_BASE_URL, adminId)
|
||||
)
|
||||
);
|
||||
else promises.push(Promise.resolve([0]));
|
||||
@@ -143,6 +148,8 @@ export function DX4App({
|
||||
const classF = classRes[0] === 1 ? classRes[1].userclassname : false;
|
||||
const adminF = adminRes[0] === 1 ? adminRes[1].adminName : false;
|
||||
setUserFrNames({ admin: adminF, class: classF });
|
||||
if (adminRes[0] === 1)
|
||||
setManaging([-1, 2].includes(adminRes[1].adminPrems));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
@@ -259,28 +266,65 @@ export function DX4App({
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{adminId != "" && managing && (
|
||||
<motion.div
|
||||
className="flex w-full justify-end"
|
||||
layout="position"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
>
|
||||
<a
|
||||
// onClick={() => {
|
||||
// setsetRoute(setRoute, `/editor/${apiFormatDate(selectedDate)}`);
|
||||
// }}
|
||||
href={`#/classprofile`}
|
||||
className={`flex items-center justify-center border p-1 gap-2 px-2 cursor-pointer rounded-xl transition-colors ${
|
||||
pickmeMode
|
||||
? "border-pink-400/30 text-pink-300/80 lg:hover:text-white lg:hover:border-pink-500"
|
||||
: "border-zinc-700 text-zinc-400 lg:hover:text-white lg:hover:border-white"
|
||||
} `}
|
||||
>
|
||||
<BiWrench /> управление
|
||||
</a>
|
||||
</motion.div>
|
||||
)}
|
||||
<motion.div
|
||||
layout="position"
|
||||
className="flex flex-col gap-5 w-full not-md:px-4"
|
||||
>
|
||||
{adminId != "" && (
|
||||
<motion.div className="flex w-full justify-end">
|
||||
<div className="flex gap-2 items-center justify-center">
|
||||
<DatePicker
|
||||
date={selectedDate}
|
||||
setDate={setSelectedDate}
|
||||
indicator={
|
||||
typeof rasp[2] == "number" && [1, 2].includes(rasp[2])
|
||||
}
|
||||
pink={pickmeMode}
|
||||
/>
|
||||
{adminId != "" && (
|
||||
<a
|
||||
// onClick={() => {
|
||||
// setsetRoute(setRoute, `/editor/${apiFormatDate(selectedDate)}`);
|
||||
// }}
|
||||
href={`#/editor/${apiFormatDate(selectedDate)}`}
|
||||
className="flex items-center justify-center border p-1 gap-2 px-2 lg:hover:border-white cursor-pointer rounded-xl transition-colors border-zinc-700 text-zinc-400 lg:hover:text-white"
|
||||
href={
|
||||
userFrNames.admin
|
||||
? `#/editor/${apiFormatDate(selectedDate)}`
|
||||
: undefined
|
||||
}
|
||||
className={`flex items-center justify-center p-1.5 cursor-pointer border transition *:transition-colors rounded-full ${
|
||||
pickmeMode
|
||||
? "border-pink-400/30 lg:hover:text-white hover:lg:bg-pink-500"
|
||||
: "border-zinc-700 text-white lg:hover:text-black lg:hover:bg-white"
|
||||
} `}
|
||||
>
|
||||
<BiPencil /> открыть в редакторе
|
||||
{userFrNames.admin ? (
|
||||
<BiPencil size={28} />
|
||||
) : (
|
||||
<LoadingSpinner size={28} />
|
||||
)}
|
||||
</a>
|
||||
</motion.div>
|
||||
)}
|
||||
<DatePicker
|
||||
date={selectedDate}
|
||||
setDate={setSelectedDate}
|
||||
indicator={typeof rasp[2] == "number" && [1, 2].includes(rasp[2])}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="relative">
|
||||
<LayoutGroup>
|
||||
<RaspView {...{ rasp, pickmeMode, selectedDate }} />
|
||||
@@ -306,7 +350,7 @@ export function DX4App({
|
||||
</>
|
||||
);
|
||||
}
|
||||
const CoolMainBg = React.memo(({ pickmeMode }) => {
|
||||
const CoolMainBg = memo(({ pickmeMode }) => {
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-full h-full -z-5 overflow-hidden bg-gradient-to-br to-black to-50% bg-fixed
|
||||
@@ -315,7 +359,7 @@ const CoolMainBg = React.memo(({ pickmeMode }) => {
|
||||
);
|
||||
});
|
||||
|
||||
export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => {
|
||||
export const RaspView = memo(({ rasp, pickmeMode, selectedDate }) => {
|
||||
return (
|
||||
<AnimatePresence mode="popLayout">
|
||||
{rasp[0] && typeof rasp[1] == "object" && (
|
||||
@@ -324,10 +368,14 @@ export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => {
|
||||
//layout="position"
|
||||
key={selectedDate}
|
||||
className={
|
||||
"w-full border-collapse border-spacing-x-0 border-spacing-y-[10px] bg-gradient-to-br backdrop-blur-xl mb-10 " +
|
||||
"w-full border-collapse border-spacing-x-0 border-spacing-y-[10px] bg-gradient-to-br mb-10 " +
|
||||
(pickmeMode ? "from-pink-400/30" : "from-zinc-800/40") +
|
||||
" to-transparent rounded-2xl selectable"
|
||||
}
|
||||
onContextMenu={(ev) => {
|
||||
ev.preventDefault();
|
||||
itemRedo.action();
|
||||
}}
|
||||
variants={{
|
||||
hid: { opacity: 0 },
|
||||
show: { opacity: 1 },
|
||||
@@ -372,61 +420,7 @@ export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => {
|
||||
</thead>
|
||||
<tbody className="*:*:text-center *:*:py-2 *:not-last:border-b *:not-last:border-zinc-800 text-white">
|
||||
{rasp[1].map((d, i) => (
|
||||
<tr
|
||||
//layout="position"
|
||||
key={`table-header-${i}`}
|
||||
className="transition-colors *:transition-colors lg:hover:bg-zinc-300/15"
|
||||
//rasp-key={"rasp_key_" + i + Array(d).join()}
|
||||
>
|
||||
<td
|
||||
//layout="position"
|
||||
//layoutId={`table-num-${i}`}
|
||||
>
|
||||
{d["num"]}
|
||||
</td>
|
||||
<td
|
||||
//layout="position"
|
||||
//layoutId={`table-subj-${i}`}
|
||||
>
|
||||
{d.subject
|
||||
.toString()
|
||||
.split("\n")
|
||||
.map((line, i, array) => (
|
||||
<React.Fragment key={i}>
|
||||
{line}
|
||||
{i < array.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
)) || d.subject}
|
||||
</td>
|
||||
<td
|
||||
//layout="position"
|
||||
//layoutId={`table-cab-${i}`}
|
||||
>
|
||||
{d.cabinet
|
||||
.toString()
|
||||
.split("\n")
|
||||
.map((line, i, array) => (
|
||||
<React.Fragment key={i}>
|
||||
{line}
|
||||
{i < array.length - 1 && <br />}
|
||||
</React.Fragment>
|
||||
)) || d.cabinet}
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
//layout="position"
|
||||
//layoutId={`table-time-${i}`}
|
||||
className="flex items-center justify-center min-h-9"
|
||||
>
|
||||
<div className="flex gap-0.5 flex-col *:block w-[70%] *:font-light">
|
||||
<span className="text-left">
|
||||
{d["t-begin"]} -
|
||||
</span>
|
||||
<span className="text-right">{d["t-end"]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<TableItem {...{ d }} key={`table-header-${i}`} />
|
||||
))}
|
||||
</tbody>
|
||||
</motion.table>
|
||||
@@ -457,7 +451,7 @@ export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const UserSettings = React.memo(
|
||||
const UserSettings = memo(
|
||||
({
|
||||
openUserSettings,
|
||||
userFrNames,
|
||||
@@ -476,8 +470,8 @@ const UserSettings = React.memo(
|
||||
<motion.div
|
||||
className="flex flex-col bg-grain gap-2 bg-zinc-700/20 p-2 rounded-2xl"
|
||||
variants={{
|
||||
hidden: { opacity: 0, y: 10, filter: "blur(2px)" },
|
||||
show: { opacity: 1, y: 0, filter: "blur(0)" },
|
||||
hidden: { opacity: 0, y: 10 },
|
||||
show: { opacity: 1, y: 0 },
|
||||
}}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
@@ -507,21 +501,20 @@ const UserSettings = React.memo(
|
||||
// ],
|
||||
[
|
||||
<BiSolidUser />,
|
||||
`админ ${
|
||||
userFrNames["admin"] ? userFrNames["admin"] : adminId
|
||||
}`,
|
||||
() => {},
|
||||
<>
|
||||
{`админ ${userFrNames["admin"] ? userFrNames["admin"] : ""}`}
|
||||
{!userFrNames["admin"] && <TextPreloader width="30px" />}
|
||||
</>,
|
||||
() => {
|
||||
setsetRoute(setRoute, "/adminprofile");
|
||||
},
|
||||
adminId != "",
|
||||
],
|
||||
[
|
||||
<BiLink />,
|
||||
classLinkCopied ? "скопированно" : "коп. ссылку",
|
||||
classLinkCopied ? "скопировано" : "коп. ссылку",
|
||||
() => {
|
||||
copyToClipboard(
|
||||
location.href.substring(0, location.href.search("#")) +
|
||||
"#//" +
|
||||
classId
|
||||
).then((a) => {
|
||||
copyDXLink(classId).then((a) => {
|
||||
if (a) {
|
||||
setClassLinkCopied(true);
|
||||
setTimeout(() => {
|
||||
@@ -556,3 +549,88 @@ const UserSettings = React.memo(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
class GlobalListener {
|
||||
constructor() {
|
||||
this.listeners = [];
|
||||
}
|
||||
listen(fn, once = false) {
|
||||
const wrapper = (...args) => {
|
||||
try {
|
||||
fn(...args);
|
||||
} finally {
|
||||
if (once) this.listeners = this.listeners.filter((l) => l !== wrapper);
|
||||
}
|
||||
};
|
||||
this.listeners.push(wrapper);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter((l) => l !== wrapper);
|
||||
};
|
||||
}
|
||||
action(...args) {
|
||||
this.listeners.slice().forEach((fn) => {
|
||||
try {
|
||||
fn(...args);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const itemRedo = new GlobalListener();
|
||||
|
||||
const TableItem = memo(({ d }) => {
|
||||
const [sel, setSel] = useState(false);
|
||||
useEffect(() => {
|
||||
itemRedo.listen(() => {
|
||||
setSel(false);
|
||||
});
|
||||
}, []);
|
||||
return (
|
||||
<tr
|
||||
//layout="position"
|
||||
className={
|
||||
"transition-colors *:transition-colors " +
|
||||
(sel
|
||||
? "bg-zinc-300/15 lg:hover:bg-zinc-300/20"
|
||||
: " lg:hover:bg-zinc-300/15")
|
||||
}
|
||||
onClick={() => {
|
||||
setSel(!sel);
|
||||
}}
|
||||
>
|
||||
<td>{d["num"]}</td>
|
||||
<td>
|
||||
{d.subject
|
||||
.toString()
|
||||
.split("\n")
|
||||
.map((line, i, array) => (
|
||||
<Fragment key={i}>
|
||||
{line}
|
||||
{i < array.length - 1 && <br />}
|
||||
</Fragment>
|
||||
)) || d.subject}
|
||||
</td>
|
||||
<td>
|
||||
{d.cabinet
|
||||
.toString()
|
||||
.split("\n")
|
||||
.map((line, i, array) => (
|
||||
<Fragment key={i}>
|
||||
{line}
|
||||
{i < array.length - 1 && <br />}
|
||||
</Fragment>
|
||||
)) || d.cabinet}
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center min-h-9">
|
||||
<div className="flex gap-0.5 flex-col *:block w-[70%] *:font-light">
|
||||
<span className="text-left">{d["t-begin"]} -</span>
|
||||
<span className="text-right">{d["t-end"]}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
+20
-29
@@ -7,15 +7,7 @@ import { BiHide, BiShow, BiSolidErrorCircle, BiBlock } from "react-icons/bi";
|
||||
import { LoginReq, AdminReq } from "../trash/req_mgr";
|
||||
import { API_BASE_URL } from "../App";
|
||||
import { b64d } from "../trash/helpers";
|
||||
const AvailableSymbols = (() => {
|
||||
const a = "abcdefghijklmnopqrstuvwxyz"; // alphabet
|
||||
const A = a.toUpperCase(); // ALPHABET
|
||||
const n = "1234567890"; // numbers
|
||||
const s = "_-"; // specials
|
||||
return (a + n + A + s).split("");
|
||||
})();
|
||||
|
||||
const AvailableAdminSymbols = [...AvailableSymbols, ..."$()*^%@!+=.".split("")];
|
||||
import { AvailableAdminSymbols, AvailableSymbols } from "../trash/dengi";
|
||||
|
||||
export function LoginPage({
|
||||
classIDAtom,
|
||||
@@ -51,7 +43,7 @@ export function LoginPage({
|
||||
setsetRoute(setRoute, "/app");
|
||||
} else {
|
||||
const al = await AdminReq(API_BASE_URL, aid);
|
||||
console.log(al);
|
||||
// console.log(al);
|
||||
if (al[0] === 1) {
|
||||
// yes, I know that check must go on backend, but im stupid lazy piece of shit 💀💀💀
|
||||
// TODO: FIX LATER
|
||||
@@ -61,8 +53,8 @@ export function LoginPage({
|
||||
setsetRoute(setRoute, "/app");
|
||||
} else {
|
||||
// you are not from here
|
||||
console.log(al);
|
||||
setErrorText("админ не пренадлежит этому классу");
|
||||
// console.log(al);
|
||||
setErrorText("админ не найден");
|
||||
}
|
||||
} else {
|
||||
setErrorText(al[1]);
|
||||
@@ -108,9 +100,9 @@ export function LoginPage({
|
||||
<AnimatePresence mode="popLayout">
|
||||
{errorText.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ y: 50, opacity: 0, filter: "blur(5px)" }}
|
||||
animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ y: 50, opacity: 0, filter: "blur(5px)" }}
|
||||
initial={{ y: 50, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 50, opacity: 0 }}
|
||||
layout="position"
|
||||
className="p-2 bg-gradient-to-r from-zinc-900 to-white/40 text-white rounded-xl w-[90vw] max-w-[600px] flex gap-2 items-center justify-between"
|
||||
>
|
||||
@@ -128,24 +120,23 @@ export function LoginPage({
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<motion.div
|
||||
className="flex lg:backdrop-blur-xl flex-col rounded-2xl w-[90vw] items-center justify-center py-3 max-w-[600px] bg-grain focus-within:border-white/10 border border-transparent transition-colors"
|
||||
className="flex flex-col rounded-2xl w-[90vw] items-center justify-center py-3 max-w-[600px] bg-grain focus-within:border-white/10 border border-transparent transition-colors relative"
|
||||
initial={{
|
||||
y: 30,
|
||||
filter: "blur(5px)",
|
||||
opacity: 0,
|
||||
}}
|
||||
layout="position"
|
||||
animate={{ y: 0, filter: "blur(0)", opacity: 1 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
>
|
||||
<motion.img src={Logo} alt="logo dengi" width={200} />
|
||||
<motion.h1 className="text-zinc-600">добро пожаловать.</motion.h1>
|
||||
<motion.h2 className="text-3xl font-light">вход</motion.h2>
|
||||
<img src={Logo} alt="logo dengi" width={200} />
|
||||
<h1 className="text-zinc-600">добро пожаловать.</h1>
|
||||
<h2 className="text-3xl font-light">вход</h2>
|
||||
<form
|
||||
action="#"
|
||||
className="mt-2 flex flex-col gap-3 *:outline-none *:border *:border-transparent *:focus:border-white"
|
||||
onSubmit={formSubmit}
|
||||
>
|
||||
<motion.input
|
||||
<input
|
||||
type="text"
|
||||
name="d"
|
||||
autoComplete="off"
|
||||
@@ -170,7 +161,7 @@ export function LoginPage({
|
||||
disabled={isLogining}
|
||||
minLength={3}
|
||||
/>
|
||||
<motion.span className="w-full flex gap-2 items-center justify-center ">
|
||||
<span className="w-full flex gap-2 items-center justify-center ">
|
||||
<Switch
|
||||
type="checkbox"
|
||||
onChange={(e) => setIsAdmin(e)}
|
||||
@@ -178,7 +169,7 @@ export function LoginPage({
|
||||
name=""
|
||||
/>
|
||||
админка
|
||||
</motion.span>
|
||||
</span>
|
||||
<CoolInputWithHide
|
||||
{...{ enabled: isAdmin, setEnteredAId, isLogining, adminEnteredId }}
|
||||
/>
|
||||
@@ -200,8 +191,8 @@ export function LoginPage({
|
||||
<AnimatePresence mode="popLayout">
|
||||
{isLogining && (
|
||||
<motion.div
|
||||
initial={{ y: -40, opacity: 0, filter: "blur(5px)" }}
|
||||
animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
|
||||
initial={{ y: -40, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
className="p-2 w-[90vw] max-w-[600px] border-2 rounded-xl border-zinc-500 flex gap-2 items-center justify-center hover:border-white transition-colors bg-grain"
|
||||
layout="position"
|
||||
>
|
||||
@@ -224,7 +215,7 @@ function CoolLoginButton({ display, isLogining }) {
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
ref={logbuttonRef}
|
||||
exit={{ y: 40, opacity: 0, rotate: 20, filter: "blur(3px)" }}
|
||||
exit={{ y: 40, opacity: 0, rotate: 20 }}
|
||||
onMouseMove={(e) => {
|
||||
const rect = e.target.getBoundingClientRect();
|
||||
e.target.style.setProperty(
|
||||
@@ -257,10 +248,10 @@ function CoolInputWithHide({
|
||||
exit={{ y: 10, opacity: 0 }}
|
||||
className="flex *:outline-none *:border *:border-transparent *:focus:border-white"
|
||||
>
|
||||
<motion.input
|
||||
<input
|
||||
type={!showAId ? "password" : "text"}
|
||||
name="ad"
|
||||
className="bg-zinc-800/50 p-2 rounded-l-2xl max-w-[19b0px]"
|
||||
className="bg-zinc-800/50 p-2 rounded-l-2xl"
|
||||
placeholder="id админки"
|
||||
autoComplete="off"
|
||||
disabled={isLogining}
|
||||
|
||||
+42
-19
@@ -1,4 +1,4 @@
|
||||
import { motion, AnimatePresence, LayoutGroup } from "motion/react";
|
||||
import { motion, LayoutGroup } from "motion/react";
|
||||
|
||||
import { useAtom } from "jotai";
|
||||
import { useState, useEffect } from "react";
|
||||
@@ -7,20 +7,16 @@ import { AdminUserFRequest, classUserFRequest } from "../trash/req_mgr";
|
||||
import {
|
||||
BiCalendar,
|
||||
BiCalendarEdit,
|
||||
BiCheck,
|
||||
BiExit,
|
||||
BiPencil,
|
||||
BiPlus,
|
||||
BiRedo,
|
||||
BiTime,
|
||||
BiTrash,
|
||||
BiUndo,
|
||||
BiX,
|
||||
} from "react-icons/bi";
|
||||
import { formatDate, parseApiDate } from "../trash/dengi";
|
||||
import {
|
||||
DengiButton,
|
||||
DengiYesOrNoDialog,
|
||||
TextPreloader,
|
||||
// CoolMouseBg
|
||||
} from "../trash/trash-components";
|
||||
import { TableEditor } from "../trash/editorTable";
|
||||
@@ -30,6 +26,7 @@ import { API_BASE_URL } from "../App";
|
||||
export default function EditorPage(props) {
|
||||
const [classId, _setUserId] = useAtom(props.classIDAtom);
|
||||
const [adminId, _setAdminId] = useAtom(props.adminAtom);
|
||||
const [pickmeMode, _setPickmeMode] = useAtom(props.pickmeModeAtom);
|
||||
const [route, setRoute] = useAtom(props.routeAtom);
|
||||
const dateRaw = props.secondPathSegment(route);
|
||||
useEffect(() => {
|
||||
@@ -78,7 +75,7 @@ export default function EditorPage(props) {
|
||||
}, [classId, adminId]);
|
||||
// on rerenderr
|
||||
useEffect(() => {
|
||||
console.log("editor cache clean");
|
||||
// console.log("editor cache clean");
|
||||
props.dengi_cash.delete(
|
||||
"editor_schedule",
|
||||
"editor_changes",
|
||||
@@ -163,9 +160,17 @@ export default function EditorPage(props) {
|
||||
<span>
|
||||
{userFrNames["class"] ? userFrNames["class"] : classId}
|
||||
</span>
|
||||
<span className="block h-[1px] w-[20px] -rotate-45 bg-zinc-600" />
|
||||
<span
|
||||
className={`block h-[1px] w-[20px] -rotate-45 ${
|
||||
pickmeMode ? "bg-pink-600" : "bg-zinc-600"
|
||||
} `}
|
||||
/>
|
||||
<span>
|
||||
{userFrNames["admin"] ? userFrNames["admin"] : adminId}
|
||||
{userFrNames["admin"] ? (
|
||||
userFrNames["admin"]
|
||||
) : (
|
||||
<TextPreloader />
|
||||
)}
|
||||
</span>
|
||||
</motion.span>
|
||||
<motion.span
|
||||
@@ -188,13 +193,20 @@ export default function EditorPage(props) {
|
||||
)
|
||||
}
|
||||
>
|
||||
<BiPencil size={25} />
|
||||
<BiPencil
|
||||
color={pickmeMode ? "#FF0078" : undefined}
|
||||
size={25}
|
||||
/>
|
||||
{formatDate(date)}
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="#/app"
|
||||
className="flex items-center justify-center border p-1 gap-2 px-2 lg:hover:border-white cursor-pointer rounded-xl transition-colors border-zinc-700 text-zinc-400 lg:hover:text-white"
|
||||
className={`flex items-center justify-center border p-1 gap-2 px-2 cursor-pointer rounded-xl transition-colors ${
|
||||
pickmeMode
|
||||
? "border-pink-400/30 text-pink-300/80 lg:hover:text-white lg:hover:border-pink-500"
|
||||
: "border-zinc-700 text-zinc-400 lg:hover:text-white lg:hover:border-white"
|
||||
} `}
|
||||
onClick={(e) => {
|
||||
if (changesWereMade) {
|
||||
e.preventDefault();
|
||||
@@ -207,12 +219,20 @@ export default function EditorPage(props) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center gap-5 select-none bg-grain bg-zinc-800/35 p-2 rounded-xl">
|
||||
<div
|
||||
className={`flex items-center justify-center gap-5 select-none bg-grain ${
|
||||
pickmeMode ? "bg-pink-900/35" : "bg-zinc-800/35"
|
||||
} p-2 rounded-xl`}
|
||||
>
|
||||
<LayoutGroup>
|
||||
{tabs.map((item, idx) => (
|
||||
<div
|
||||
className={
|
||||
"flex items-center justify-center gap-1 p-2 rounded-2xl md:hover:bg-white/10 transition-colors relative md:hover:text-white cursor-pointer " +
|
||||
`flex items-center justify-center gap-1 p-2 rounded-2xl ${
|
||||
pickmeMode
|
||||
? "md:hover:bg-pink-400/10"
|
||||
: "md:hover:bg-white/10"
|
||||
} transition-colors relative md:hover:text-white cursor-pointer ` +
|
||||
(idx == selectedTab ? "text-white" : "text-zinc-500")
|
||||
}
|
||||
key={idx}
|
||||
@@ -230,7 +250,9 @@ export default function EditorPage(props) {
|
||||
{idx == selectedTab && (
|
||||
<motion.div
|
||||
layoutId="editor_tabs_underline"
|
||||
className="absolute -bottom-[4px] left-0 h-[3px] bg-white/70 rounded-full w-full"
|
||||
className={`absolute -bottom-[4px] left-0 h-[3px] ${
|
||||
pickmeMode ? "bg-pink-500/70" : "bg-white/70"
|
||||
} rounded-full w-full`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -246,6 +268,7 @@ export default function EditorPage(props) {
|
||||
setChangesWereMade,
|
||||
date,
|
||||
adminId,
|
||||
pickmeMode,
|
||||
}}
|
||||
dengiCash={props.dengi_cash}
|
||||
/>
|
||||
@@ -255,16 +278,16 @@ export default function EditorPage(props) {
|
||||
</motion.div>
|
||||
{/* <CoolMouseBg /> */}
|
||||
</div>
|
||||
<DefaultEditorBG />
|
||||
<DefaultEditorBG pickmeMode={pickmeMode} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
function DefaultEditorBG() {
|
||||
function DefaultEditorBG({ pickmeMode }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"w-full h-screen bg-gradient-to-br fixed top-0 left-0 via-black via-50% gap-5 overflow-hidden from-white/2 to-blue-500/10 -z-5 bg-fixed"
|
||||
}
|
||||
className={`w-full h-screen bg-gradient-to-br fixed top-0 left-0 via-black via-50% gap-5 overflow-hidden ${
|
||||
!pickmeMode ? "from-white/2" : "from-pink-400/10"
|
||||
} to-blue-500/10 -z-5 bg-fixed`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-207
@@ -1,212 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DndContext, closestCenter, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { TextPreloader } from "../trash/trash-components";
|
||||
|
||||
// Configuration
|
||||
const COLUMNS = [
|
||||
{ key: 'position', label: 'Pos', width: 'w-16', placeholder: '#' },
|
||||
{ key: 'subject', label: 'Subject', width: 'flex-1', placeholder: 'Enter subject' },
|
||||
{ key: 'time', label: 'Time', width: 'w-24', placeholder: '00:00' },
|
||||
{ key: 'room', label: 'Room', width: 'w-20', placeholder: 'A-1' },
|
||||
];
|
||||
|
||||
const INITIAL_DATA = [
|
||||
{ id: '1', position: '1', subject: 'Mathematics', time: '09:00', room: 'A-1' },
|
||||
{ id: '2', position: '2', subject: 'Physics', time: '10:00', room: 'B-2' },
|
||||
{ id: '3', position: '3', subject: 'Chemistry', time: '11:00', room: 'A-3' },
|
||||
{ id: '4', position: '4', subject: 'Biology', time: '13:00', room: 'C-1' },
|
||||
];
|
||||
|
||||
function SortableRow({ item, columns, onUpdate, onDelete }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: item.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
export default function TestPage() {
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="bg-gray-800 rounded-lg overflow-hidden mb-2"
|
||||
>
|
||||
<div className="flex items-stretch">
|
||||
{columns.map((col) => (
|
||||
<input
|
||||
key={col.key}
|
||||
type="text"
|
||||
value={item[col.key] || ''}
|
||||
onChange={(e) => onUpdate(item.id, col.key, e.target.value)}
|
||||
className={`${col.width} px-3 py-3 bg-transparent text-white border-r border-gray-700 last:border-r-0 focus:outline-none focus:bg-gray-700/50 placeholder-gray-500`}
|
||||
placeholder={col.placeholder}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="w-12 bg-gray-900 flex items-center justify-center cursor-move touch-none select-none text-gray-400 hover:text-white"
|
||||
>
|
||||
⋮
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDelete(item.id)}
|
||||
className="w-12 bg-red-600/20 text-red-400 hover:bg-red-600/40 hover:text-red-300 active:bg-red-600/60"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<>
|
||||
<div className="bg-black w-screen h-screen flex items-center justify-center">
|
||||
<TextPreloader />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ScheduleEditor({
|
||||
initialData = INITIAL_DATA,
|
||||
columns = COLUMNS,
|
||||
onChange,
|
||||
}) {
|
||||
const [items, setItems] = useState(initialData);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 200,
|
||||
tolerance: 8,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const updateItems = (newItems) => {
|
||||
setItems(newItems);
|
||||
onChange?.(newItems);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id);
|
||||
const newIndex = items.findIndex((item) => item.id === over.id);
|
||||
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
|
||||
// Update positions based on new order
|
||||
const positions = newItems.map(item => item.position);
|
||||
const reorderedItems = newItems.map((item, idx) => ({
|
||||
...item,
|
||||
position: positions[idx]
|
||||
}));
|
||||
|
||||
updateItems(reorderedItems);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = (id, field, value) => {
|
||||
const newItems = items.map(item =>
|
||||
item.id === id ? { ...item, [field]: value } : item
|
||||
);
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
const newItems = items.filter(item => item.id !== id);
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
const newId = String(Date.now());
|
||||
const lastPosition = items.length > 0
|
||||
? String(parseInt(items[items.length - 1].position || '0') + 1)
|
||||
: '1';
|
||||
|
||||
const newItem = { id: newId, position: lastPosition };
|
||||
columns.forEach(col => {
|
||||
if (col.key !== 'position') {
|
||||
newItem[col.key] = '';
|
||||
}
|
||||
});
|
||||
|
||||
updateItems([...items, newItem]);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
updateItems([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 p-4 pb-24">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-6 text-white">
|
||||
Schedule Editor
|
||||
</h1>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={items.map(item => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div>
|
||||
{items.map((item) => (
|
||||
<SortableRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
columns={columns}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{items.length === 0 && (
|
||||
<div className="border-2 border-dashed border-gray-700 rounded-lg p-12 text-center text-gray-500">
|
||||
No items. Add a row to get started.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex gap-3">
|
||||
<button
|
||||
onClick={handleAddRow}
|
||||
className="flex-1 bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
+ Add Row
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="px-6 py-3 rounded-lg border-2 border-gray-700 text-gray-300 font-semibold hover:bg-gray-800 active:bg-gray-700"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 rounded-lg bg-gray-800 text-sm text-gray-300">
|
||||
<p className="font-bold mb-2 text-white">Instructions:</p>
|
||||
<ul className="space-y-1">
|
||||
<li>• Drag rows by the handle (⋮) on the right to reorder</li>
|
||||
<li>• Positions move with their rows when dragged</li>
|
||||
<li>• Click any field to edit</li>
|
||||
<li>• Click × to delete a row</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DndContext, closestCenter, PointerSensor, TouchSensor, useSensor, useSensors } from '@dnd-kit/core';
|
||||
import { arrayMove, SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
|
||||
// Configuration
|
||||
const COLUMNS = [
|
||||
{ key: 'position', label: 'Pos', width: 'w-16', placeholder: '#' },
|
||||
{ key: 'subject', label: 'Subject', width: 'flex-1', placeholder: 'Enter subject' },
|
||||
{ key: 'time', label: 'Time', width: 'w-24', placeholder: '00:00' },
|
||||
{ key: 'room', label: 'Room', width: 'w-20', placeholder: 'A-1' },
|
||||
];
|
||||
|
||||
const INITIAL_DATA = [
|
||||
{ id: '1', position: '1', subject: 'Mathematics', time: '09:00', room: 'A-1' },
|
||||
{ id: '2', position: '2', subject: 'Physics', time: '10:00', room: 'B-2' },
|
||||
{ id: '3', position: '3', subject: 'Chemistry', time: '11:00', room: 'A-3' },
|
||||
{ id: '4', position: '4', subject: 'Biology', time: '13:00', room: 'C-1' },
|
||||
];
|
||||
|
||||
function SortableRow({ item, columns, onUpdate, onDelete }) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: item.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="bg-gray-800 rounded-lg overflow-hidden mb-2"
|
||||
>
|
||||
<div className="flex items-stretch">
|
||||
{columns.map((col) => (
|
||||
<input
|
||||
key={col.key}
|
||||
type="text"
|
||||
value={item[col.key] || ''}
|
||||
onChange={(e) => onUpdate(item.id, col.key, e.target.value)}
|
||||
className={`${col.width} px-3 py-3 bg-transparent text-white border-r border-gray-700 last:border-r-0 focus:outline-none focus:bg-gray-700/50 placeholder-gray-500`}
|
||||
placeholder={col.placeholder}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="w-12 bg-gray-900 flex items-center justify-center cursor-move touch-none select-none text-gray-400 hover:text-white"
|
||||
>
|
||||
⋮
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDelete(item.id)}
|
||||
className="w-12 bg-red-600/20 text-red-400 hover:bg-red-600/40 hover:text-red-300 active:bg-red-600/60"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ScheduleEditor({
|
||||
initialData = INITIAL_DATA,
|
||||
columns = COLUMNS,
|
||||
onChange,
|
||||
}) {
|
||||
const [items, setItems] = useState(initialData);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint: {
|
||||
delay: 200,
|
||||
tolerance: 8,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const updateItems = (newItems) => {
|
||||
setItems(newItems);
|
||||
onChange?.(newItems);
|
||||
};
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (over && active.id !== over.id) {
|
||||
const oldIndex = items.findIndex((item) => item.id === active.id);
|
||||
const newIndex = items.findIndex((item) => item.id === over.id);
|
||||
|
||||
const newItems = arrayMove(items, oldIndex, newIndex);
|
||||
|
||||
// Update positions based on new order
|
||||
const positions = newItems.map(item => item.position);
|
||||
const reorderedItems = newItems.map((item, idx) => ({
|
||||
...item,
|
||||
position: positions[idx]
|
||||
}));
|
||||
|
||||
updateItems(reorderedItems);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = (id, field, value) => {
|
||||
const newItems = items.map(item =>
|
||||
item.id === id ? { ...item, [field]: value } : item
|
||||
);
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
const newItems = items.filter(item => item.id !== id);
|
||||
updateItems(newItems);
|
||||
};
|
||||
|
||||
const handleAddRow = () => {
|
||||
const newId = String(Date.now());
|
||||
const lastPosition = items.length > 0
|
||||
? String(parseInt(items[items.length - 1].position || '0') + 1)
|
||||
: '1';
|
||||
|
||||
const newItem = { id: newId, position: lastPosition };
|
||||
columns.forEach(col => {
|
||||
if (col.key !== 'position') {
|
||||
newItem[col.key] = '';
|
||||
}
|
||||
});
|
||||
|
||||
updateItems([...items, newItem]);
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
updateItems([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 p-4 pb-24">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<h1 className="text-2xl font-bold mb-6 text-white">
|
||||
Schedule Editor
|
||||
</h1>
|
||||
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={items.map(item => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div>
|
||||
{items.map((item) => (
|
||||
<SortableRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
columns={columns}
|
||||
onUpdate={handleUpdate}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
{items.length === 0 && (
|
||||
<div className="border-2 border-dashed border-gray-700 rounded-lg p-12 text-center text-gray-500">
|
||||
No items. Add a row to get started.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 flex gap-3">
|
||||
<button
|
||||
onClick={handleAddRow}
|
||||
className="flex-1 bg-blue-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-blue-700 active:bg-blue-800"
|
||||
>
|
||||
+ Add Row
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="px-6 py-3 rounded-lg border-2 border-gray-700 text-gray-300 font-semibold hover:bg-gray-800 active:bg-gray-700"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 rounded-lg bg-gray-800 text-sm text-gray-300">
|
||||
<p className="font-bold mb-2 text-white">Instructions:</p>
|
||||
<ul className="space-y-1">
|
||||
<li>• Drag rows by the handle (⋮) on the right to reorder</li>
|
||||
<li>• Positions move with their rows when dragged</li>
|
||||
<li>• Click any field to edit</li>
|
||||
<li>• Click × to delete a row</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+32
-4
@@ -53,7 +53,11 @@ export function usToRussianDay(usDay) {
|
||||
export function formatDate(date) {
|
||||
return `${
|
||||
dayOfWeeksNames[usToRussianDay(date.getDay())]
|
||||
}, ${date.getDate()} ${monthNames[date.getMonth()]}`;
|
||||
}, ${date.getDate()} ${monthNames[date.getMonth()]}${
|
||||
date.getFullYear() != new Date().getFullYear()
|
||||
? " " + date.getFullYear()
|
||||
: ""
|
||||
}`;
|
||||
}
|
||||
export function apiFormatDate(date) {
|
||||
return (
|
||||
@@ -128,9 +132,33 @@ export function convertEditorToApi(arr) {
|
||||
}
|
||||
|
||||
export function isTimeBeforeNow(timeStr) {
|
||||
const [h, m] = timeStr.split(':').map(Number);
|
||||
const [h, m] = timeStr.split(":").map(Number);
|
||||
if (!Number.isFinite(h) || !Number.isFinite(m)) return false;
|
||||
const now = new Date();
|
||||
const t = new Date(now.getFullYear(), now.getMonth(), now.getDate(), h, m, 0, 0);
|
||||
const t = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
h,
|
||||
m,
|
||||
0,
|
||||
0
|
||||
);
|
||||
return t.getTime() < now.getTime();
|
||||
}
|
||||
}
|
||||
export const premsMeanings = {
|
||||
"-1": "неограниченый",
|
||||
// 0: "reserved"
|
||||
1: "обычный",
|
||||
2: "управляющий",
|
||||
};
|
||||
|
||||
export const AvailableSymbols = (() => {
|
||||
const a = "abcdefghijklmnopqrstuvwxyz"; // alphabet
|
||||
const A = a.toUpperCase(); // ALPHABET
|
||||
const n = "1234567890"; // numbers
|
||||
const s = "_-"; // specials
|
||||
return (a + n + A + s).split("");
|
||||
})();
|
||||
|
||||
export const AvailableAdminSymbols = [...AvailableSymbols, ..."$()*^%@!+=.".split("")];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@@ -155,6 +155,7 @@ function SortableRow({
|
||||
onDuplicate,
|
||||
setFocusEdit,
|
||||
setHoverEdit,
|
||||
pickmeMode,
|
||||
}) {
|
||||
const {
|
||||
attributes,
|
||||
@@ -175,7 +176,9 @@ function SortableRow({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="bg-zinc-900 bg-grain rounded-lg mb-2"
|
||||
className={`${
|
||||
pickmeMode ? "bg-pink-500/35" : "bg-zinc-900"
|
||||
} bg-grain rounded-lg mb-2`}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex">
|
||||
@@ -188,7 +191,11 @@ function SortableRow({
|
||||
wrap="hard"
|
||||
rows={1}
|
||||
onChange={(e) => onUpdate(item.id, col.key, e.target.value)}
|
||||
className={`px-1 py-3 text-center bg-transparent text-white border-r border-zinc-700 last:border-r-0 last:rounded-tr-lg focus:outline-none focus:bg-gray-700/50 placeholder-gray-500 transition-colors hover:bg-gray-700/20 first:rounded-tl-lg max-h-min resize-none block min-w-0 grow ${col.width}`}
|
||||
className={`px-1 py-3 text-center bg-transparent text-white border-r ${
|
||||
pickmeMode ? "border-pink-400/50" : "border-zinc-700"
|
||||
} last:border-r-0 last:rounded-tr-lg focus:outline-none focus:bg-gray-700/50 placeholder-gray-500 transition-colors hover:bg-gray-700/20 first:rounded-tl-lg max-h-min resize-none block min-w-0 grow ${
|
||||
col.width
|
||||
}`}
|
||||
placeholder={col.placeholder}
|
||||
onFocus={() => setFocusEdit(col.key)}
|
||||
onBlur={() => setFocusEdit(null)}
|
||||
@@ -244,6 +251,7 @@ export function TableEditor({
|
||||
adminId,
|
||||
date,
|
||||
dengiCash,
|
||||
pickmeMode = false,
|
||||
}) {
|
||||
const [items, setItems] = useState(initialData);
|
||||
const [undoItems, setUndoItems] = useState([]);
|
||||
@@ -553,7 +561,9 @@ export function TableEditor({
|
||||
|
||||
<span
|
||||
key={col.key + i + "divider"}
|
||||
className="w-4 h-[1px] bg-zinc-600 -rotate-45 last:hidden"
|
||||
className={`w-4 h-[1px] ${
|
||||
pickmeMode ? "bg-pink-600" : "bg-zinc-600"
|
||||
} -rotate-45 last:hidden`}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
@@ -585,6 +595,7 @@ export function TableEditor({
|
||||
onDuplicate={handleDuplicate}
|
||||
setFocusEdit={setFocusEdit}
|
||||
setHoverEdit={setHoverEdit}
|
||||
pickmeMode={pickmeMode}
|
||||
/>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -600,7 +611,9 @@ export function TableEditor({
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
}}
|
||||
className="border-2 border-dashed border-gray-700 rounded-lg p-12 text-center text-gray-500"
|
||||
className={`border-2 border-dashed ${
|
||||
pickmeMode ? "border-pink-700" : "border-gray-700"
|
||||
} rounded-lg p-12 text-center text-gray-500`}
|
||||
>
|
||||
пусто
|
||||
</motion.div>
|
||||
|
||||
+19
-73
@@ -1,80 +1,26 @@
|
||||
import { makeRequest } from "./req_mgr";
|
||||
|
||||
export async function EditorPrefillRequest(host, tab, adminid, date) {
|
||||
try {
|
||||
const req = await fetch(host + "?editor_data", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
adminId: adminid,
|
||||
tab: tab,
|
||||
day: date,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (
|
||||
req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
) {
|
||||
return [1, await req.json()];
|
||||
} else {
|
||||
return [req.status, await req.text()];
|
||||
}
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
return makeRequest(host + "?editor_data", {
|
||||
adminId: adminid,
|
||||
tab,
|
||||
day: date,
|
||||
});
|
||||
}
|
||||
export async function EditorPublishRequest(host, tab, adminid, date, newData) {
|
||||
try {
|
||||
const req = await fetch(host + "?editor_publish", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
adminId: adminid,
|
||||
tab: tab,
|
||||
day: date,
|
||||
newData: newData,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (
|
||||
req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
) {
|
||||
return [1, await req.json()];
|
||||
} else {
|
||||
return [req.status, await req.text()];
|
||||
}
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
export async function EditorPublishRequest(host, tab, adminid, date, newData) {
|
||||
return makeRequest(host + "?editor_publish", {
|
||||
adminId: adminid,
|
||||
tab,
|
||||
day: date,
|
||||
newData,
|
||||
});
|
||||
}
|
||||
|
||||
export async function EditorRemoveRequest(host, tab, adminid, date) {
|
||||
try {
|
||||
const req = await fetch(host + "?editor_fullitems_del", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
adminId: adminid,
|
||||
tab: tab,
|
||||
day: date,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (
|
||||
req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
) {
|
||||
return [1, await req.json()];
|
||||
} else {
|
||||
return [req.status, await req.text()];
|
||||
}
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
return makeRequest(host + "?editor_fullitems_del", {
|
||||
adminId: adminid,
|
||||
tab,
|
||||
day: date,
|
||||
});
|
||||
}
|
||||
|
||||
+28
-21
@@ -23,10 +23,7 @@ export function b64d(str) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/** by claude
|
||||
* Universal copy-to-clipboard function
|
||||
* Works with modern Clipboard API and falls back to legacy methods
|
||||
* @param {string} text - The text to copy to clipboard
|
||||
* @returns {Promise<boolean>} - Returns true if successful, false otherwise
|
||||
*/
|
||||
@@ -37,34 +34,44 @@ export async function copyToClipboard(text) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('Clipboard API failed, trying fallback:', err);
|
||||
console.warn("Clipboard API failed, trying fallback:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// Method 2: Fallback for older browsers or non-secure contexts
|
||||
try {
|
||||
const textarea = document.createElement('textarea');
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
|
||||
// Make it invisible and non-intrusive
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.left = '-999999px';
|
||||
textarea.style.top = '-999999px';
|
||||
textarea.style.opacity = '0';
|
||||
textarea.setAttribute('readonly', '');
|
||||
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.left = "-999999px";
|
||||
textarea.style.top = "-999999px";
|
||||
textarea.style.opacity = "0";
|
||||
textarea.setAttribute("readonly", "");
|
||||
document.body.appendChild(textarea);
|
||||
|
||||
// Select and copy
|
||||
textarea.select();
|
||||
textarea.setSelectionRange(0, text.length);
|
||||
|
||||
const success = document.execCommand('copy');
|
||||
const success = document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
|
||||
return success;
|
||||
} catch (err) {
|
||||
console.error('All copy methods failed:', err);
|
||||
console.error("All copy methods failed:", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dx4 specific
|
||||
export async function copyDXLink(classId, adminId = undefined) {
|
||||
return copyToClipboard(
|
||||
location.href.substring(0, location.href.search("#")) +
|
||||
"#//" +
|
||||
classId +
|
||||
(adminId ? ";" + b64e(adminId) : "")
|
||||
);
|
||||
}
|
||||
|
||||
export const randomString = (length, symbols) => {
|
||||
const len = symbols.length;
|
||||
return Array.from(
|
||||
{ length },
|
||||
() => symbols[Math.floor(Math.random() * len)]
|
||||
).join("");
|
||||
};
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
/**
|
||||
* MD5 hash implementation
|
||||
* Based on: http://www.myersdaily.org/joseph/javascript/md5-text.html
|
||||
*/
|
||||
|
||||
const HEX_CHARS = '0123456789abcdef';
|
||||
|
||||
// Pre-compute add32 function based on environment
|
||||
const add32 = (() => {
|
||||
const test = (a, b) => (a + b) & 0xFFFFFFFF;
|
||||
// Test if simple addition works correctly
|
||||
if (test(0x80000000, 0x80000000) === 0) {
|
||||
return test;
|
||||
}
|
||||
// Fallback for environments with non-standard 32-bit arithmetic
|
||||
return (x, y) => {
|
||||
const lsw = (x & 0xFFFF) + (y & 0xFFFF);
|
||||
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
|
||||
return (msw << 16) | (lsw & 0xFFFF);
|
||||
};
|
||||
})();
|
||||
|
||||
const cmn = (q, a, b, x, s, t) => {
|
||||
a = add32(add32(a, q), add32(x, t));
|
||||
return add32((a << s) | (a >>> (32 - s)), b);
|
||||
};
|
||||
|
||||
const ff = (a, b, c, d, x, s, t) => cmn((b & c) | ((~b) & d), a, b, x, s, t);
|
||||
const gg = (a, b, c, d, x, s, t) => cmn((b & d) | (c & (~d)), a, b, x, s, t);
|
||||
const hh = (a, b, c, d, x, s, t) => cmn(b ^ c ^ d, a, b, x, s, t);
|
||||
const ii = (a, b, c, d, x, s, t) => cmn(c ^ (b | (~d)), a, b, x, s, t);
|
||||
|
||||
const md5blk = (s) => {
|
||||
const blks = [];
|
||||
for (let i = 0; i < 64; i += 4) {
|
||||
blks[i >> 2] = s.charCodeAt(i) +
|
||||
(s.charCodeAt(i + 1) << 8) +
|
||||
(s.charCodeAt(i + 2) << 16) +
|
||||
(s.charCodeAt(i + 3) << 24);
|
||||
}
|
||||
return blks;
|
||||
};
|
||||
|
||||
const md5cycle = (x, k) => {
|
||||
let a = x[0], b = x[1], c = x[2], d = x[3];
|
||||
|
||||
a = ff(a, b, c, d, k[0], 7, -680876936);
|
||||
d = ff(d, a, b, c, k[1], 12, -389564586);
|
||||
c = ff(c, d, a, b, k[2], 17, 606105819);
|
||||
b = ff(b, c, d, a, k[3], 22, -1044525330);
|
||||
a = ff(a, b, c, d, k[4], 7, -176418897);
|
||||
d = ff(d, a, b, c, k[5], 12, 1200080426);
|
||||
c = ff(c, d, a, b, k[6], 17, -1473231341);
|
||||
b = ff(b, c, d, a, k[7], 22, -45705983);
|
||||
a = ff(a, b, c, d, k[8], 7, 1770035416);
|
||||
d = ff(d, a, b, c, k[9], 12, -1958414417);
|
||||
c = ff(c, d, a, b, k[10], 17, -42063);
|
||||
b = ff(b, c, d, a, k[11], 22, -1990404162);
|
||||
a = ff(a, b, c, d, k[12], 7, 1804603682);
|
||||
d = ff(d, a, b, c, k[13], 12, -40341101);
|
||||
c = ff(c, d, a, b, k[14], 17, -1502002290);
|
||||
b = ff(b, c, d, a, k[15], 22, 1236535329);
|
||||
|
||||
a = gg(a, b, c, d, k[1], 5, -165796510);
|
||||
d = gg(d, a, b, c, k[6], 9, -1069501632);
|
||||
c = gg(c, d, a, b, k[11], 14, 643717713);
|
||||
b = gg(b, c, d, a, k[0], 20, -373897302);
|
||||
a = gg(a, b, c, d, k[5], 5, -701558691);
|
||||
d = gg(d, a, b, c, k[10], 9, 38016083);
|
||||
c = gg(c, d, a, b, k[15], 14, -660478335);
|
||||
b = gg(b, c, d, a, k[4], 20, -405537848);
|
||||
a = gg(a, b, c, d, k[9], 5, 568446438);
|
||||
d = gg(d, a, b, c, k[14], 9, -1019803690);
|
||||
c = gg(c, d, a, b, k[3], 14, -187363961);
|
||||
b = gg(b, c, d, a, k[8], 20, 1163531501);
|
||||
a = gg(a, b, c, d, k[13], 5, -1444681467);
|
||||
d = gg(d, a, b, c, k[2], 9, -51403784);
|
||||
c = gg(c, d, a, b, k[7], 14, 1735328473);
|
||||
b = gg(b, c, d, a, k[12], 20, -1926607734);
|
||||
|
||||
a = hh(a, b, c, d, k[5], 4, -378558);
|
||||
d = hh(d, a, b, c, k[8], 11, -2022574463);
|
||||
c = hh(c, d, a, b, k[11], 16, 1839030562);
|
||||
b = hh(b, c, d, a, k[14], 23, -35309556);
|
||||
a = hh(a, b, c, d, k[1], 4, -1530992060);
|
||||
d = hh(d, a, b, c, k[4], 11, 1272893353);
|
||||
c = hh(c, d, a, b, k[7], 16, -155497632);
|
||||
b = hh(b, c, d, a, k[10], 23, -1094730640);
|
||||
a = hh(a, b, c, d, k[13], 4, 681279174);
|
||||
d = hh(d, a, b, c, k[0], 11, -358537222);
|
||||
c = hh(c, d, a, b, k[3], 16, -722521979);
|
||||
b = hh(b, c, d, a, k[6], 23, 76029189);
|
||||
a = hh(a, b, c, d, k[9], 4, -640364487);
|
||||
d = hh(d, a, b, c, k[12], 11, -421815835);
|
||||
c = hh(c, d, a, b, k[15], 16, 530742520);
|
||||
b = hh(b, c, d, a, k[2], 23, -995338651);
|
||||
|
||||
a = ii(a, b, c, d, k[0], 6, -198630844);
|
||||
d = ii(d, a, b, c, k[7], 10, 1126891415);
|
||||
c = ii(c, d, a, b, k[14], 15, -1416354905);
|
||||
b = ii(b, c, d, a, k[5], 21, -57434055);
|
||||
a = ii(a, b, c, d, k[12], 6, 1700485571);
|
||||
d = ii(d, a, b, c, k[3], 10, -1894986606);
|
||||
c = ii(c, d, a, b, k[10], 15, -1051523);
|
||||
b = ii(b, c, d, a, k[1], 21, -2054922799);
|
||||
a = ii(a, b, c, d, k[8], 6, 1873313359);
|
||||
d = ii(d, a, b, c, k[15], 10, -30611744);
|
||||
c = ii(c, d, a, b, k[6], 15, -1560198380);
|
||||
b = ii(b, c, d, a, k[13], 21, 1309151649);
|
||||
a = ii(a, b, c, d, k[4], 6, -145523070);
|
||||
d = ii(d, a, b, c, k[11], 10, -1120210379);
|
||||
c = ii(c, d, a, b, k[2], 15, 718787259);
|
||||
b = ii(b, c, d, a, k[9], 21, -343485551);
|
||||
|
||||
x[0] = add32(a, x[0]);
|
||||
x[1] = add32(b, x[1]);
|
||||
x[2] = add32(c, x[2]);
|
||||
x[3] = add32(d, x[3]);
|
||||
};
|
||||
|
||||
const rhex = (n) => {
|
||||
let s = '';
|
||||
for (let j = 0; j < 4; j++) {
|
||||
s += HEX_CHARS[(n >> (j * 8 + 4)) & 0x0F] + HEX_CHARS[(n >> (j * 8)) & 0x0F];
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
const hex = (x) => x.map(rhex).join('');
|
||||
|
||||
const md51 = (s) => {
|
||||
const n = s.length;
|
||||
const state = [1732584193, -271733879, -1732584194, 271733878];
|
||||
|
||||
let i;
|
||||
for (i = 64; i <= n; i += 64) {
|
||||
md5cycle(state, md5blk(s.substring(i - 64, i)));
|
||||
}
|
||||
|
||||
s = s.substring(i - 64);
|
||||
const tail = new Array(16).fill(0);
|
||||
|
||||
for (i = 0; i < s.length; i++) {
|
||||
tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3);
|
||||
}
|
||||
|
||||
tail[i >> 2] |= 0x80 << ((i % 4) << 3);
|
||||
|
||||
if (i > 55) {
|
||||
md5cycle(state, tail);
|
||||
tail.fill(0);
|
||||
}
|
||||
|
||||
tail[14] = n * 8;
|
||||
md5cycle(state, tail);
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate MD5 hash of a string
|
||||
* @param {string} s - Input string
|
||||
* @returns {string} MD5 hash in hexadecimal format
|
||||
*/
|
||||
export function md5(s) {
|
||||
return hex(md51(s));
|
||||
}
|
||||
+41
-115
@@ -1,127 +1,53 @@
|
||||
import { apiFormatDate } from "./dengi";
|
||||
|
||||
export async function makeRequest(url, params) {
|
||||
try {
|
||||
const req = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams(params).toString(),
|
||||
});
|
||||
|
||||
return req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
? [1, await req.json()]
|
||||
: [req.status, await req.text()];
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
}
|
||||
|
||||
export async function LoginReq(host, username) {
|
||||
try {
|
||||
const req = await fetch(host + "?login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded", // 💀 важный заголовок
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
classId: username,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (
|
||||
req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
) {
|
||||
return [1, await req.json()];
|
||||
} else {
|
||||
return [req.status, await req.text()];
|
||||
}
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
return makeRequest(host + "?login", { classId: username });
|
||||
}
|
||||
|
||||
export async function RaspGet(host, classId, day) {
|
||||
try {
|
||||
const req = await fetch(host + "?req_client_schedule", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded", // 💀 важный заголовок
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
classId: classId,
|
||||
day: apiFormatDate(day),
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (
|
||||
req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
) {
|
||||
return [1, await req.json()];
|
||||
} else {
|
||||
return [req.status, await req.text()];
|
||||
}
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
}
|
||||
export async function AdminReq(host, adminid) {
|
||||
try {
|
||||
const req = await fetch(host + "?admin_login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded", // 💀 dengi
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
adminId: adminid,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (
|
||||
req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
) {
|
||||
return [1, await req.json()];
|
||||
} else {
|
||||
return [req.status, await req.text()];
|
||||
}
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
return makeRequest(host + "?req_client_schedule", {
|
||||
classId,
|
||||
day: apiFormatDate(day),
|
||||
});
|
||||
}
|
||||
|
||||
export async function AdminUserFRequest(host, adminid) {
|
||||
try {
|
||||
const req = await fetch(host + "?get_admin_user_f", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
adminId: adminid,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (
|
||||
req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
) {
|
||||
return [1, await req.json()];
|
||||
} else {
|
||||
return [req.status, await req.text()];
|
||||
}
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
export async function AdminReq(host, adminId) {
|
||||
return makeRequest(host + "?admin_login", { adminId });
|
||||
}
|
||||
|
||||
export async function classUserFRequest(host, classid) {
|
||||
try {
|
||||
const req = await fetch(host + "?get_class_user_f", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
classId: classid,
|
||||
}).toString(),
|
||||
});
|
||||
|
||||
if (
|
||||
req.ok &&
|
||||
req.headers.get("Content-Type")?.includes("application/json")
|
||||
) {
|
||||
return [1, await req.json()];
|
||||
} else {
|
||||
return [req.status, await req.text()];
|
||||
}
|
||||
} catch (err) {
|
||||
return [-1, err];
|
||||
}
|
||||
export async function AdminUserFRequest(host, adminId) {
|
||||
return makeRequest(host + "?get_admin_user_f", { adminId });
|
||||
}
|
||||
|
||||
export async function classUserFRequest(host, classId) {
|
||||
return makeRequest(host + "?get_class_user_f", { classId });
|
||||
}
|
||||
|
||||
/**@param {{action:string,person?:string,value?}} data */
|
||||
export async function AdminUpdateRequest(host, adminId, data) {
|
||||
return makeRequest(host + "?update_admin", { adminId, ...data });
|
||||
}
|
||||
/**@param {{action:string,value?}} data */
|
||||
export async function ClassUpdateRequest(host, adminId, data) {
|
||||
return makeRequest(host + "?classprofile_edit", { adminId, ...data });
|
||||
}
|
||||
export async function getClassProfile(host, adminId) {
|
||||
return makeRequest(host + "?classprofile_view", { adminId });
|
||||
}
|
||||
|
||||
+179
-27
@@ -1,8 +1,15 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BiCheck, BiLeftArrowAlt, BiRightArrowAlt, BiX } from "react-icons/bi";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
BiCheck,
|
||||
BiHide,
|
||||
BiLeftArrowAlt,
|
||||
BiRightArrowAlt,
|
||||
BiShow,
|
||||
BiX,
|
||||
} from "react-icons/bi";
|
||||
import { formatDate, getDateTags } from "./dengi.js";
|
||||
import { AnimatePresence, motion, LayoutGroup } from "motion/react";
|
||||
|
||||
import trashcss from "./trash.module.css";
|
||||
export function Switch({
|
||||
checked,
|
||||
defaultChecked = false,
|
||||
@@ -67,7 +74,7 @@ export function Switch({
|
||||
);
|
||||
}
|
||||
|
||||
export function LoadingSpinner(props) {
|
||||
export function LoadingSpinner({ size }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -75,8 +82,8 @@ export function LoadingSpinner(props) {
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={props.size}
|
||||
height={props.size}
|
||||
width={size}
|
||||
height={size}
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
@@ -90,10 +97,14 @@ export function LoadingSpinner(props) {
|
||||
);
|
||||
}
|
||||
|
||||
function DatePickerButton({ next = false, date, setDate }) {
|
||||
function DatePickerButton({ next = false, date, setDate, pink = false }) {
|
||||
return (
|
||||
<span
|
||||
className="cursor-pointer lg:hover:*:fill-black lg:hover:bg-white transition *:transition-colors rounded-full"
|
||||
className={`cursor-pointer ${
|
||||
!pink
|
||||
? "lg:hover:*:fill-black lg:hover:bg-white"
|
||||
: "lg:hover:*:fill-white lg:hover:bg-pink-500"
|
||||
} transition *:transition-colors rounded-full`}
|
||||
onClick={() => {
|
||||
const d = new Date(date);
|
||||
d.setDate(d.getDate() + (next ? 1 : -1));
|
||||
@@ -105,17 +116,29 @@ function DatePickerButton({ next = false, date, setDate }) {
|
||||
);
|
||||
}
|
||||
|
||||
export const DatePicker = React.memo((props) => {
|
||||
export const DatePicker = (props) => {
|
||||
const dateTag = getDateTags(props.date);
|
||||
|
||||
return (
|
||||
<LayoutGroup>
|
||||
<div className="flex items-center justify-between px-2 py-1 w-full bg-grain border-zinc-800/40 border rounded-2xl relative">
|
||||
<DatePickerButton date={props.date} setDate={props.setDate} />
|
||||
<div
|
||||
className={`flex items-center justify-between px-2 py-1 w-full bg-grain ${
|
||||
!props.pink ? "border-zinc-800/40" : "border-pink-500/20"
|
||||
} border rounded-2xl relative`}
|
||||
>
|
||||
<DatePickerButton
|
||||
date={props.date}
|
||||
setDate={props.setDate}
|
||||
pink={props.pink}
|
||||
/>
|
||||
<AnimatePresence mode="popLayout">
|
||||
<motion.span
|
||||
key={formatDate(props.date)}
|
||||
className="cursor-pointer flex items-center gap-1 justify-center w-[80%] text-zinc-300 lg:hover:text-black *:transition-colors transition-colors p-1 rounded-3xl lg:hover:bg-white px-3 overflow-hidden lg:hover:*:text-black"
|
||||
className={`cursor-pointer flex items-center gap-1 justify-center w-[80%] lg:hover:text-black *:transition-colors transition-colors p-1 rounded-3xl text-zinc-300 ${
|
||||
!props.pink
|
||||
? "lg:hover:bg-white lg:hover:*:text-black"
|
||||
: "lg:hover:bg-pink-500 lg:hover:*:text-white"
|
||||
} px-3 overflow-hidden `}
|
||||
variants={{
|
||||
a: { opacity: 0, filter: "blur(1px)" },
|
||||
b: { opacity: 1, filter: "blur(0px)" },
|
||||
@@ -131,7 +154,9 @@ export const DatePicker = React.memo((props) => {
|
||||
className={
|
||||
"block min-w-2 min-h-2 rounded-full mx-1 transition-colors" +
|
||||
(props.indicator
|
||||
? " bg-zinc-300 animate-pulse"
|
||||
? props.pink
|
||||
? " bg-pink-500 animate-pulse"
|
||||
: " bg-zinc-300 animate-pulse"
|
||||
: " bg-transparent")
|
||||
}
|
||||
layoutId="datepicker_indicator"
|
||||
@@ -151,7 +176,11 @@ export const DatePicker = React.memo((props) => {
|
||||
layoutId="datepicker_date_div"
|
||||
layout="position"
|
||||
>
|
||||
<span className="h-[1px] -rotate-50 bg-zinc-800 w-[20px] " />
|
||||
<span
|
||||
className={`h-[1px] -rotate-50 ${
|
||||
props.pink ? "bg-pink-400/50" : "bg-zinc-800"
|
||||
} w-[20px] `}
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.span
|
||||
initial={{ x: 10, opacity: 0 }}
|
||||
@@ -165,11 +194,16 @@ export const DatePicker = React.memo((props) => {
|
||||
)}
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
<DatePickerButton next date={props.date} setDate={props.setDate} />
|
||||
<DatePickerButton
|
||||
next
|
||||
date={props.date}
|
||||
setDate={props.setDate}
|
||||
pink={props.pink}
|
||||
/>
|
||||
</div>
|
||||
</LayoutGroup>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export function DengiButton({
|
||||
onClick,
|
||||
@@ -235,23 +269,24 @@ export function DengiButton({
|
||||
// );
|
||||
// }
|
||||
|
||||
const defaultDialogContent = {
|
||||
message: "?",
|
||||
yesBtn: "da",
|
||||
noBtn: "net",
|
||||
bgDissmis: true,
|
||||
yesBlocked: false,
|
||||
yesIcon: <BiCheck />,
|
||||
noIcon: <BiX />,
|
||||
noBtnStyle: "",
|
||||
yesBtnStyle: "",
|
||||
};
|
||||
export function DengiYesOrNoDialog({
|
||||
content = {
|
||||
message: "?",
|
||||
yesBtn: "da",
|
||||
noBtn: "net",
|
||||
bgDissmis: true,
|
||||
yesIcon: <BiCheck />,
|
||||
noIcon: <BiX />,
|
||||
noBtnStyle: "",
|
||||
yesBtnStyle: "",
|
||||
},
|
||||
content = defaultDialogContent,
|
||||
show,
|
||||
onAnswer,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{/* Background */}
|
||||
<AnimatePresence>
|
||||
{show && (
|
||||
<motion.div
|
||||
@@ -280,6 +315,7 @@ export function DengiYesOrNoDialog({
|
||||
<DengiButton
|
||||
className={content.yesBtnStyle + " w-full"}
|
||||
onClick={() => onAnswer(true)}
|
||||
disabled={content.yesBlocked}
|
||||
>
|
||||
{content.yesIcon}
|
||||
{content.yesBtn}
|
||||
@@ -299,3 +335,119 @@ export function DengiYesOrNoDialog({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function DengiEditable({
|
||||
value,
|
||||
password = false,
|
||||
disWHidden = true,
|
||||
disabled = false,
|
||||
showLoading = true,
|
||||
minLenght = 1,
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onSubmit = ({ value, setIdle }) => {},
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
onInput = ({ value, setIdle }) => {},
|
||||
pink = false,
|
||||
}) {
|
||||
const [val, setVal] = useState(value);
|
||||
const [hid, setHid] = useState(true);
|
||||
const [idle, setIdle] = useState(false);
|
||||
useEffect(() => {
|
||||
setVal(value);
|
||||
}, [value]);
|
||||
return (
|
||||
<form
|
||||
action="#"
|
||||
className="flex rounded-xl overflow-hidden max-w-50 h-8 relative"
|
||||
onSubmit={(e) => {
|
||||
if (idle) return;
|
||||
e.preventDefault();
|
||||
onSubmit({ value: val, setIdle });
|
||||
}}
|
||||
>
|
||||
{value !== false || !showLoading ? (
|
||||
<input
|
||||
type={hid && password ? "password" : "text"}
|
||||
value={val}
|
||||
minLength={minLenght}
|
||||
disabled={(disWHidden && password && hid) || disabled || idle}
|
||||
className={`${pink ? "bg-pink-700/20" : "bg-zinc-800"} ${
|
||||
val === value && !password ? "rounded-r-xl" : ""
|
||||
} pl-2 rounded-l-xl w-full py-1 transition`}
|
||||
onInput={(e) => {
|
||||
setVal(e.target.value);
|
||||
onInput({ value: e.target.value, setIdle });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className={`w-full ${
|
||||
pink ? "bg-pink-700/20" : "bg-zinc-800"
|
||||
} py-1 w-full flex items-center justify-center`}
|
||||
>
|
||||
<LoadingSpinner size={25} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{password && (
|
||||
<button
|
||||
className={`${
|
||||
pink
|
||||
? "bg-pink-400 hover:bg-pink-600"
|
||||
: "bg-zinc-600 hover:bg-zinc-700 "
|
||||
} cursor-pointer transition-colors ${
|
||||
val === value ? "rounded-r-xl" : ""
|
||||
} p-1 text-2xl `}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
if ((disWHidden && password && hid) || disabled || idle) return;
|
||||
setVal(value);
|
||||
}}
|
||||
onClick={() => {
|
||||
setHid(!hid);
|
||||
}}
|
||||
>
|
||||
{hid ? <BiShow /> : <BiHide />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<AnimatePresence mode="popLayout">
|
||||
{val !== value && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, translateX: 10 }}
|
||||
animate={{ opacity: 1, translateX: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
disabled={idle}
|
||||
className={`${
|
||||
pink
|
||||
? "bg-pink-400 disabled:bg-pink-900/50 hover:bg-pink-600"
|
||||
: "bg-zinc-600 disabled:bg-zinc-900 hover:bg-zinc-700"
|
||||
} p-1 text-2xl rounded-r-xl transition-colors not-disabled:cursor-pointer`}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setVal(value);
|
||||
}}
|
||||
>
|
||||
{idle ? <LoadingSpinner size={24} /> : <BiCheck />}
|
||||
</motion.button>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export function TextPreloader({ width = "5rem" }) {
|
||||
return (
|
||||
<span
|
||||
className={"h-4 block bg-zinc-900 overflow-hidden rounded-full w-(--w)"}
|
||||
style={{ "--w": width }}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"block h-10 -translate-y-3 w-3 bg-zinc-300 relative blur-[7px] " +
|
||||
trashcss.anim_preloader
|
||||
}
|
||||
></span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
.anim_preloader {
|
||||
animation: anim-preloader 1s infinite linear;
|
||||
}
|
||||
@keyframes anim-preloader {
|
||||
from {
|
||||
transform: translateX(-50px) rotate(20deg);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc((var(--w, 50px)) + (50px))) rotate(20deg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user