Files
dx4/src/pages/DX4AppPage.jsx
T
2025-11-29 20:53:09 +03:00

559 lines
17 KiB
React

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, 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";
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 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]);
const handleKeyPress = (event) => {
switch (event.key) {
case "a":
case "ArrowLeft":
setSelectedDate((prevDate) => {
const newDate = new Date(prevDate);
newDate.setDate(newDate.getDate() - 1);
return newDate;
});
break;
case "d":
case "ArrowRight":
setSelectedDate((prevDate) => {
const newDate = new Date(prevDate);
newDate.setDate(newDate.getDate() + 1);
return newDate;
});
break;
default:
break;
}
};
useEffect(() => {
window.addEventListener("keydown", handleKeyPress);
return () => {
window.removeEventListener("keydown", handleKeyPress);
};
}, []);
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")
}
>
<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">
<UserSettings
{...{
openUserSettings,
userFrNames,
setUserId,
setsetRoute,
setRoute,
adminId,
classId,
pickmeMode,
}}
/>
</AnimatePresence>
</motion.div>
</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">
<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>
)}
<DatePicker
date={selectedDate}
setDate={setSelectedDate}
indicator={typeof rasp[2] == "number" && [1, 2].includes(rasp[2])}
/>
<div className="relative">
<LayoutGroup>
<RaspView {...{ rasp, pickmeMode, selectedDate }} />
</LayoutGroup>
{!rasp[0] && (
<motion.div
className="w-full flex justify-center"
initial="hid"
animate="show"
variants={{
show: { opacity: 1, scale: 1 },
hid: { opacity: 0, scale: 0 },
}}
>
<LoadingSpinner size={25} />
</motion.div>
)}
</div>
</motion.div>
</motion.div>
</motion.div>
<CoolMainBg {...{ pickmeMode }} />
</>
);
}
const CoolMainBg = React.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
${pickmeMode ? "from-pink-400/10" : "from-white/2"}`}
/>
);
});
export const RaspView = React.memo(({ rasp, pickmeMode, selectedDate }) => {
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"
>
n урока
</th>
<th
//layout="position"
//layoutId="table-subj-h"
>
предмет
</th>
<th
//layout="position"
//layoutId="table-cab-h"
>
каб
</th>
<th
//layout="position"
//layoutId="table-t-h"
>
<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
//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"]}&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 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<span className="text-2xl font-bold">нет уроков</span>
</motion.div>
)}
{typeof rasp[1] == "string" && rasp[1].startsWith("ошибка") && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
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>
);
});
const UserSettings = React.memo(
({
openUserSettings,
userFrNames,
setUserId,
setsetRoute,
setRoute,
adminId,
classId,
pickmeMode,
}) => {
const [classLinkCopied, setClassLinkCopied] = useState(false);
return (
<>
{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>
)}
</>
);
}
);