671 lines
21 KiB
React
671 lines
21 KiB
React
import { Fragment, memo, useEffect, useRef, useState } from "react";
|
|
import {
|
|
BiDotsHorizontalRounded,
|
|
BiExit,
|
|
BiSolidUser,
|
|
BiPencil,
|
|
BiLink,
|
|
BiWrench,
|
|
} from "react-icons/bi";
|
|
import Logo from "../assets/logo.svg";
|
|
import LogoAlternative from "../assets/fully_alternative_logo.svg";
|
|
import {
|
|
// AnimatedCanvas,
|
|
LoadingSpinner,
|
|
DatePicker,
|
|
TextPreloader,
|
|
// CoolMouseBg,
|
|
} from "../trash/trash-components";
|
|
import { AdminReq, 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 { copyDXLink, ifstyle } from "../trash/helpers";
|
|
import { useWebHaptics } from "web-haptics/react";
|
|
import { DengiCaching } from "../trash/dengiCashingSystem";
|
|
|
|
/**
|
|
* @param {{classIDAtom,adminAtom,setsetRoute,routeAtom,pickmeModeAtom,dengi_cash: DengiCaching}} param0
|
|
*/
|
|
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());
|
|
const [managing, setManaging] = useState(false);
|
|
|
|
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 (d[0] == 1) {
|
|
try {
|
|
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,
|
|
d[1]["type"] ? d[1]["type"] : -1,
|
|
]);
|
|
} catch (exception) {
|
|
//console.error(exception);
|
|
if (!cancelled) setRasp([true, exception.toString()]);
|
|
}
|
|
} else {
|
|
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)]);
|
|
}
|
|
|
|
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(`admindata-${adminId}`, async () =>
|
|
AdminReq(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 });
|
|
if (adminRes[0] === 1)
|
|
setManaging([-1, 2].includes(adminRes[1].adminPrems));
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
})();
|
|
}, [classId, adminId]);
|
|
|
|
/**
|
|
* @param {KeyboardEvent} event
|
|
*/
|
|
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);
|
|
|
|
if(window['DX_OPEN_DATE']){
|
|
setSelectedDate(window['DX_OPEN_DATE'])
|
|
window['DX_OPEN_DATE'] = undefined
|
|
}
|
|
|
|
return () => {
|
|
window.removeEventListener("keydown", handleKeyPress);
|
|
};
|
|
}, []);
|
|
|
|
if (classId === "") {
|
|
setsetRoute(setRoute, "/");
|
|
}
|
|
const { trigger } = useWebHaptics();
|
|
|
|
const raspDragStartX = useRef(0);
|
|
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={pickmeMode ? LogoAlternative : Logo}
|
|
alt="logo"
|
|
width={100}
|
|
draggable={false}
|
|
/>
|
|
<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
|
|
className={
|
|
"rounded-full w-fit h-fit" +
|
|
ifstyle(openUserSettings, "border")
|
|
}
|
|
>
|
|
<BiDotsHorizontalRounded
|
|
size={35}
|
|
onClick={() => {
|
|
setOpenUserSettings(!openUserSettings);
|
|
trigger([{ duration: 10 }, { delay: 100, duration: 10 }], {
|
|
intensity: 1,
|
|
});
|
|
}}
|
|
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>
|
|
|
|
{adminId != "" && managing && (
|
|
<motion.div
|
|
className="flex w-full justify-end not-md:px-4"
|
|
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
|
|
className="flex flex-col gap-5 w-full not-md:px-4"
|
|
>
|
|
<motion.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={
|
|
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"
|
|
} `}
|
|
>
|
|
{userFrNames.admin ? (
|
|
<BiPencil size={28} />
|
|
) : (
|
|
<LoadingSpinner size={28} />
|
|
)}
|
|
</a>
|
|
)}
|
|
</motion.div>
|
|
<motion.div
|
|
className="relative min-h-[50vh]"
|
|
drag="x"
|
|
dragConstraints={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
|
dragElastic={0.2}
|
|
onDragStart={(ev) => {
|
|
raspDragStartX.current = ev.clientX;
|
|
}}
|
|
onDragEnd={(ev) => {
|
|
if (raspDragStartX.current > ev.clientX + 150) {
|
|
const d = new Date(selectedDate);
|
|
d.setDate(d.getDate() + 1);
|
|
setSelectedDate(d);
|
|
trigger([{ duration: 10 }], { intensity: 1 });
|
|
} else if (raspDragStartX.current < ev.clientX - 150) {
|
|
const d = new Date(selectedDate);
|
|
d.setDate(d.getDate() - 1);
|
|
setSelectedDate(d);
|
|
trigger([{ duration: 10 }], { intensity: 1 });
|
|
}
|
|
}}
|
|
>
|
|
<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>
|
|
)}
|
|
</motion.div>
|
|
</motion.div>
|
|
</motion.div>
|
|
</motion.div>
|
|
<CoolMainBg {...{ 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
|
|
${pickmeMode ? "from-pink-400/10" : "from-white/2"}`}
|
|
/>
|
|
);
|
|
});
|
|
|
|
export const RaspView = 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 mb-10 " +
|
|
(pickmeMode ? "from-pink-400/30" : "from-[#101011ee]") +
|
|
" to-[#030303fa] rounded-2xl"
|
|
}
|
|
onContextMenu={(ev) => {
|
|
ev.preventDefault();
|
|
itemRedo.action();
|
|
}}
|
|
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) => (
|
|
<TableItem {...{ d }} key={`table-header-${i}`} />
|
|
))}
|
|
</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 = 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 },
|
|
show: { opacity: 1, y: 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"] : ""}`}
|
|
{!userFrNames["admin"] && <TextPreloader width="30px" />}
|
|
</>,
|
|
() => {
|
|
setsetRoute(setRoute, "/adminprofile");
|
|
},
|
|
adminId != "",
|
|
],
|
|
[
|
|
<BiLink />,
|
|
classLinkCopied ? "скопировано" : "коп. ссылку",
|
|
() => {
|
|
copyDXLink(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>
|
|
)}
|
|
</>
|
|
);
|
|
},
|
|
);
|
|
|
|
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 last:*:first:rounded-bl-2xl last:*:last:rounded-br-2xl " +
|
|
(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>
|
|
);
|
|
});
|