first commit; dx 1.0 beta

This commit is contained in:
2025-11-09 22:35:16 +03:00
commit 4d01725955
153 changed files with 6504 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
export default function Panel() {
return <>
<div>
<div>
</div>
</div>
</>;
}
+534
View File
@@ -0,0 +1,534 @@
import React, { useEffect, useRef, useState } from "react";
import {
BiDotsHorizontalRounded,
BiExit,
BiSolidUser,
BiStar,
BiPencil,
BiLink,
} from "react-icons/bi";
import Logo from "../assets/logo.svg";
import {
Switch,
LoadingSpinner,
DatePicker,
// CoolMouseBg,
} from "../trash/trash-components";
import {
AdminUserFRequest,
classUserFRequest,
RaspGet,
} from "../trash/req_mgr";
import { AnimatePresence, 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";
export function DX4App({
classIDAtom,
adminAtom,
setsetRoute,
routeAtom,
pickmeModeAtom,
dengi_cash,
}) {
const [classId, setUserId] = useAtom(classIDAtom);
const [adminId, _setAdminId] = useAtom(adminAtom);
const [openUserSettings, setOpenUserSettings] = useState(false);
const setRoute = useAtom(routeAtom)[1];
const [pickmeMode, setPickmeMode] = useAtom(pickmeModeAtom);
const [rasp, setRasp] = useState([false, {}, 0]);
const [selectedDate, setSelectedDate] = useState(new Date());
useEffect(() => {
let cancelled = false;
setRasp([false, {}]);
const cacheName = `rasp-${classId}-${selectedDate.toDateString()}`;
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,
]);
//console.log(d, d[1] ? d[1] : false);
dengi_cash.cache(cacheName, [
d[1]["data"] && d[1]["data"][0] ? d[1]["data"] : -322,
d[1]["type"] ? d[1]["type"] : -1,
]);
} catch (exception) {
//console.error(exception);
setRasp([true, exception.toString()]);
}
} else {
setRasp([true, "ошибка: " + d[1].toString()]);
//console.log(d[1]);
}
},
(err) => {
setRasp([true, "ошибка: " + err]);
}
);
} else {
setRasp([true, ...dengi_cash.get(cacheName)]);
}
return () => {
cancelled = true;
};
//console.log(dengi_cash);
}, [selectedDate]);
const [userFrNames, setUserFrNames] = useState({
admin: false,
class: false,
});
const [classLinkCopied, setClassLinkCopied] = useState(false);
const lessonsEndedTriggered = useRef(false);
//TODO: Auto change day v2
useEffect(() => {
if (
lessonsEndedTriggered.current ||
typeof rasp[1] != "object" ||
rasp[1].lenght <= 0 //||
//apiFormatDate(selectedDate) !== apiFormatDate(new Date())
)
return;
try {
if (!rasp[1].at) return;
const tEnd = rasp[1].at(-1)["t-end"];
if (!/\d{1,2}:\d{2}$/.test(tEnd)) return; // not time type
if (isTimeBeforeNow(tEnd)) {
console.log(true);
const d = new Date(selectedDate);
d.setDate(d.getDate() + 1);
setSelectedDate(d);
lessonsEndedTriggered.current = true;
console.log("LOG: Auto change day triggired");
}
} catch (err) {
console.error(err);
return;
}
}, [rasp]);
useEffect(() => {
(async () => {
let cancelled = false;
const promises = [];
if (classId !== "")
promises.push(
dengi_cash.withCacheAsync(`userfr-${classId}`, async () =>
classUserFRequest(API_BASE_URL, classId)
)
);
else promises.push(Promise.resolve([0]));
if (adminId !== "")
promises.push(
dengi_cash.withCacheAsync(`adminfr-${adminId}`, async () =>
AdminUserFRequest(API_BASE_URL, adminId)
)
);
else promises.push(Promise.resolve([0]));
Promise.all(promises).then(([classRes, adminRes]) => {
if (cancelled) return;
const classF = classRes[0] === 1 ? classRes[1].userclassname : false;
const adminF = adminRes[0] === 1 ? adminRes[1].adminName : false;
setUserFrNames({ admin: adminF, class: classF });
});
return () => {
cancelled = true;
};
})();
}, [classId, adminId]);
if (classId === "") {
setsetRoute(setRoute, "/");
}
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 }}
>
<motion.div
className={
"flex items-center w-full justify-between px-5" +
ifstyle(openUserSettings, " pt-5")
}
layout="position"
>
<motion.img
src={Logo}
alt="logo"
width={100}
draggable={false}
layout="position"
/>
<motion.div className="flex gap-5 items-center">
<AnimatePresence mode="popLayout">
{!openUserSettings && (
<motion.span
className="font-light"
variants={{ hidden: { opacity: 0 }, show: { opacity: 1 } }}
initial="hidden"
animate="show"
exit="hidden"
>
{userFrNames["class"] ? userFrNames["class"] : classId}
</motion.span>
)}
</AnimatePresence>
<motion.span
layout="position"
className={
"rounded-full w-fit h-fit" +
ifstyle(openUserSettings, "border")
}
>
<BiDotsHorizontalRounded
size={35}
onClick={() => setOpenUserSettings(!openUserSettings)}
onContextMenu={(e) => {
e.preventDefault();
setPickmeMode(!pickmeMode);
}}
className={
"rounded-full " +
(pickmeMode
? "lg:hover:bg-pink-400"
: "lg:hover:bg-white lg:hover:fill-black") +
" lg:hover:scale-105 transition cursor-pointer"
}
/>
</motion.span>
<AnimatePresence mode="popLayout">
{openUserSettings && (
<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)" },
}}
initial="hidden"
animate="show"
exit="hidden"
>
{[
// icon
// text
// onclick
// condition
[
<BiExit />,
"выход",
() => {
setUserId("");
setsetRoute(setRoute, "/");
},
true,
],
// [
// <BiStar />,
// "пикми режим",
// () => {
// setPickmeMode(!pickmeMode);
// },
// true,
// ],
[
<BiSolidUser />,
`админ ${
userFrNames["admin"] ? userFrNames["admin"] : adminId
}`,
() => {},
adminId != "",
],
[
<BiLink />,
classLinkCopied ? "скопированно" : "коп. ссылку",
() => {
copyToClipboard(
location.href.substring(
0,
location.href.search("#")
) +
"#//" +
classId
).then((a) => {
if (a) {
setClassLinkCopied(true);
setTimeout(() => {
setClassLinkCopied(false);
}, 2000);
}
});
},
classId != "",
],
].map(
(item, i) =>
item[3] && (
<div
key={i}
className={
"flex gap-2 items-center justify-center transition-colors " +
(pickmeMode
? "lg:hover:bg-pink-400"
: "lg:hover:bg-white lg:hover:text-black") +
" p-1 rounded-xl cursor-pointer"
}
onClick={item[2]}
>
{item[0]} {item[1]}
</div>
)
)}
</motion.div>
)}
</AnimatePresence>
</motion.div>
</motion.div>
{adminId != "" && (
<motion.div
layout="position"
className="flex w-full justify-end px-5"
>
<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"
>
<BiPencil /> открыть в редакторе
</a>
</motion.div>
)}
<motion.div
layout="position"
className="flex flex-col gap-5 w-full not-md:px-4"
>
<DatePicker
date={selectedDate}
setDate={setSelectedDate}
indicator={typeof rasp[2] == "number" && [1, 2].includes(rasp[2])}
/>
<RaspView {...{ rasp, pickmeMode, selectedDate }} />
<AnimatePresence mode="popLayout">
{!rasp[0] && (
<motion.div
className="w-full flex justify-center"
initial="hid"
animate="show"
exit="hid"
variants={{
show: { opacity: 1, scale: 1 },
hid: { opacity: 0, scale: 0 },
}}
>
<LoadingSpinner size={25} />
</motion.div>
)}
</AnimatePresence>
</motion.div>
</motion.div>
{/* <CoolMouseBg /> */}
</motion.div>
<CoolMainBg {...{ pickmeMode }} />
</>
);
}
function CoolMainBg({ 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
${pickmeMode ? "from-pink-400/10" : "from-white/2"}`}
/>
);
}
export function RaspView({ rasp, pickmeMode, selectedDate }) {
const [tableColHover, setTableColHover] = useState(-1);
return (
<AnimatePresence mode="popLayout">
{rasp[0] && typeof rasp[1] == "object" && (
<motion.table
//layoutId="schedule_table"
//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 " +
(pickmeMode ? "from-pink-400/30" : "from-zinc-800/40") +
" to-transparent rounded-2xl selectable"
}
variants={{
hid: { opacity: 0 },
show: { opacity: 1 },
}}
initial="hid"
animate="show"
exit="hid"
>
<thead className="">
<tr className=" *:text-center text-zinc-400 *:py-2 border-b *:my-5 *:min-w-15 *:lg:hover:text-zinc-200 *:transition-colors *:font-light">
<th
className="max-w-10"
//layout="position"
//layoutId="table-num-h"
onMouseEnter={() => {
setTableColHover(0);
}}
onMouseLeave={() => {
setTableColHover(-1);
}}
>
n урока
</th>
<th
//layout="position"
//layoutId="table-subj-h"
onMouseEnter={() => {
setTableColHover(1);
}}
onMouseLeave={() => {
setTableColHover(-1);
}}
>
предмет
</th>
<th
//layout="position"
//layoutId="table-cab-h"
onMouseEnter={() => {
setTableColHover(2);
}}
onMouseLeave={() => {
setTableColHover(-1);
}}
>
каб
</th>
<th
//layout="position"
//layoutId="table-t-h"
onMouseEnter={() => {
setTableColHover(3);
}}
onMouseLeave={() => {
setTableColHover(-1);
}}
>
<div className="flex items-center justify-center gap-2">
{rasp[2] == 2 && (
<span className="w-2 h-2 animate-pulse rounded-full bg-zinc-300 block" />
)}
время
</div>
</th>
</tr>
</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
className={
(tableColHover == 0 ? "lg:bg-zinc-300/15" : "") +
" font-bold"
}
//layout="position"
//layoutId={`table-num-${i}`}
>
{d["num"]}
</td>
<td
className={tableColHover == 1 ? "lg:bg-zinc-300/15" : ""}
//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}`}
className={tableColHover == 2 ? "lg:bg-zinc-300/15" : ""}
>
{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 className={tableColHover == 3 ? "lg:bg-zinc-300/15" : ""}>
<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"]}&nbsp;&nbsp;-
</span>
<span className="text-right">{d["t-end"]}</span>
</div>
</div>
</td>
</tr>
))}
</tbody>
</motion.table>
)}
{typeof rasp[1] == "number" && rasp[1] === -322 && (
<motion.div
className="w-full flex justify-center"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 10 }}
>
<span className="text-2xl font-bold">нет уроков</span>
</motion.div>
)}
{typeof rasp[1] == "string" && rasp[1].startsWith("ошибка") && (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, x: 10 }}
className="w-full flex justify-center"
>
<span className="text-xl text-center text-transparent bg-clip-text bg-gradient-to-r from-red-300 to-red-500">
{rasp[1]}
</span>
</motion.div>
)}
</AnimatePresence>
);
}
+300
View File
@@ -0,0 +1,300 @@
import { useState, useRef, useEffect } from "react";
import { useAtom } from "jotai";
import { motion, AnimatePresence } from "motion/react";
import Logo from "../assets/logo.svg";
import { Switch, LoadingSpinner } from "../trash/trash-components";
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("")];
export function LoginPage({
classIDAtom,
adminAtom,
setsetRoute,
routeAtom,
secondPathSegment,
}) {
const [enteredId, setEnteredId] = useState("");
const [isAdmin, setIsAdmin] = useState(false);
const [adminEnteredId, setEnteredAId] = useState("");
const [userId, setUserId] = useAtom(classIDAtom);
const [route, setRoute] = useAtom(routeAtom);
const [isLogining, setLogining] = useState(false); // disables all inputs, shows
const [errorText, setErrorText] = useState("");
const [adminId, setAdminId] = useAtom(adminAtom);
/** @type {string} */
let autolog = secondPathSegment(route);
const formSubmit = async (
event = null,
cid = enteredId,
aid = adminEnteredId,
isa = isAdmin
) => {
event && event.preventDefault();
setLogining(true);
setErrorText("");
const l = await LoginReq(API_BASE_URL, cid);
if (l[0] === 1) {
if (!isa) {
setUserId(l[1]["classid"]);
setsetRoute(setRoute, "/app");
} else {
const al = await AdminReq(API_BASE_URL, aid);
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
if (l[1]["classid"] == al[1]["adminClass"]) {
setUserId(l[1]["classid"]);
setAdminId(al[1]["adminId"]);
setsetRoute(setRoute, "/app");
} else {
// you are not from here
console.log(al);
setErrorText("админ не пренадлежит этому классу");
}
} else {
setErrorText(al[1]);
}
}
} else {
setErrorText(l[1]);
}
setLogining(false);
};
if (userId !== "") {
setsetRoute(setRoute, "/app");
return;
} else if (adminId !== "") {
setAdminId(""); // NOTE: adminId with no userID is insecure
}
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
// NOTE: this hook shouldn't called if user already
// loggined, so i doesn't violating rules of hooks
if (autolog && autolog != "") {
let name, admin;
if (autolog.includes(";")) {
// /name;admin
let a = autolog.split(";");
name = a[0];
admin = b64d(a[1]);
} else {
// /name
name = autolog;
}
setEnteredId(name);
admin && setEnteredAId(admin);
formSubmit(null, name, admin, Boolean(admin));
}
}, [autolog]);
return (
<div className="w-full h-screen flex flex-col items-center justify-center bg-gradient-to-br from-white/50 to-transparent to-50% gap-5 overflow-hidden">
<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)" }}
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"
>
<BiSolidErrorCircle className="fill-red-300" size={20} />
<span>
ошибка:{" "}
<span className="font-extrabold font-mono selectable">
{errorText}
</span>
</span>
<div className="cursor-pointer" onClick={() => setErrorText("")}>
<BiBlock size={20} />
</div>
</motion.div>
)}
</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"
initial={{
y: 30,
filter: "blur(5px)",
opacity: 0,
}}
layout="position"
animate={{ y: 0, filter: "blur(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>
<form
action="#"
className="mt-2 flex flex-col gap-3 *:outline-none *:border *:border-transparent *:focus:border-white"
onSubmit={formSubmit}
>
<motion.input
type="text"
name="d"
autoComplete="off"
value={enteredId}
onInput={async (event) => {
/** @type {string} */
let val = event.target.value;
let nval = "";
val.split("").forEach((i) => {
// console.log(i);
if (AvailableSymbols.includes(i)) {
// console.log(true);
nval = nval + i;
}
});
nval = nval.replaceAll(" ", "").toUpperCase().substring(0, 10);
event.target.value = nval;
setEnteredId(nval);
}}
className="bg-zinc-800/50 p-2 rounded-2xl w-full min-w-[206px]"
placeholder="id класса"
disabled={isLogining}
minLength={3}
/>
<motion.span className="w-full flex gap-2 items-center justify-center ">
<Switch
type="checkbox"
onChange={(e) => setIsAdmin(e)}
disabled={isLogining}
name=""
/>
админка
</motion.span>
<CoolInputWithHide
{...{ enabled: isAdmin, setEnteredAId, isLogining, adminEnteredId }}
/>
<CoolLoginButton display={enteredId.length > 2} />
</form>
{enteredId.length > 2 && !isLogining && (
<div className="text-xs my-3 text-zinc-500">
входя, вы принимаете{" "}
<a
target="_blank"
href="./terms.html"
className="text-white border-b border-transparent lg:hover:border-white transition-colors"
>
пользовательское соглашение
</a>
</div>
)}
</motion.div>
<AnimatePresence mode="popLayout">
{isLogining && (
<motion.div
initial={{ y: -40, opacity: 0, filter: "blur(5px)" }}
animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
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"
>
<LoadingSpinner size={15} />
загрузка...
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function CoolLoginButton({ display, isLogining }) {
const logbuttonRef = useRef(null);
return (
<AnimatePresence mode="popLayout">
{display && (
<motion.button
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
ref={logbuttonRef}
exit={{ y: 40, opacity: 0, rotate: 20, filter: "blur(3px)" }}
onMouseMove={(e) => {
const rect = e.target.getBoundingClientRect();
e.target.style.setProperty(
"--tw-gradient-via-position",
`${((e.clientX - rect.left) / rect.width) * 100}%`
);
}}
className="bg-gradient-to-r from-neutral-900 to-zinc-900 p-2 rounded-xl not-disabled:cursor-pointer transition-colors duration-200 not-disabled:hover:via-white via-30% not-disabled:hover:text-black disabled:via-white disabled:via-0%! disabled:animate-pulse"
disabled={isLogining}
>
войти
</motion.button>
)}
</AnimatePresence>
);
}
function CoolInputWithHide({
isLogining,
setEnteredAId,
adminEnteredId,
enabled,
}) {
const [showAId, setShowAID] = useState(false);
return (
<AnimatePresence mode="popLayout">
{enabled && (
<motion.div
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 10, opacity: 0 }}
className="flex *:outline-none *:border *:border-transparent *:focus:border-white"
>
<motion.input
type={!showAId ? "password" : "text"}
name="ad"
className="bg-zinc-800/50 p-2 rounded-l-2xl max-w-[19b0px]"
placeholder="id админки"
autoComplete="off"
disabled={isLogining}
value={adminEnteredId}
required
onInput={(e) => {
/** @type {string}*/
let val = e.target.value;
let nval = "";
val.split("").forEach((i) => {
if (AvailableAdminSymbols.includes(i)) {
nval = nval + i;
}
});
e.target.value = nval;
setEnteredAId(nval);
}}
/>
<motion.div
layoutCrossfade
className={
" p-2 rounded-r-2xl border-l border-l-zinc-500 transition-colors " +
(showAId ? " bg-white" : " bg-zinc-800/50")
}
onClick={() => setShowAID(!showAId)}
>
{!showAId ? (
<BiShow color="#fff" size={23} />
) : (
<BiHide color="#000" size={23} />
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
+294
View File
@@ -0,0 +1,294 @@
import { motion, AnimatePresence, LayoutGroup } from "motion/react";
import { useAtom } from "jotai";
import { useState, useEffect } from "react";
import Logo from "../assets/logo.svg";
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,
// CoolMouseBg
} from "../trash/trash-components";
import { TableEditor } from "../trash/editorTable";
import { ifstyle } from "../trash/helpers";
import { API_BASE_URL } from "../App";
export default function EditorPage(props) {
const [classId, _setUserId] = useAtom(props.classIDAtom);
const [adminId, _setAdminId] = useAtom(props.adminAtom);
const [route, setRoute] = useAtom(props.routeAtom);
const dateRaw = props.secondPathSegment(route);
useEffect(() => {
if (
dateRaw == "" ||
adminId == "" ||
!RegExp(/\d{2}-\d{2}-\d{2}$/gm).test(dateRaw)
) {
props.setsetRoute(setRoute, "/app");
}
}, [dateRaw]);
const date = parseApiDate(dateRaw);
const [userFrNames, setUserFrNames] = useState({
admin: false,
class: false,
});
useEffect(() => {
(async () => {
let cancelled = false;
const promises = [];
if (classId !== "")
promises.push(
props.dengi_cash.withCacheAsync(`userfr-${classId}`, async () =>
classUserFRequest(API_BASE_URL, classId)
)
);
else promises.push(Promise.resolve([0]));
if (adminId !== "")
promises.push(
props.dengi_cash.withCacheAsync(`adminfr-${adminId}`, async () =>
AdminUserFRequest(API_BASE_URL, adminId)
)
);
else promises.push(Promise.resolve([0]));
Promise.all(promises).then(([classRes, adminRes]) => {
if (cancelled) return;
const classF = classRes[0] === 1 ? classRes[1].userclassname : false;
const adminF = adminRes[0] === 1 ? adminRes[1].adminName : false;
setUserFrNames({ admin: adminF, class: classF });
});
return () => {
cancelled = true;
};
})();
}, [classId, adminId]);
// on rerenderr
useEffect(() => {
console.log("editor cache clean");
props.dengi_cash.delete(
"editor_schedule",
"editor_changes",
"editor_rings"
);
}, []); // clean on full rerender
// tabs
const tabs = [
[
<>
<BiCalendarEdit />
замены
</>,
<>{/* content */}</>,
],
[
<>
<BiCalendar />
расписание
</>,
<>{/* content */}</>,
],
[
<>
<BiTime />
звонки
</>,
<>{/* content */}</>,
],
];
const [selectedTab, selectTab] = useState(0);
const [changesWereMade, setChangesWereMade] = useState(false);
const [tabDiagShow, setTabDiagShow] = useState(false);
const [tabDiagRequestTab, setTabDiagRequestTab] = useState(0); // 404 for exit
return (
<>
<div className="overflow-x-hidden">
<DengiYesOrNoDialog
show={tabDiagShow}
content={{
message: "изменения будут удалены. продолжить?",
yesBtn: "да",
noBtn: "нет",
yesIcon: <BiTrash />,
noIcon: <BiX size={20} />,
bgDissmis: true,
yesBtnStyle: "bg-red-500! text-white! md:hover:bg-red-600!",
}}
onAnswer={(answ) => {
if (answ && tabDiagRequestTab == 404) location.hash = "/app";
else if (answ) selectTab(tabDiagRequestTab);
setTabDiagShow(0);
setTabDiagRequestTab(0);
}}
/>
</div>
<div className="w-full h-screen flex flex-col items-center justify-center z-5">
<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 }}
>
<motion.div
className="flex items-center w-full justify-between px-5"
layout="position"
>
<motion.img
src={Logo}
alt="logo"
width={100}
draggable={false}
layout="position"
/>
<motion.div className="flex gap-5 items-center">
<motion.span
className="font-light flex gap-2 items-center"
variants={{ hidden: { opacity: 0 }, show: { opacity: 1 } }}
initial="hidden"
animate="show"
exit="hidden"
>
<span>
{userFrNames["class"] ? userFrNames["class"] : classId}
</span>
<span className="block h-[1px] w-[20px] -rotate-45 bg-zinc-600" />
<span>
{userFrNames["admin"] ? userFrNames["admin"] : adminId}
</span>
</motion.span>
<motion.span
layout="position"
className={"rounded-full w-fit h-fit"}
></motion.span>
</motion.div>
</motion.div>
<motion.div
layout="position"
className="flex w-full px-5 flex-col gap-5"
>
<div className="flex w-full justify-between">
<div
className={
"flex items-center gap-2 transition-colors " +
ifstyle(
selectedTab == 2,
"text-transparent pointer-events-none select-none"
)
}
>
<BiPencil 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"
onClick={(e) => {
if (changesWereMade) {
e.preventDefault();
setTabDiagRequestTab(404);
setTabDiagShow(true);
}
}}
>
<BiExit /> назад
</a>
</div>
</div>
<div className="flex items-center justify-center gap-5 select-none bg-grain 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 " +
(idx == selectedTab ? "text-white" : "text-zinc-500")
}
key={idx}
onClick={() => {
if (selectedTab == idx) return;
if (!changesWereMade) {
selectTab(idx);
} else {
setTabDiagRequestTab(idx);
setTabDiagShow(true);
}
}}
>
{item}
{idx == selectedTab && (
<motion.div
layoutId="editor_tabs_underline"
className="absolute -bottom-[4px] left-0 h-[3px] bg-white/70 rounded-full w-full"
/>
)}
</div>
))}
</LayoutGroup>
</div>
<motion.div className="flex flex-col gap-2 pb-10">
{
<TableEditor
{...{
selectedTab,
setChangesWereMade,
date,
adminId,
}}
dengiCash={props.dengi_cash}
/>
}
</motion.div>
</motion.div>
</motion.div>
{/* <CoolMouseBg /> */}
</div>
<DefaultEditorBG />
</>
);
}
function DefaultEditorBG() {
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"
}
/>
);
}
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
// ВАНЕЧКА ПИКМИЛЁВ <3
+212
View File
@@ -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>
);
}