From 27f2bbdb4756237d8e15a0876e6a709386c7705d Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Fri, 3 Feb 2023 21:47:54 +0800 Subject: [PATCH] init --- Dockerfile | 10 + components/Timetable.tsx | 193 ++++++++++++++++ config/index.ts | 4 + next.config.js | 3 + pages/api/admin.ts | 23 ++ pages/api/begin.ts | 10 + pages/api/hello.ts | 13 -- pages/api/html.ts | 467 +++++++++++++++++++++++++++++++++++++++ pages/api/limit.ts | 10 + pages/api/pause.ts | 10 + pages/api/record.ts | 57 +++++ pages/edit.tsx | 11 + pages/index.tsx | 139 +++--------- store/index.ts | 2 + styles/Home.module.css | 278 ----------------------- styles/globals.css | 107 --------- test.ts | 13 ++ 17 files changed, 845 insertions(+), 505 deletions(-) create mode 100644 Dockerfile create mode 100644 components/Timetable.tsx create mode 100644 config/index.ts create mode 100644 pages/api/admin.ts create mode 100644 pages/api/begin.ts delete mode 100644 pages/api/hello.ts create mode 100644 pages/api/html.ts create mode 100644 pages/api/limit.ts create mode 100644 pages/api/pause.ts create mode 100644 pages/api/record.ts create mode 100644 pages/edit.tsx create mode 100644 store/index.ts create mode 100644 test.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0b8c9b3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM node:19 + +EXPOSE 3000 + +COPY . /app +WORKDIR /app + +RUN ["npm", "run", "build"] + +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/components/Timetable.tsx b/components/Timetable.tsx new file mode 100644 index 0000000..57ff86c --- /dev/null +++ b/components/Timetable.tsx @@ -0,0 +1,193 @@ +import React from "react"; + +interface Conflicts { + [index: string]: HTMLInputElement[]; +} +interface ConflictsTmp { + [index: string]: Set; +} +interface IndexToElement { + [index: string]: HTMLInputElement; +} + +const indexToElement: IndexToElement = {}; +const conflicts: Conflicts = {}; +const marks: (HTMLInputElement | null)[][] = []; + +const Timetable = ({ user }) => { + const [editable, setEditable] = React.useState(true); + const ref = React.useRef(); + + const handleSelect = async (event: Event) => { + const target: HTMLInputElement = event.target; + console.log("select", target.name, target.checked); + + const changedInputs: any = []; + // find whether there are checked input in conflict + for (const input of conflicts[target.name]) { + if (input.name === target.name) continue; + if (input.checked) { + alert("Error: Conflict select"); + location.reload(); + return + } + } + for (const input of conflicts[target.name]) { + if (input.name === target.name) continue; + if (target.checked) { + if (input.getAttribute("disabled") === null) { + input.setAttribute("disabled", "true"); + changedInputs.push({ input, disable: false }); + } + } else { + if (input.getAttribute("disabled") === "true") { + input.removeAttribute("disabled"); + changedInputs.push({ input, disable: true }); + } + } + } + // post request + const resp = await fetch("/api/record", { + method: "POST", + headers: { "Content-Type": "appliction/json" }, + body: JSON.stringify({ + name: target.name, + checked: target.checked, + user, + }), + }); + if (!resp.ok) { + const json = await resp.json(); + alert(json.error); + // revert conflict changed input + for (const { input, disable } of changedInputs) { + if (disable) { + input.addAttribute("disabled", "true"); + } else { + input.removeAttribute("disabled"); + } + } + } + }; + + const handleInput = (event: React.ChangeEvent): boolean => { + const { target } = event; + // validate + if (target?.children[0]?.tagName !== "TABLE") { + console.log("not a table"); + return false; + } + + console.log(target.innerHTML); + + // turn off editable + setEditable(false); + + const table = target.children[0]; + table.setAttribute("border", "1"); + + // mark cell + const conflictsTmp: ConflictsTmp = {}; + const tbody = table.children[table.children.length - 1]; + for (const tr_index in tbody.children) { + const tr = tbody.children[tr_index]; + const row: (HTMLInputElement | null)[] = []; + for (const td_index in tr.children) { + const td = tr.children[td_index]; + if (td.tagName !== "TD") continue; + if (td.getAttribute("bgcolor")?.toUpperCase() !== "#39CEFF") { + row.push(null); + continue; + } + + const index = `${tr_index},${td_index}`; + + const placeholders = td.textContent?.trim().split(","); + if (placeholders === undefined) continue; + if (conflictsTmp[index] === undefined) conflictsTmp[index] = new Set(); + for (const ph of placeholders) conflictsTmp[index].add(ph); + + // mount click event + const input = document.createElement("input"); + input.setAttribute("type", "checkbox"); + input.onchange = handleSelect; + input.name = index; + td.innerHTML = ""; + td.appendChild(input); + indexToElement[index] = input; + + row.push(input); + } + marks.push(row); + } + + // resolve conflicts + for (const index in conflictsTmp) { + if (conflicts[index] === undefined) conflicts[index] = []; + for (const ph of Array.from(conflictsTmp[index])) { + for (const conflictIndex in conflictsTmp) { + if (conflictsTmp[conflictIndex].has(ph)) + conflicts[index].push(indexToElement[conflictIndex]); + } + } + } + + console.log(conflicts); + + return true; + }; + + const refresh = async () => { + const resp = await fetch(`/api/record?name=${user}`); + const json = await resp.json(); + const occupied: string[] = json.occupied; + const myselect: string[] = json.myselect; + console.log(json); + for (const index in indexToElement) { + if (occupied.includes(index)) { + indexToElement[index].style.display = "none"; + } else { + indexToElement[index].style.display = ""; + } + indexToElement[index].checked = myselect.includes(index); + } + }; + React.useEffect(() => { + const interval = setInterval(() => { + refresh(); + }, 1000); + return () => { + clearInterval(interval); + }; + }); + + React.useEffect(() => { + fetch("/api/html") + .then((resp) => resp.json()) + .then((json) => { + console.log(ref); + ref.current.innerHTML = json.html; + }) + .then(() => { + handleInput({ target: ref.current }); + refresh(); + }); + }, []); + + return ( + <> + + Login as {user} +
+ + ); +}; + +export default Timetable; diff --git a/config/index.ts b/config/index.ts new file mode 100644 index 0000000..d65e503 --- /dev/null +++ b/config/index.ts @@ -0,0 +1,4 @@ +export default { + begin: false, + limit: 2, +} \ No newline at end of file diff --git a/next.config.js b/next.config.js index a843cbe..89c3ce9 100644 --- a/next.config.js +++ b/next.config.js @@ -1,6 +1,9 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, + typescript: { + ignoreBuildErrors: true, + }, } module.exports = nextConfig diff --git a/pages/api/admin.ts b/pages/api/admin.ts new file mode 100644 index 0000000..6a7c91c --- /dev/null +++ b/pages/api/admin.ts @@ -0,0 +1,23 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import store from "@/store"; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse>, +) { + if (req.method === 'POST') { + // update store + console.log('admin', req.body) + const json = req.body + for (const key in json) { + store[key] = json[key]; + } + const keys = Object.keys(json) + for (const key in store) { + if (json[key] === undefined) { + delete store[key] + } + } + } + res.status(200).json(store); +} diff --git a/pages/api/begin.ts b/pages/api/begin.ts new file mode 100644 index 0000000..970ae32 --- /dev/null +++ b/pages/api/begin.ts @@ -0,0 +1,10 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import config from "@/config"; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + config.begin = true; + res.status(200).json(config); +} diff --git a/pages/api/hello.ts b/pages/api/hello.ts deleted file mode 100644 index f8bcc7e..0000000 --- a/pages/api/hello.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Next.js API route support: https://nextjs.org/docs/api-routes/introduction -import type { NextApiRequest, NextApiResponse } from 'next' - -type Data = { - name: string -} - -export default function handler( - req: NextApiRequest, - res: NextApiResponse -) { - res.status(200).json({ name: 'John Doe' }) -} diff --git a/pages/api/html.ts b/pages/api/html.ts new file mode 100644 index 0000000..c375267 --- /dev/null +++ b/pages/api/html.ts @@ -0,0 +1,467 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import store from "@/store"; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse>, +) { + res.status(200).json({ + html: ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
T6值班2月6日/周一2月7日/周二2月8日/周三2月9日/周四2月10日/周五T6值班2月11日/周六2月12日/周日
08:00-10:001245609:00-12:003238
10:00-12:00789101112:00-15:003339
12:00-14:00121314151615:00-17:303440
14:00-16:001718192021


16:00-18:002223242526


18:00-21:002728293031


T6值班2月13日/周一2月14日/周二





08:00-10:004450





10:00-12:004551





12:00-14:004652





14:00-16:004753





16:00-18:004854





18:00-21:004955























V26值班2月6日/周一2月7日/周二2月8日/周三2月9日/周四2月10日/周五


08:00-10:0012456


10:00-12:007891011


12:00-14:001213141516


14:00-16:001718192021


16:00-18:002223242526


18:00-21:002728293031


V26值班2月13日/周一2月14日/周二





08:00-10:004450





10:00-12:004551





12:00-14:004652





14:00-16:004753





16:00-18:004854





18:00-21:004955














电话值班2月6日/周一2月7日/周二2月8日/周三2月9日/周四2月10日/周五电话值班+图书馆值班2月11日/周六2月12日/周日
07:50-08:501245609:00-12:003238
11:30-13:307,128,139,1410,1511,1612:00-15:003339
17:00-21:00
电话值班+图书馆值班
22,2723,2824,2925,3036,3115:00-17:303440
电话值班2月13日/周一2月14日/周二


17:30-21:003536
07:50-08:504450





11:30-13:3045,4651,52





17:00-21:00
电话值班+图书馆值班
48,4954,55





+ + + + +edit.tsx:6:12 +` + }); +} diff --git a/pages/api/limit.ts b/pages/api/limit.ts new file mode 100644 index 0000000..6efb7e6 --- /dev/null +++ b/pages/api/limit.ts @@ -0,0 +1,10 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import config from "@/config"; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + config.limit = parseInt(req.query.limit) || 2; + res.status(200).json(config); +} \ No newline at end of file diff --git a/pages/api/pause.ts b/pages/api/pause.ts new file mode 100644 index 0000000..5fec025 --- /dev/null +++ b/pages/api/pause.ts @@ -0,0 +1,10 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import config from "@/config"; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + config.begin = false; + res.status(200).json(config); +} \ No newline at end of file diff --git a/pages/api/record.ts b/pages/api/record.ts new file mode 100644 index 0000000..2b55949 --- /dev/null +++ b/pages/api/record.ts @@ -0,0 +1,57 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import type { NextApiRequest, NextApiResponse } from "next"; +import store from "@/store"; +import config from "@/config"; + +export default function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method === "POST") { + if (!config.begin) { + res.status(400).json({ + error: "还没到开时间哦", + }); + return; + } + const json = JSON.parse(req.body); + console.log("request", json); + if (json.checked) { + let count = 0; + for (const name in store) { + if (store[name] == json.user) { + count += 1; + if (count >= config.limit) { + res.status(403).json({ + error: `超过选择数量限制: ${config.limit}`, + }); + return; + } + } + } + // check whether it is alreadly occupied + if (store[json.name] !== undefined) { + res.status(403).json({ + error: `当前位置已被他人占用`, + }); + return; + } + store[json.name] = json.user; + } else { + delete store[json.name]; + } + } + console.log("query", req.query); + const resp = { + occupied: [], + myselect: [], + }; + for (const key in store) { + if (store[key] !== req.query.name) { + resp.occupied.push(key); + } else { + resp.myselect.push(key); + } + } + res.status(200).json(resp); +} diff --git a/pages/edit.tsx b/pages/edit.tsx new file mode 100644 index 0000000..cba61a9 --- /dev/null +++ b/pages/edit.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +const EditPage = () => { + return
{ + console.log(event.currentTarget.innerHTML); + }} + contentEditable="true">
; +}; + +export default EditPage; diff --git a/pages/index.tsx b/pages/index.tsx index 3a20955..1031de7 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,11 +1,15 @@ -import Head from 'next/head' -import Image from 'next/image' -import { Inter } from '@next/font/google' -import styles from '@/styles/Home.module.css' - -const inter = Inter({ subsets: ['latin'] }) +import React from "react"; +import Head from "next/head"; +import Timetable from "@/components/Timetable"; export default function Home() { + const [user, setUser] = React.useState(""); + const [begin, setBegin] = React.useState(false); + + React.useEffect(() => { + setUser(localStorage.getItem("user") || ""); + }, []); + return ( <> @@ -14,110 +18,31 @@ export default function Home() { -
-
-

- Get started by editing  - pages/index.tsx -

+
+ {!begin && ( -
- -
- Next.js Logo -
- 13 setUser(event.target.value)} /> +
-
- - + )} + {begin && }
- ) + ); } diff --git a/store/index.ts b/store/index.ts new file mode 100644 index 0000000..238ecec --- /dev/null +++ b/store/index.ts @@ -0,0 +1,2 @@ +const store: Record = {}; +export default store; diff --git a/styles/Home.module.css b/styles/Home.module.css index 27dfff5..e69de29 100644 --- a/styles/Home.module.css +++ b/styles/Home.module.css @@ -1,278 +0,0 @@ -.main { - display: flex; - flex-direction: column; - justify-content: space-between; - align-items: center; - padding: 6rem; - min-height: 100vh; -} - -.description { - display: inherit; - justify-content: inherit; - align-items: inherit; - font-size: 0.85rem; - max-width: var(--max-width); - width: 100%; - z-index: 2; - font-family: var(--font-mono); -} - -.description a { - display: flex; - justify-content: center; - align-items: center; - gap: 0.5rem; -} - -.description p { - position: relative; - margin: 0; - padding: 1rem; - background-color: rgba(var(--callout-rgb), 0.5); - border: 1px solid rgba(var(--callout-border-rgb), 0.3); - border-radius: var(--border-radius); -} - -.code { - font-weight: 700; - font-family: var(--font-mono); -} - -.grid { - display: grid; - grid-template-columns: repeat(4, minmax(25%, auto)); - width: var(--max-width); - max-width: 100%; -} - -.card { - padding: 1rem 1.2rem; - border-radius: var(--border-radius); - background: rgba(var(--card-rgb), 0); - border: 1px solid rgba(var(--card-border-rgb), 0); - transition: background 200ms, border 200ms; -} - -.card span { - display: inline-block; - transition: transform 200ms; -} - -.card h2 { - font-weight: 600; - margin-bottom: 0.7rem; -} - -.card p { - margin: 0; - opacity: 0.6; - font-size: 0.9rem; - line-height: 1.5; - max-width: 30ch; -} - -.center { - display: flex; - justify-content: center; - align-items: center; - position: relative; - padding: 4rem 0; -} - -.center::before { - background: var(--secondary-glow); - border-radius: 50%; - width: 480px; - height: 360px; - margin-left: -400px; -} - -.center::after { - background: var(--primary-glow); - width: 240px; - height: 180px; - z-index: -1; -} - -.center::before, -.center::after { - content: ''; - left: 50%; - position: absolute; - filter: blur(45px); - transform: translateZ(0); -} - -.logo, -.thirteen { - position: relative; -} - -.thirteen { - display: flex; - justify-content: center; - align-items: center; - width: 75px; - height: 75px; - padding: 25px 10px; - margin-left: 16px; - transform: translateZ(0); - border-radius: var(--border-radius); - overflow: hidden; - box-shadow: 0px 2px 8px -1px #0000001a; -} - -.thirteen::before, -.thirteen::after { - content: ''; - position: absolute; - z-index: -1; -} - -/* Conic Gradient Animation */ -.thirteen::before { - animation: 6s rotate linear infinite; - width: 200%; - height: 200%; - background: var(--tile-border); -} - -/* Inner Square */ -.thirteen::after { - inset: 0; - padding: 1px; - border-radius: var(--border-radius); - background: linear-gradient( - to bottom right, - rgba(var(--tile-start-rgb), 1), - rgba(var(--tile-end-rgb), 1) - ); - background-clip: content-box; -} - -/* Enable hover only on non-touch devices */ -@media (hover: hover) and (pointer: fine) { - .card:hover { - background: rgba(var(--card-rgb), 0.1); - border: 1px solid rgba(var(--card-border-rgb), 0.15); - } - - .card:hover span { - transform: translateX(4px); - } -} - -@media (prefers-reduced-motion) { - .thirteen::before { - animation: none; - } - - .card:hover span { - transform: none; - } -} - -/* Mobile */ -@media (max-width: 700px) { - .content { - padding: 4rem; - } - - .grid { - grid-template-columns: 1fr; - margin-bottom: 120px; - max-width: 320px; - text-align: center; - } - - .card { - padding: 1rem 2.5rem; - } - - .card h2 { - margin-bottom: 0.5rem; - } - - .center { - padding: 8rem 0 6rem; - } - - .center::before { - transform: none; - height: 300px; - } - - .description { - font-size: 0.8rem; - } - - .description a { - padding: 1rem; - } - - .description p, - .description div { - display: flex; - justify-content: center; - position: fixed; - width: 100%; - } - - .description p { - align-items: center; - inset: 0 0 auto; - padding: 2rem 1rem 1.4rem; - border-radius: 0; - border: none; - border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); - background: linear-gradient( - to bottom, - rgba(var(--background-start-rgb), 1), - rgba(var(--callout-rgb), 0.5) - ); - background-clip: padding-box; - backdrop-filter: blur(24px); - } - - .description div { - align-items: flex-end; - pointer-events: none; - inset: auto 0 0; - padding: 2rem; - height: 200px; - background: linear-gradient( - to bottom, - transparent 0%, - rgb(var(--background-end-rgb)) 40% - ); - z-index: 1; - } -} - -/* Tablet and Smaller Desktop */ -@media (min-width: 701px) and (max-width: 1120px) { - .grid { - grid-template-columns: repeat(2, 50%); - } -} - -@media (prefers-color-scheme: dark) { - .vercelLogo { - filter: invert(1); - } - - .logo, - .thirteen img { - filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); - } -} - -@keyframes rotate { - from { - transform: rotate(360deg); - } - to { - transform: rotate(0deg); - } -} diff --git a/styles/globals.css b/styles/globals.css index d4f491e..e69de29 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,107 +0,0 @@ -:root { - --max-width: 1100px; - --border-radius: 12px; - --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', - 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', - 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; - - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; - - --primary-glow: conic-gradient( - from 180deg at 50% 50%, - #16abff33 0deg, - #0885ff33 55deg, - #54d6ff33 120deg, - #0071ff33 160deg, - transparent 360deg - ); - --secondary-glow: radial-gradient( - rgba(255, 255, 255, 1), - rgba(255, 255, 255, 0) - ); - - --tile-start-rgb: 239, 245, 249; - --tile-end-rgb: 228, 232, 233; - --tile-border: conic-gradient( - #00000080, - #00000040, - #00000030, - #00000020, - #00000010, - #00000010, - #00000080 - ); - - --callout-rgb: 238, 240, 241; - --callout-border-rgb: 172, 175, 176; - --card-rgb: 180, 185, 188; - --card-border-rgb: 131, 134, 135; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - - --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); - --secondary-glow: linear-gradient( - to bottom right, - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0), - rgba(1, 65, 255, 0.3) - ); - - --tile-start-rgb: 2, 13, 46; - --tile-end-rgb: 2, 5, 19; - --tile-border: conic-gradient( - #ffffff80, - #ffffff40, - #ffffff30, - #ffffff20, - #ffffff10, - #ffffff10, - #ffffff80 - ); - - --callout-rgb: 20, 20, 20; - --callout-border-rgb: 108, 108, 108; - --card-rgb: 100, 100, 100; - --card-border-rgb: 200, 200, 200; - } -} - -* { - box-sizing: border-box; - padding: 0; - margin: 0; -} - -html, -body { - max-width: 100vw; - overflow-x: hidden; -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} - -a { - color: inherit; - text-decoration: none; -} - -@media (prefers-color-scheme: dark) { - html { - color-scheme: dark; - } -} diff --git a/test.ts b/test.ts new file mode 100644 index 0000000..2cfe808 --- /dev/null +++ b/test.ts @@ -0,0 +1,13 @@ +const json = await fetch("https://itsc.yongyuancv.cn/api/admin").then((resp) => + resp.json() +); + +const result = await fetch("http://localhost:3000/api/admin", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(json), +}).then(resp => resp.text()) + +console.log(result)