This commit is contained in:
2026-03-22 21:14:40 +03:00
parent 8aa1182641
commit 2dbe7866ff
9 changed files with 174 additions and 60 deletions
+3
View File
@@ -17,6 +17,7 @@
"react-icons": "^5.5.0",
"tailwindcss": "^4.1.12",
"use-debounce": "^10.0.6",
"web-haptics": "^0.0.6",
},
"devDependencies": {
"@eslint/js": "^9.33.0",
@@ -1152,6 +1153,8 @@
"vite-plugin-ssr-ssg": ["vite-plugin-ssr-ssg@1.4.1", "", { "dependencies": { "compression": "^1.7.4", "consola": "^2.15.3", "express": "^4.17.1", "fs-extra": "^9.1.0", "recursive-readdir": "^2.2.2", "yargs": "^16.2.0" }, "bin": { "vite-ssrg": "dist/bin.js" } }, "sha512-lXw6VyzIrHNN5aOdtJmaf3rVb8g/stRu/xSJ653PDbO2P6J/4i2PxXoCStwj4Exry7JE3YpxujOJ4KOtgwCT3A=="],
"web-haptics": ["web-haptics@0.0.6", "", { "peerDependencies": { "react": ">=18", "react-dom": ">=18", "svelte": ">=4", "vue": ">=3" }, "optionalPeers": ["react", "react-dom", "svelte", "vue"] }, "sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw=="],
"webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="],
"whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="],
+6
View File
@@ -41,6 +41,7 @@
<title>dx 4</title>
</head>
<body>
<div id="root">
<style>
div[data-ktkz-loading] {
@@ -113,6 +114,11 @@
</div>
</div>
</div>
<div k-version>
dx4 v2.1
</div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+2 -1
View File
@@ -21,7 +21,8 @@
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"tailwindcss": "^4.1.12",
"use-debounce": "^10.0.6"
"use-debounce": "^10.0.6",
"web-haptics": "^0.0.6"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
+10 -1
View File
@@ -34,6 +34,15 @@ input {
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) {
/* free ad blocker*/
body:not(.dev) > *:not(#root):not(vite-error-overlay):not([k-version]) {
display: none !important;
}
[k-version] {
position: fixed;
right: 10px;
bottom: 10px;
font-size: 1rem;
color: #eee5;
}
+5 -1
View File
@@ -22,8 +22,12 @@ import "@fontsource/jost/700.css";
import "@fontsource/jost/800.css";
//import "@fontsource/jost/900.css";
if (import.meta.env.DEV) {
document.body.classList.add("dev");
}
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>
</StrictMode>,
);
+49 -24
View File
@@ -10,24 +10,20 @@ import {
import Logo from "../assets/logo.svg";
import LogoAlternative from "../assets/fully_alternative_logo.svg";
import {
Switch,
// AnimatedCanvas,
LoadingSpinner,
DatePicker,
TextPreloader,
// CoolMouseBg,
} from "../trash/trash-components";
import {
AdminReq,
AdminUserFRequest,
classUserFRequest,
RaspGet,
} from "../trash/req_mgr";
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";
export function DX4App({
classIDAtom,
@@ -78,7 +74,7 @@ export function DX4App({
(err) => {
if (cancelled) return;
setRasp([true, "ошибка: " + err]);
}
},
);
} else {
if (cancelled) return;
@@ -133,15 +129,15 @@ export function DX4App({
if (classId !== "")
promises.push(
dengi_cash.withCacheAsync(`userfr-${classId}`, async () =>
classUserFRequest(API_BASE_URL, classId)
)
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)
)
AdminReq(API_BASE_URL, adminId),
),
);
else promises.push(Promise.resolve([0]));
Promise.all(promises).then(([classRes, adminRes]) => {
@@ -192,6 +188,9 @@ export function DX4App({
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">
@@ -236,7 +235,12 @@ export function DX4App({
>
<BiDotsHorizontalRounded
size={35}
onClick={() => setOpenUserSettings(!openUserSettings)}
onClick={() => {
setOpenUserSettings(!openUserSettings);
trigger([{ duration: 10 }, { delay: 100, duration: 10 }], {
intensity: 1,
});
}}
onContextMenu={(e) => {
e.preventDefault();
setPickmeMode(!pickmeMode);
@@ -269,7 +273,7 @@ export function DX4App({
{adminId != "" && managing && (
<motion.div
className="flex w-full justify-end"
className="flex w-full justify-end not-md:px-4"
layout="position"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@@ -326,7 +330,28 @@ export function DX4App({
</a>
)}
</div>
<div className="relative">
<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>
@@ -343,7 +368,7 @@ export function DX4App({
<LoadingSpinner size={25} />
</motion.div>
)}
</div>
</motion.div>
</motion.div>
</motion.div>
</motion.div>
@@ -370,8 +395,8 @@ export const RaspView = memo(({ rasp, pickmeMode, selectedDate }) => {
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-zinc-800/40") +
" to-transparent rounded-2xl selectable"
(pickmeMode ? "from-pink-400/30" : "from-[#101011ee]") +
" to-[#030303fa] rounded-2xl"
}
onContextMenu={(ev) => {
ev.preventDefault();
@@ -419,7 +444,7 @@ export const RaspView = memo(({ rasp, pickmeMode, selectedDate }) => {
</th>
</tr>
</thead>
<tbody className="*:*:text-center *:*:py-2 *:not-last:border-b *:not-last:border-zinc-800 text-white">
<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}`} />
))}
@@ -542,13 +567,13 @@ const UserSettings = memo(
>
{item[0]} {item[1]}
</div>
)
),
)}
</motion.div>
)}
</>
);
}
},
);
class GlobalListener {
@@ -592,10 +617,10 @@ const TableItem = memo(({ d }) => {
<tr
//layout="position"
className={
"transition-colors *:transition-colors " +
"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")
? "*:bg-zinc-300/15 lg:hover:*:bg-zinc-300/20"
: " lg:hover:*:bg-zinc-300/15")
}
onClick={() => {
setSel(!sel);
+12 -6
View File
@@ -22,6 +22,7 @@ import {
import { TableEditor } from "../trash/editorTable";
import { ifstyle } from "../trash/helpers";
import { API_BASE_URL } from "../App";
import { useWebHaptics } from "web-haptics/react";
export default function EditorPage(props) {
const [classId, _setUserId] = useAtom(props.classIDAtom);
@@ -49,17 +50,18 @@ export default function EditorPage(props) {
let cancelled = false;
const promises = [];
if (classId !== "")
// TODO: rewrite this bullshit into one pretty request
promises.push(
props.dengi_cash.withCacheAsync(`userfr-${classId}`, async () =>
classUserFRequest(API_BASE_URL, classId)
)
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)
)
AdminUserFRequest(API_BASE_URL, adminId),
),
);
else promises.push(Promise.resolve([0]));
Promise.all(promises).then(([classRes, adminRes]) => {
@@ -79,9 +81,12 @@ export default function EditorPage(props) {
props.dengi_cash.delete(
"editor_schedule",
"editor_changes",
"editor_rings"
"editor_rings",
);
}, []); // clean on full rerender
const { trigger } = useWebHaptics();
// tabs
const tabs = [
[
@@ -189,7 +194,7 @@ export default function EditorPage(props) {
"flex items-center gap-2 transition-colors " +
ifstyle(
selectedTab == 2,
"text-transparent pointer-events-none select-none"
"text-transparent pointer-events-none select-none",
)
}
>
@@ -238,6 +243,7 @@ export default function EditorPage(props) {
key={idx}
onClick={() => {
if (selectedTab == idx) return;
trigger([{ duration: 35 }], { intensity: 1 });
if (!changesWereMade) {
selectTab(idx);
} else {
+28 -11
View File
@@ -42,6 +42,7 @@ import {
EditorRemoveRequest,
} from "./editor_req_mgr";
import { API_BASE_URL } from "../App";
import { useWebHaptics } from "web-haptics/react";
// Configuration
const COLUMNS = [
@@ -171,7 +172,7 @@ function SortableRow({
transition,
opacity: isDragging ? 0.5 : 1,
};
const { trigger } = useWebHaptics();
return (
<div
ref={setNodeRef}
@@ -207,7 +208,10 @@ function SortableRow({
<span className="w-[98%] mx-auto h-[1px] bg-zinc-500" />
<div className="flex justify-end w-full">
<button
onClick={() => onDuplicate(item.id)}
onClick={() => {
onDuplicate(item.id);
trigger([{ duration: 25 }], { intensity: 0.7 });
}}
className="min-w-12 min-h-12 transition-colors bg-zinc-600/20 text-zinc-400 hover:bg-zinc-600/40 hover:text-zinc-300 flex items-center justify-center rounded-l-md"
>
<BiCopy size={20} />
@@ -220,7 +224,14 @@ function SortableRow({
<BiDotsVertical size={24} />
</div>
<button
onClick={() => onDelete(item.id)}
onClick={() => {
onDelete(item.id);
trigger([
{ duration: 40, intensity: 0.7 },
{ delay: 40, duration: 40, intensity: 0.7 },
{ delay: 40, duration: 40, intensity: 0.9 },
]);
}}
className="min-w-12 min-h-12 transition-colors bg-red-600/20 text-red-400 hover:bg-red-600/40 hover:text-red-300 active:bg-red-600/60 flex items-center justify-center rounded-r-md"
>
<BiX size={24} />
@@ -235,7 +246,7 @@ async function getPrefilledRasp(tab, date, adminId) {
API_BASE_URL,
tab,
adminId,
apiFormatDate(date)
apiFormatDate(date),
);
if (a[0] == 1 && a[1]) {
return convertApiToEditor(a[1]);
@@ -291,7 +302,7 @@ export function TableEditor({
delay: 200,
tolerance: 8,
},
})
}),
);
const updateItems = (newItems) => {
@@ -330,7 +341,7 @@ export function TableEditor({
clearTimeout(updateTimeoutRef.current[key]);
}
const newItems = items.map((item) =>
item.id === id ? { ...item, [field]: value } : item
item.id === id ? { ...item, [field]: value } : item,
);
updateItems(newItems);
updateTimeoutRef.current[key] = setTimeout(() => {
@@ -338,7 +349,7 @@ export function TableEditor({
const itemsWithOriginalValue = items.map((item) =>
item.id === id
? { ...item, [field]: previousValueRef.current[key] }
: item
: item,
);
setUndoItems([itemsWithOriginalValue, ...undoItems]);
@@ -406,7 +417,7 @@ export function TableEditor({
const lastPosition =
items.length > 0
? String(
parseInt(sortedItems[sortedItems.length - 1].position || "0") + 1
parseInt(sortedItems[sortedItems.length - 1].position || "0") + 1,
)
: "1";
@@ -425,7 +436,7 @@ export function TableEditor({
left: window.scrollX, // preserve current X
behavior: "smooth",
}),
100
100,
);
};
@@ -461,7 +472,7 @@ export function TableEditor({
selectedTab,
adminId,
apiFormatDate(date),
JSON.stringify(convertEditorToApi(items))
JSON.stringify(convertEditorToApi(items)),
).then(() => {
setLoading(false);
});
@@ -471,6 +482,11 @@ export function TableEditor({
setDeleteDiag(true);
};
const { trigger } = useWebHaptics();
const handleDragOver = (ev) => {
trigger([{ duration: 25 }], { intensity: 0.7 });
};
let deleteDialogBusy = false; // NOTE: we don't need a state
return (
<>
@@ -496,7 +512,7 @@ export function TableEditor({
API_BASE_URL,
selectedTab,
adminId,
apiFormatDate(date)
apiFormatDate(date),
).then(() => {
deleteDialogBusy = false; // for 100% sure
setDeleteDiag(false);
@@ -573,6 +589,7 @@ export function TableEditor({
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
onDragOver={handleDragOver}
>
<SortableContext
items={items.map((item) => item.id)}
+59 -16
View File
@@ -7,9 +7,11 @@ import {
BiShow,
BiX,
} from "react-icons/bi";
import { formatDate, getDateTags } from "./dengi.js";
import { apiFormatDate, formatDate, getDateTags } from "./dengi.js";
import { AnimatePresence, motion, LayoutGroup } from "motion/react";
import trashcss from "./trash.module.css";
import { useWebHaptics } from "web-haptics/react";
export function Switch({
checked,
defaultChecked = false,
@@ -98,6 +100,7 @@ export function LoadingSpinner({ size }) {
}
function DatePickerButton({ next = false, date, setDate, pink = false }) {
const { trigger } = useWebHaptics();
return (
<span
className={`cursor-pointer ${
@@ -109,6 +112,7 @@ function DatePickerButton({ next = false, date, setDate, pink = false }) {
const d = new Date(date);
d.setDate(d.getDate() + (next ? 1 : -1));
setDate(d);
trigger([{ duration: 15 }], { intensity: 1 });
}}
>
{!next ? <BiLeftArrowAlt size={34} /> : <BiRightArrowAlt size={34} />}
@@ -118,7 +122,7 @@ function DatePickerButton({ next = false, date, setDate, pink = false }) {
export const DatePicker = (props) => {
const dateTag = getDateTags(props.date);
const { trigger } = useWebHaptics();
return (
<LayoutGroup>
<div
@@ -133,20 +137,27 @@ export const DatePicker = (props) => {
/>
<AnimatePresence mode="popLayout">
<motion.span
key={formatDate(props.date)}
// key={formatDate(props.date)}
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)" },
}}
initial="a"
animate="b"
exit="a"
// variants={{
// a: { opacity: 0, filter: "blur(1px)" },
// b: { opacity: 1, filter: "blur(0px)" },
// }}
// initial="a"
// animate="b"
// exit="a"
onClick={() => {
if (apiFormatDate(props.date) != apiFormatDate(new Date()))
trigger([
{ duration: 10, intensity: 1 },
{ delay: 40, duration: 20, intensity: 0.26 },
{ delay: 90, duration: 20, intensity: 0.34 },
]);
props.setDate(new Date());
}}
>
@@ -160,7 +171,7 @@ export const DatePicker = (props) => {
: " bg-transparent")
}
layoutId="datepicker_indicator"
layout="position"
// layout="position"
/>
<motion.span
layoutId="datepicker_date"
@@ -173,7 +184,7 @@ export const DatePicker = (props) => {
<>
<motion.div
className="flex"
layoutId="datepicker_date_div"
// layoutId="datepicker_date_div"
layout="position"
>
<span
@@ -183,9 +194,9 @@ export const DatePicker = (props) => {
/>
</motion.div>
<motion.span
initial={{ x: 10, opacity: 0 }}
initial={{ opacity: 0 }}
transition={{ delay: 0.1 }}
animate={{ x: 0, opacity: 1 }}
animate={{ opacity: 1 }}
className="text-zinc-600 italic"
>
{dateTag}
@@ -211,6 +222,8 @@ export function DengiButton({
disabled = false,
className = "",
}) {
const { trigger } = useWebHaptics();
return (
<span
role="button"
@@ -223,7 +236,19 @@ export function DengiButton({
(className ? className : "")
}
aria-disabled={disabled}
onClick={disabled ? null : onClick}
onClick={
disabled
? null
: () => {
onClick();
trigger([
{
duration: 40,
intensity: 0.8,
},
]);
}
}
>
{children}
</span>
@@ -285,6 +310,22 @@ export function DengiYesOrNoDialog({
show,
onAnswer,
}) {
const { trigger } = useWebHaptics();
useEffect(() => {
if (show) {
trigger([
{
duration: 40,
intensity: 0.8,
},
{
delay: 15,
duration: 170,
intensity: 0.9,
},
]);
}
}, [show]);
return (
<>
<AnimatePresence>
@@ -296,8 +337,10 @@ export function DengiYesOrNoDialog({
exit="off"
className="fixed z-99 bg-black/50 backdrop-blur-xl top-0 left-0 w-full h-full overflow-hidden flex items-center justify-center"
onClick={(e) => {
if (e.target === e.currentTarget && content.bgDissmis)
if (e.target === e.currentTarget && content.bgDissmis) {
onAnswer(false);
trigger([{ duration: 40, intensity: 1 }]);
}
}}
>
<motion.div