1
0

ref: remove packages/next

This commit is contained in:
alikia2x (寒寒) 2025-11-17 01:40:52 +08:00
parent 30f8a2ffe8
commit 86e1a2622b
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG Key ID: 56209E0CCD8420C6
138 changed files with 3 additions and 14018 deletions

View File

@ -4,10 +4,6 @@ data
*.txt
*.md
*config*
Inter.css
MiSans.css
*.yaml
*.yml
*.mdx
packages/core/drizzle/cred
packages/core/drizzle/main
*.mdx

View File

@ -0,0 +1 @@
drizzle/main

View File

@ -1,44 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# fumadocs
.source

View File

@ -1,3 +0,0 @@
p {
word-break: break-all;
}

View File

@ -1,17 +0,0 @@
import HeaderServer from "@/components/shell/HeaderServer";
import tpLicense from "@/content/THIRD-PARTY-LICENSES.txt";
import projectLicense from "@/content/LICENSE.txt";
export default function LicensePage() {
return (
<>
<HeaderServer />
<main className="lg:max-w-4xl lg:mx-auto">
<p className="leading-10"> V AGPL 3.0 </p>
<pre className="break-all whitespace-pre-wrap">{projectLicense}</pre>
<p className="leading-10"></p>
<pre className="break-all whitespace-pre-wrap">{tpLicense}</pre>
</main>
</>
);
}

View File

@ -1,8 +0,0 @@
"use client";
import About from "@/content/about.mdx";
import "./content.css";
export default function AboutContent() {
return <About />;
}

View File

@ -1,43 +0,0 @@
@import "tailwindcss";
.content {
h1 {
@apply text-3xl;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-xl;
}
h4 {
@apply text-lg;
}
p {
@apply my-4;
}
ul {
@apply list-disc list-inside my-4;
}
ol {
@apply list-decimal list-inside my-4;
}
li {
@apply my-2;
}
th {
@apply bg-gray-200 font-medium;
}
ul li p,
ol li p {
@apply inline;
}
}

View File

@ -1,17 +0,0 @@
import { Header } from "@/components/shell/Header";
import { getCurrentUser } from "@/lib/userAuth";
import AboutContent from "./AboutContent";
export default async function AboutPage() {
const user = await getCurrentUser();
return (
<>
<Header user={user} />
<main className="flex flex-col items-center min-h-screen gap-8 md:mt-12 relative z-0">
<div className="w-full lg:w-2/3 xl:w-1/2 content px-8 md:px-12 lg:px-0">
<AboutContent />
</div>
</main>
</>
);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,449 +0,0 @@
@font-face {
font-family: "Inter Variable";
font-style: normal;
font-weight: 100 900;
font-display: fallback;
src: url("InterVariable.woff2") format("woff2");
}
@font-face {
font-family: "Inter Variable";
font-style: italic;
font-weight: 100 900;
font-display: fallback;
src: url("InterVariable-Italic.woff2") format("woff2");
}
/* static fonts */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100;
font-display: fallback;
src: url("Inter-Thin.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 100;
font-display: fallback;
src: url("Inter-ThinItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 200;
font-display: fallback;
src: url("Inter-ExtraLight.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 200;
font-display: fallback;
src: url("Inter-ExtraLightItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url("Inter-Light.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url("Inter-LightItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url("Inter-Regular.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url("Inter-Italic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: fallback;
src: url("Inter-Medium.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 500;
font-display: fallback;
src: url("Inter-MediumItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url("Inter-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url("Inter-SemiBoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: fallback;
src: url("Inter-Bold.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 700;
font-display: fallback;
src: url("Inter-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 800;
font-display: fallback;
src: url("Inter-ExtraBold.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 800;
font-display: fallback;
src: url("Inter-ExtraBoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 900;
font-display: fallback;
src: url("Inter-Black.woff2") format("woff2");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 900;
font-display: fallback;
src: url("Inter-BlackItalic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 100;
font-display: fallback;
src: url("InterDisplay-Thin.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 100;
font-display: fallback;
src: url("InterDisplay-ThinItalic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 200;
font-display: fallback;
src: url("InterDisplay-ExtraLight.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 200;
font-display: fallback;
src: url("InterDisplay-ExtraLightItalic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url("InterDisplay-Light.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 300;
font-display: fallback;
src: url("InterDisplay-LightItalic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url("InterDisplay-Regular.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 400;
font-display: fallback;
src: url("InterDisplay-Italic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 500;
font-display: fallback;
src: url("InterDisplay-Medium.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 500;
font-display: fallback;
src: url("InterDisplay-MediumItalic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url("InterDisplay-SemiBold.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 600;
font-display: fallback;
src: url("InterDisplay-SemiBoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 700;
font-display: fallback;
src: url("InterDisplay-Bold.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 700;
font-display: fallback;
src: url("InterDisplay-BoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 800;
font-display: fallback;
src: url("InterDisplay-ExtraBold.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 800;
font-display: fallback;
src: url("InterDisplay-ExtraBoldItalic.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: normal;
font-weight: 900;
font-display: fallback;
src: url("InterDisplay-Black.woff2") format("woff2");
}
@font-face {
font-family: "InterDisplay";
font-style: italic;
font-weight: 900;
font-display: fallback;
src: url("InterDisplay-BlackItalic.woff2") format("woff2");
}
@font-feature-values InterVariable {
@character-variant {
cv01: 1;
cv02: 2;
cv03: 3;
cv04: 4;
cv05: 5;
cv06: 6;
cv07: 7;
cv08: 8;
cv09: 9;
cv10: 10;
cv11: 11;
cv12: 12;
cv13: 13;
alt-1: 1; /* Alternate one */
alt-3: 9; /* Flat-top three */
open-4: 2; /* Open four */
open-6: 3; /* Open six */
open-9: 4; /* Open nine */
lc-l-with-tail: 5; /* Lower-case L with tail */
simplified-u: 6; /* Simplified u */
alt-double-s: 7; /* Alternate German double s */
uc-i-with-serif: 8; /* Upper-case i with serif */
uc-g-with-spur: 10; /* Capital G with spur */
single-story-a: 11; /* Single-story a */
compact-lc-f: 12; /* Compact f */
compact-lc-t: 13; /* Compact t */
}
@styleset {
ss01: 1;
ss02: 2;
ss03: 3;
ss04: 4;
ss05: 5;
ss06: 6;
ss07: 7;
ss08: 8;
open-digits: 1; /* Open digits */
disambiguation: 2; /* Disambiguation (with zero) */
disambiguation-except-zero: 4; /* Disambiguation (no zero) */
round-quotes-and-commas: 3; /* Round quotes &amp; commas */
square-punctuation: 7; /* Square punctuation */
square-quotes: 8; /* Square quotes */
circled-characters: 5; /* Circled characters */
squared-characters: 6; /* Squared characters */
}
}
@font-feature-values Inter {
@character-variant {
cv01: 1;
cv02: 2;
cv03: 3;
cv04: 4;
cv05: 5;
cv06: 6;
cv07: 7;
cv08: 8;
cv09: 9;
cv10: 10;
cv11: 11;
cv12: 12;
cv13: 13;
alt-1: 1; /* Alternate one */
alt-3: 9; /* Flat-top three */
open-4: 2; /* Open four */
open-6: 3; /* Open six */
open-9: 4; /* Open nine */
lc-l-with-tail: 5; /* Lower-case L with tail */
simplified-u: 6; /* Simplified u */
alt-double-s: 7; /* Alternate German double s */
uc-i-with-serif: 8; /* Upper-case i with serif */
uc-g-with-spur: 10; /* Capital G with spur */
single-story-a: 11; /* Single-story a */
compact-lc-f: 12; /* Compact f */
compact-lc-t: 13; /* Compact t */
}
@styleset {
ss01: 1;
ss02: 2;
ss03: 3;
ss04: 4;
ss05: 5;
ss06: 6;
ss07: 7;
ss08: 8;
open-digits: 1; /* Open digits */
disambiguation: 2; /* Disambiguation (with zero) */
disambiguation-except-zero: 4; /* Disambiguation (no zero) */
round-quotes-and-commas: 3; /* Round quotes &amp; commas */
square-punctuation: 7; /* Square punctuation */
square-quotes: 8; /* Square quotes */
circled-characters: 5; /* Circled characters */
squared-characters: 6; /* Squared characters */
}
}
@font-feature-values InterDisplay {
@character-variant {
cv01: 1;
cv02: 2;
cv03: 3;
cv04: 4;
cv05: 5;
cv06: 6;
cv07: 7;
cv08: 8;
cv09: 9;
cv10: 10;
cv11: 11;
cv12: 12;
cv13: 13;
alt-1: 1; /* Alternate one */
alt-3: 9; /* Flat-top three */
open-4: 2; /* Open four */
open-6: 3; /* Open six */
open-9: 4; /* Open nine */
lc-l-with-tail: 5; /* Lower-case L with tail */
simplified-u: 6; /* Simplified u */
alt-double-s: 7; /* Alternate German double s */
uc-i-with-serif: 8; /* Upper-case i with serif */
uc-g-with-spur: 10; /* Capital G with spur */
single-story-a: 11; /* Single-story a */
compact-lc-f: 12; /* Compact f */
compact-lc-t: 13; /* Compact t */
}
@styleset {
ss01: 1;
ss02: 2;
ss03: 3;
ss04: 4;
ss05: 5;
ss06: 6;
ss07: 7;
ss08: 8;
open-digits: 1; /* Open digits */
disambiguation: 2; /* Disambiguation (with zero) */
disambiguation-except-zero: 4; /* Disambiguation (no zero) */
round-quotes-and-commas: 3; /* Round quotes &amp; commas */
square-punctuation: 7; /* Square punctuation */
square-quotes: 8; /* Square quotes */
circled-characters: 5; /* Circled characters */
squared-characters: 6; /* Squared characters */
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,87 +0,0 @@
@font-face {
font-family: "MiSans VF";
font-style: normal;
font-weight: 150 700;
font-display: fallback;
src: url("MiSans VF.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 100;
font-display: fallback;
src: url("MiSans-Thin.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 200;
font-display: fallback;
src: url("MiSans-ExtraLight.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 300;
font-display: fallback;
src: url("MiSans-Light.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 360;
font-display: fallback;
src: url("MiSans-Normal.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 400;
font-display: fallback;
src: url("MiSans-Regular.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 500;
font-display: fallback;
src: url("MiSans-Medium.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 600;
font-display: fallback;
src: url("MiSans-Demibold.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 700;
font-display: fallback;
src: url("MiSans-Semibold.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 800;
font-display: fallback;
src: url("MiSans-Bold.woff2") format("woff2");
}
@font-face {
font-family: "MiSans";
font-style: normal;
font-weight: 900;
font-display: fallback;
src: url("MiSans-Heavy.woff2") format("woff2");
}

View File

@ -1,106 +0,0 @@
@import url("./fonts/InterFont/Inter.css");
@import url("./fonts/MiSans/MiSans.css");
@import "tailwindcss";
@theme {
--color-background: #fff8f6;
--color-on-background: #2a1613;
--color-surface: #fff8f6;
--color-surface-dim: #f7d2cc;
--color-surface-bright: #fff8f6;
--color-surface-container-lowest: #ffffff;
--color-surface-container-low: #fff0ee;
--color-surface-container: #ffe9e6;
--color-surface-container-high: #ffe2dd;
--color-surface-container-highest: #ffdad4;
--color-on-surface: #2a1613;
--color-surface-variant: #ffdad4;
--color-on-surface-variant: #5f3e39;
--color-inverse-surface: #422b27;
--color-inverse-on-surface: #ffedea;
--color-outline: #946e68;
--color-outline-variant: #eabcb4;
--color-shadow: #000000;
--color-scrim: #000000;
--color-surface-tint: #c00100;
--color-primary: #a50100;
--color-on-primary: #ffffff;
--color-primary-container: #eb0000;
--color-on-primary-container: #ffffff;
--color-inverse-primary: #ffb4a8;
--color-secondary: #b4271a;
--color-on-secondary: #ffffff;
--color-secondary-container: #ff7460;
--color-on-secondary-container: #2f0000;
--color-tertiary: #6f4800;
--color-on-tertiary: #ffffff;
--color-tertiary-container: #9f6900;
--color-on-tertiary-container: #ffffff;
--color-error: #ba1a1a;
--color-on-error: #ffffff;
--color-error-container: #ffdad6;
--color-on-error-container: #410002;
--color-dark-background: #210e0b;
--color-dark-on-background: #ffdad4;
--color-dark-surface: #210e0b;
--color-dark-surface-dim: #210e0b;
--color-dark-surface-bright: #4b332f;
--color-dark-surface-container-lowest: #1b0907;
--color-dark-surface-container-low: #2a1613;
--color-dark-surface-container: #2f1a17;
--color-dark-surface-container-high: #3a2421;
--color-dark-surface-container-highest: #462f2b;
--color-dark-on-surface: #ffdad4;
--color-dark-surface-variant: #5f3e39;
--color-dark-on-surface-variant: #eabcb4;
--color-dark-inverse-surface: #ffdad4;
--color-dark-inverse-on-surface: #422b27;
--color-dark-outline: #b08780;
--color-dark-outline-variant: #5f3e39;
--color-dark-shadow: #000000;
--color-dark-scrim: #000000;
--color-dark-surface-tint: #ffb4a8;
--color-dark-primary: #ffb4a8;
--color-dark-on-primary: #690000;
--color-dark-primary-container: #de0000;
--color-dark-on-primary-container: #ffffff;
--color-dark-inverse-primary: #c00100;
--color-dark-secondary: #ffb4a8;
--color-dark-on-secondary: #690000;
--color-dark-secondary-container: #870100;
--color-dark-on-secondary-container: #ffc9c0;
--color-dark-tertiary: #feba54;
--color-dark-on-tertiary: #452b00;
--color-dark-tertiary-container: #966300;
--color-dark-on-tertiary-container: #ffffff;
--color-dark-error: #ffb4ab;
--color-dark-on-error: #690005;
--color-dark-error-container: #93000a;
--color-dark-on-error-container: #ffdad6;
}
a {
@apply text-primary dark:text-dark-primary;
}
:root {
font-family: "Inter", "MiSans", sans-serif;
font-weight: 400;
@apply bg-surface dark:bg-dark-surface text-on-surface dark:text-dark-on-surface;
}
[type="search"]::-webkit-search-cancel-button,
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
appearance: none;
}
@supports (font-variation-settings: normal) {
:root {
font-family: "Inter Variable", "MiSans VF", sans-serif;
font-optical-sizing: auto;
font-weight: 330;
}
}

View File

@ -1,24 +0,0 @@
import type { Metadata } from "next";
import "./global.css";
import React from "react";
import { routing } from "@/i18n/routing";
import { hasLocale } from "next-intl";
import { notFound } from "next/navigation";
export const metadata: Metadata = {
title: "中 V 档案馆"
};
export default async function RootLayout({
children,
params
}: Readonly<{
children: React.ReactNode;
params: Promise<{ locale: string }>;
}>) {
const { locale } = await params;
if (!hasLocale(routing.locales, locale)) {
notFound();
}
return <>{children}</>;
}

View File

@ -1,125 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import TextField from "@/components/ui/TextField";
import LoadingSpinner from "@/components/icons/LoadingSpinner";
import { Portal } from "@/components/utils/Portal";
import { Dialog } from "@/components/ui/Dialog";
import { setLocale } from "yup";
import { useTranslations } from "next-intl";
import { useCaptcha } from "@/components/hooks/useCaptcha";
import useSWRMutation from "swr/mutation";
import { FilledButton } from "@/components/ui/Buttons/FilledButton";
import { ApiRequestError } from "@/lib/net";
import { useRouter } from "next/navigation";
import { requestLogin } from "./request";
import { ErrorDialog } from "@/components/utils/ErrorDialog";
setLocale({
mixed: {
default: "yup_errors.field_invalid",
required: () => ({ key: "yup_errors.field_required" })
},
string: {
min: ({ min }) => ({ key: "yup_errors.field_too_short", values: { min } }),
max: ({ max }) => ({ key: "yup_errors.field_too_big", values: { max } })
}
});
export interface LocalizedMessage {
key: string;
values: {
[key: string]: number | string;
};
}
interface RegistrationFormProps {
backendURL: string;
}
const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
const [usernameInput, setUsername] = useState("");
const [passwordInput, setPassword] = useState("");
const [showDialog, setShowDialog] = useState(false);
const [dialogContent, setDialogContent] = useState(<></>);
const [isLoading, setLoading] = useState(false);
const t = useTranslations("");
const { startCaptcha, captchaResult, captchaUsed, setCaptchaUsedState, captchaError } = useCaptcha({
backendURL,
route: "POST-/user"
});
const { trigger } = useSWRMutation(`${backendURL}/login/session`, requestLogin);
const router = useRouter();
const translateErrorMessage = (item: LocalizedMessage | string, path?: string) => {
if (typeof item === "string") {
return item;
}
return t(`${item.key}`, { ...item.values, field: path ? t(path) : "" });
};
const register = async () => {
try {
if (captchaUsed || !captchaResult) {
await startCaptcha();
}
const result = await trigger({
data: {
username: usernameInput,
password: passwordInput
},
setShowDialog,
captchaResult,
setCaptchaUsedState,
translateErrorMessage,
setDialogContent,
t
});
if (result) {
router.push("/");
}
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!captchaError || captchaError === undefined) return;
const err = captchaError as ApiRequestError;
setShowDialog(true);
if (err.code && err.code == -1) {
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)}>
<p></p>
</ErrorDialog>
);
}
}, [captchaError]);
useEffect(() => {
startCaptcha();
}, []);
return (
<form
className="w-full flex flex-col gap-6"
onSubmit={async (e) => {
setLoading(true);
e.preventDefault();
await register();
}}
>
<TextField labelText="用户名" inputText={usernameInput} onInputTextChange={setUsername} />
<TextField labelText="密码" type="password" inputText={passwordInput} onInputTextChange={setPassword} />
<FilledButton type="submit" disabled={isLoading}>
{isLoading ? <LoadingSpinner /> : <span></span>}
</FilledButton>
<Portal>
<Dialog show={showDialog}>{dialogContent}</Dialog>
</Portal>
</form>
);
};
export default SignUpForm;

View File

@ -1,42 +0,0 @@
import { LeftArrow } from "@/components/icons/LeftArrow";
import { RightArrow } from "@/components/icons/RightArrow";
import LoginForm from "./LoginForm";
import { Link, redirect } from "@/i18n/navigation";
import { getLocale } from "next-intl/server";
import { getCurrentUser } from "@/lib/userAuth";
export default async function LoginPage() {
const user = await getCurrentUser();
const locale = await getLocale();
if (user) {
redirect({
href: `/user/${user.uid}/profile`,
locale: locale
});
}
return (
<main className="relative flex-grow pt-8 px-4 md:w-full md:h-full md:flex md:items-center md:justify-center">
<div
className="md:w-[40rem] rounded-md md:p-8 md:-translate-y-6
md:bg-surface-container md:dark:bg-dark-surface-container"
>
<p className="mb-2">
<Link href="/">
<LeftArrow className="inline -translate-y-0.5 scale-90 mr-1" aria-hidden="true" />
</Link>
</p>
<h1 className="text-5xl leading-[4rem] font-extralight"></h1>
<p className="mt-4 mb-6">
<Link href="/singup">
<span></span>
<RightArrow className="text-xs inline -translate-y-0.5 ml-1" aria-hidden="true" />
</Link>
</p>
<LoginForm backendURL={process.env.NEXT_PUBLIC_BACKEND_URL ?? ""} />
</div>
</main>
);
}

View File

@ -1,148 +0,0 @@
import { Dispatch, JSX, SetStateAction } from "react";
import { ApiRequestError, fetcher } from "@/lib/net";
import type { CaptchaVerificationRawResponse, ErrorResponse, SignUpResponse } from "@cvsa/backend";
import { Link } from "@/i18n/navigation";
import { LocalizedMessage } from "./LoginForm";
import { ErrorDialog } from "@/components/utils/ErrorDialog";
import { string, object, ValidationError, setLocale } from "yup";
setLocale({
mixed: {
default: "yup_errors.field_invalid",
required: () => ({ key: "yup_errors.field_required" })
},
string: {
min: ({ min }) => ({ key: "yup_errors.field_too_short", values: { min } }),
max: ({ max }) => ({ key: "yup_errors.field_too_big", values: { max } })
}
});
interface LoginFormData {
username: string;
password: string;
}
const FormSchema = object().shape({
username: string().required().max(50),
password: string().required().min(4).max(120)
});
const validateForm = async (
data: LoginFormData,
setShowDialog: Dispatch<SetStateAction<boolean>>,
setDialogContent: Dispatch<SetStateAction<JSX.Element>>,
translateErrorMessage: (item: LocalizedMessage | string, path?: string) => string
): Promise<LoginFormData | null> => {
const { username: usernameInput, password: passwordInput } = data;
try {
const formData = await FormSchema.validate({
username: usernameInput,
password: passwordInput
});
return {
username: formData.username,
password: formData.password
};
} catch (e) {
if (!(e instanceof ValidationError)) {
return null;
}
setShowDialog(true);
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)}>
<p>{translateErrorMessage(e.errors[0], e.path)}</p>
</ErrorDialog>
);
return null;
}
};
interface RequestSignUpArgs {
data: LoginFormData;
setShowDialog: Dispatch<SetStateAction<boolean>>;
setDialogContent: Dispatch<SetStateAction<JSX.Element>>;
translateErrorMessage: (item: LocalizedMessage | string, path?: string) => string;
setCaptchaUsedState: Dispatch<SetStateAction<boolean>>;
captchaResult: CaptchaVerificationRawResponse | undefined;
t: any;
}
export const requestLogin = async (url: string, { arg }: { arg: RequestSignUpArgs }) => {
const { data, setShowDialog, setDialogContent, translateErrorMessage, setCaptchaUsedState, captchaResult, t } = arg;
const res = await validateForm(data, setShowDialog, setDialogContent, translateErrorMessage);
if (!res) {
return;
}
const { username, password } = res;
try {
if (!captchaResult) {
const err = new ApiRequestError("Cannot get captcha result");
err.response = {
code: "UNKNOWN_ERROR",
message: "Cannot get captch verifiction result",
i18n: {
key: "captcha_failed_to_get"
}
} as ErrorResponse;
throw err;
}
setCaptchaUsedState(true);
const registrationResponse = await fetcher<SignUpResponse>(url, {
method: "POST",
withCredentials: true,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${captchaResult!.token}`
},
data: {
username: username,
password: password
}
});
return registrationResponse;
} catch (error) {
if (error instanceof ApiRequestError && error.response) {
const res = error.response as ErrorResponse;
setShowDialog(true);
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)} errorCode={res.code}>
<p>
<span>
{res.i18n
? t.rich(res.i18n.key, {
...res.i18n.values,
support: (chunks: string) => <Link href="/support">{chunks}</Link>
})
: res.message}
</span>
</p>
</ErrorDialog>
);
} else if (error instanceof Error) {
setShowDialog(true);
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)}>
<p></p>
<p>
<br />
{error.message}
</p>
</ErrorDialog>
);
} else {
setShowDialog(true);
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)} errorCode="UNKNOWN_ERROR">
<p></p>
<p>
<br />
<pre className="break-all">{JSON.stringify(error)}</pre>
</p>
</ErrorDialog>
);
}
}
};

View File

@ -1,60 +0,0 @@
import { ApiRequestError, fetcher } from "@/lib/net";
import { ErrorResponse } from "@cvsa/backend";
import { cookies } from "next/headers";
export async function POST() {
const backendURL = process.env.BACKEND_URL || "";
const cookieStore = await cookies();
const sessionID = cookieStore.get("session_id");
if (!sessionID) {
const response: ErrorResponse<string> = {
message: "No session_id provided",
errors: [],
code: "ENTITY_NOT_FOUND"
};
return new Response(JSON.stringify(response), {
status: 401
});
}
try {
const response = await fetcher(`${backendURL}/session/${sessionID.value}`, {
method: "DELETE"
});
const headers = response.headers;
return new Response(null, {
status: 204,
headers: {
"Set-Cookie": (headers["set-cookie"] || [""])[0]
}
});
} catch (error) {
if (error instanceof ApiRequestError && error.response) {
const res = error.response;
const code = error.code;
return new Response(JSON.stringify(res), {
status: code
});
} else if (error instanceof Error) {
const response: ErrorResponse<string> = {
message: error.message,
errors: [],
code: "SERVER_ERROR"
};
return new Response(JSON.stringify(response), {
status: 500
});
} else {
const response: ErrorResponse<string> = {
message: "Unknown error occurred",
errors: [],
code: "UNKNOWN_ERROR"
};
return new Response(JSON.stringify(response), {
status: 500
});
}
}
}

View File

@ -1,5 +0,0 @@
import { NotFound } from "@/components/utils/404";
export default async function NotFoundPage() {
return <NotFound />;
}

View File

@ -1,17 +0,0 @@
import { Header } from "@/components/shell/Header";
import { getCurrentUser } from "@/lib/userAuth";
export default async function Home() {
const user = await getCurrentUser();
return (
<>
<Header user={user} />
<main className="flex flex-col items-center justify-center h-full flex-grow gap-8 px-4">
<h1 className="text-4xl font-medium text-center"></h1>
<p>BV号或AV号~</p>
</main>
</>
);
}

View File

@ -1,147 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import TextField from "@/components/ui/TextField";
import LoadingSpinner from "@/components/icons/LoadingSpinner";
import { Portal } from "@/components/utils/Portal";
import { Dialog } from "@/components/ui/Dialog";
import { setLocale } from "yup";
import { useTranslations } from "next-intl";
import { useCaptcha } from "@/components/hooks/useCaptcha";
import useSWRMutation from "swr/mutation";
import { requestSignUp } from "./request";
import { FilledButton } from "@/components/ui/Buttons/FilledButton";
import { ErrorDialog } from "@/components/utils/ErrorDialog";
import { ApiRequestError } from "@/lib/net";
import { useRouter } from "next/navigation";
setLocale({
mixed: {
default: "field_invalid",
required: () => ({ key: "field_required" })
},
string: {
min: ({ min }) => ({ key: "field_too_short", values: { min } }),
max: ({ max }) => ({ key: "field_too_big", values: { max } })
}
});
export interface LocalizedMessage {
key: string;
values: {
[key: string]: number | string;
};
}
interface RegistrationFormProps {
backendURL: string;
}
const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
const [usernameInput, setUsername] = useState("");
const [passwordInput, setPassword] = useState("");
const [nicknameInput, setNickname] = useState("");
const [showDialog, setShowDialog] = useState(false);
const [dialogContent, setDialogContent] = useState(<></>);
const [isLoading, setLoading] = useState(false);
const t = useTranslations("");
const { startCaptcha, captchaResult, captchaUsed, setCaptchaUsedState, captchaError } = useCaptcha({
backendURL,
route: "POST-/user"
});
const { trigger } = useSWRMutation(`${backendURL}/user`, requestSignUp);
const router = useRouter();
const translateErrorMessage = (item: LocalizedMessage | string, path?: string) => {
if (typeof item === "string") {
return item;
}
return t(`${item.key}`, { ...item.values, field: path ? t(path) : "" });
};
const register = async () => {
try {
if (captchaUsed || !captchaResult) {
await startCaptcha();
}
const result = await trigger({
data: {
username: usernameInput,
password: passwordInput,
nickname: nicknameInput
},
setShowDialog,
captchaResult,
setCaptchaUsedState,
translateErrorMessage,
setDialogContent,
t
});
if (result) {
router.push("/");
}
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!captchaError || captchaError === undefined) return;
const err = captchaError as ApiRequestError;
setShowDialog(true);
if (err.code && err.code == -1) {
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)}>
<p></p>
</ErrorDialog>
);
}
}, [captchaError]);
useEffect(() => {
startCaptcha();
}, []);
return (
<form
className="w-full flex flex-col gap-6"
onSubmit={async (e) => {
setLoading(true);
e.preventDefault();
await register();
}}
>
<TextField
labelText="用户名"
inputText={usernameInput}
onInputTextChange={setUsername}
maxChar={50}
supportingText="*必填。用户名是唯一的,不区分大小写。"
/>
<TextField
labelText="密码"
type="password"
inputText={passwordInput}
onInputTextChange={setPassword}
supportingText="*必填。密码至少为 4 个字符。"
maxChar={120}
/>
<TextField
labelText="昵称"
inputText={nicknameInput}
onInputTextChange={setNickname}
supportingText="昵称可以重复。"
maxChar={30}
/>
<FilledButton type="submit" disabled={isLoading}>
{isLoading ? <LoadingSpinner /> : <span></span>}
</FilledButton>
<Portal>
<Dialog show={showDialog}>{dialogContent}</Dialog>
</Portal>
</form>
);
};
export default SignUpForm;

View File

@ -1,41 +0,0 @@
import { LeftArrow } from "@/components/icons/LeftArrow";
import { RightArrow } from "@/components/icons/RightArrow";
import SignUpForm from "./SignUpForm";
import { Link } from "@/i18n/navigation";
export default function SignupPage() {
return (
<main className="relative flex-grow pt-8 px-4 md:w-full md:h-full md:flex md:items-center md:justify-center">
<div
className="md:w-[40rem] rounded-md md:p-8 md:-translate-y-6
md:bg-surface-container md:dark:bg-dark-surface-container"
>
<p className="mb-2">
<Link href="/">
<LeftArrow className="inline -translate-y-0.5 scale-90 mr-1" aria-hidden="true" />
</Link>
</p>
<h1 className="text-5xl leading-[4rem] font-extralight"></h1>
<p className="mt-2 md:mt-3">
V
<br />
</p>
<p className="my-2">
<br />
V
</p>
<p className="mt-4 mb-7">
<Link href="/login">
<span></span>
<RightArrow className="text-xs inline -translate-y-0.5 ml-1" aria-hidden="true" />
</Link>
</p>
<SignUpForm backendURL={process.env.NEXT_PUBLIC_BACKEND_URL ?? ""} />
</div>
</main>
);
}

View File

@ -1,162 +0,0 @@
import { Dispatch, JSX, SetStateAction } from "react";
import { ApiRequestError, fetcher } from "@/lib/net";
import type { CaptchaVerificationRawResponse, ErrorResponse, SignUpResponse } from "@cvsa/backend";
import { Link } from "@/i18n/navigation";
import { LocalizedMessage } from "./SignUpForm";
import { ErrorDialog } from "@/components/utils/ErrorDialog";
import { string, object, ValidationError, setLocale } from "yup";
setLocale({
mixed: {
default: "yup_errors.field_invalid",
required: () => ({ key: "yup_errors.field_required" })
},
string: {
min: ({ min }) => ({ key: "yup_errors.field_too_short", values: { min } }),
max: ({ max }) => ({ key: "yup_errors.field_too_big", values: { max } })
}
});
interface SignUpFormData {
username: string;
password: string;
nickname?: string;
}
const FormSchema = object().shape({
username: string().required().max(50),
password: string().required().min(4).max(120),
nickname: string().optional().max(30)
});
const validateForm = async (
data: SignUpFormData,
setShowDialog: Dispatch<SetStateAction<boolean>>,
setDialogContent: Dispatch<SetStateAction<JSX.Element>>,
translateErrorMessage: (item: LocalizedMessage | string, path?: string) => string
): Promise<SignUpFormData | null> => {
const { username: usernameInput, password: passwordInput, nickname: nicknameInput } = data;
try {
const formData = await FormSchema.validate(
{
username: usernameInput,
password: passwordInput,
nickname: nicknameInput
},
{ abortEarly: false }
);
return {
username: formData.username,
password: formData.password,
nickname: formData.nickname
};
} catch (e) {
if (!(e instanceof ValidationError)) {
return null;
}
setShowDialog(true);
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)}>
<p></p>
<span>: </span>
<br />
<ol className="list-decimal list-inside">
{e.errors.map((item, i) => {
return <li key={i}>{translateErrorMessage(item, e.inner[i].path)}</li>;
})}
</ol>
</ErrorDialog>
);
return null;
}
};
interface RequestSignUpArgs {
data: SignUpFormData;
setShowDialog: Dispatch<SetStateAction<boolean>>;
setDialogContent: Dispatch<SetStateAction<JSX.Element>>;
translateErrorMessage: (item: LocalizedMessage | string, path?: string) => string;
setCaptchaUsedState: Dispatch<SetStateAction<boolean>>;
captchaResult: CaptchaVerificationRawResponse | undefined;
t: any;
}
export const requestSignUp = async (url: string, { arg }: { arg: RequestSignUpArgs }) => {
const { data, setShowDialog, setDialogContent, translateErrorMessage, setCaptchaUsedState, captchaResult, t } = arg;
const res = await validateForm(data, setShowDialog, setDialogContent, translateErrorMessage);
if (!res) {
return;
}
const { username, nickname, password } = res;
try {
if (!captchaResult) {
const err = new ApiRequestError("Cannot get captcha result");
err.response = {
code: "UNKNOWN_ERROR",
message: "Cannot get captch verifiction result",
i18n: {
key: "captcha_failed_to_get"
}
} as ErrorResponse;
throw err;
}
setCaptchaUsedState(true);
const registrationResponse = await fetcher<SignUpResponse>(url, {
method: "POST",
withCredentials: true,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${captchaResult!.token}`
},
data: {
username: username,
password: password,
nickname: nickname
}
});
return registrationResponse;
} catch (error) {
if (error instanceof ApiRequestError && error.response) {
const res = error.response as ErrorResponse;
setShowDialog(true);
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)} errorCode={res.code}>
<p></p>
<p>
: <br />
{res.i18n
? t.rich(res.i18n.key, {
...res.i18n.values,
support: (chunks: string) => <Link href="/support">{chunks}</Link>
})
: res.message}
</p>
</ErrorDialog>
);
} else if (error instanceof Error) {
setShowDialog(true);
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)}>
<p></p>
<p>
<br />
{error.message}
</p>
</ErrorDialog>
);
} else {
setShowDialog(true);
setDialogContent(
<ErrorDialog closeDialog={() => setShowDialog(false)} errorCode="UNKNOWN_ERROR">
<p></p>
<p>
<br />
<pre className="break-all">{JSON.stringify(error)}</pre>
</p>
</ErrorDialog>
);
}
}
};

View File

@ -1,175 +0,0 @@
import { format } from "date-fns";
import { zhCN } from "date-fns/locale";
import { getAllSnapshots } from "@/lib/db/snapshots/getAllSnapshots";
import { getAidFromBV } from "@/lib/db/bilibili_metadata/getAidFromBV";
import { getVideoMetadata } from "@/lib/db/bilibili_metadata/getVideoMetadata";
import { aidExists as idExists } from "@/lib/db/bilibili_metadata/aidExists";
import { notFound } from "next/navigation";
import { BiliVideoMetadataType, VideoSnapshotType } from "@cvsa/core";
import { Metadata } from "next";
import { DateTime } from "luxon";
const MetadataRow = ({ title, desc }: { title: string; desc: string | number | undefined | null }) => {
if (!desc) return <></>;
return (
<tr>
<td className="max-w-14 min-w-14 md:max-w-24 md:min-w-24 border dark:border-zinc-500 px-2 md:px-3 py-2 font-semibold">
{title}
</td>
<td className="break-all max-w-[calc(100vw-4.5rem)] border dark:border-zinc-500 px-4 py-2">{desc}</td>
</tr>
);
};
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const backendURL = process.env.BACKEND_URL;
const { id } = await params;
const res = await fetch(`${backendURL}/video/${id}/info`);
if (!res.ok) {
return {
title: "页面未找到 - 中 V 档案馆"
};
}
const data = await res.json();
return {
title: `${data.title} - 歌曲信息 - 中 V 档案馆`
};
}
export default async function VideoInfoPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
let videoInfo: BiliVideoMetadataType | null = null;
let snapshots: VideoSnapshotType[] = [];
async function getVideoAid(videoId: string | string[] | undefined) {
if (!videoId) return null;
const videoIdStr = Array.isArray(videoId) ? videoId[0] : videoId;
if (videoIdStr?.startsWith("av")) {
return parseInt(videoIdStr.slice(2));
} else if (videoIdStr?.startsWith("BV")) {
return getAidFromBV(videoIdStr);
}
return parseInt(videoIdStr);
}
const aid = await getVideoAid(id);
if (!aid) {
return notFound();
}
const exists = await idExists(aid);
if (!exists) {
return notFound();
}
try {
const videoData = await getVideoMetadata(aid);
const snapshotsData = await getAllSnapshots(aid);
videoInfo = videoData;
if (snapshotsData) {
snapshots = snapshotsData;
}
} catch (e) {
console.error(e);
}
if (!videoInfo) {
return notFound();
}
return (
<main className="flex flex-col items-center min-h-screen gap-8 mt-10 md:mt-6 relative z-0 overflow-x-auto pb-8">
<div className="w-full lg:max-w-4xl lg:mx-auto lg:p-6">
<h1 className="text-2xl font-medium ml-2 mb-4">
:{" "}
<a href={`https://www.bilibili.com/video/av${videoInfo.aid}`} className="underline">
av{videoInfo.aid}
</a>
</h1>
<div className="mb-6">
<h2 className="px-2 mb-2 text-xl font-medium"></h2>
<div className="overflow-x-auto max-w-full px-2">
<table className="table-fixed">
<tbody>
<MetadataRow title="ID" desc={videoInfo.id} />
<MetadataRow title="av 号" desc={videoInfo.aid} />
<MetadataRow title="BV 号" desc={videoInfo.bvid} />
<MetadataRow title="标题" desc={videoInfo.title} />
<MetadataRow title="描述" desc={videoInfo.description} />
<MetadataRow title="UID" desc={videoInfo.uid} />
<MetadataRow title="标签" desc={videoInfo.tags} />
<MetadataRow
title="发布时间"
desc={
videoInfo.published_at
? DateTime.fromJSDate(videoInfo.published_at).toFormat(
"yyyy-MM-dd HH:mm:ss"
)
: null
}
/>
<MetadataRow title="时长 (秒)" desc={videoInfo.duration} />
<MetadataRow
title="创建时间"
desc={DateTime.fromJSDate(videoInfo.created_at).toFormat("yyyy-MM-dd HH:mm:ss")}
/>
<MetadataRow title="封面" desc={videoInfo?.cover_url} />
</tbody>
</table>
</div>
</div>
<div>
<h2 className="px-2 mb-2 text-xl font-medium"></h2>
{snapshots && snapshots.length > 0 ? (
<div className="overflow-x-auto px-2">
<table className="table-auto w-full">
<thead>
<tr>
<th className="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th className="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th className="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th className="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th className="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th className="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th className="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
<th className="border dark:border-zinc-500 px-4 py-2 font-medium"></th>
</tr>
</thead>
<tbody>
{snapshots.map((snapshot) => (
<tr key={snapshot.id}>
<td className="border dark:border-zinc-500 px-4 py-2">
{DateTime.fromJSDate(snapshot.created_at).toFormat(
"yyyy-MM-dd HH:mm:ss"
)}
</td>
<td className="border dark:border-zinc-500 px-4 py-2">{snapshot.views}</td>
<td className="border dark:border-zinc-500 px-4 py-2">{snapshot.coins}</td>
<td className="border dark:border-zinc-500 px-4 py-2">{snapshot.likes}</td>
<td className="border dark:border-zinc-500 px-4 py-2">
{snapshot.favorites}
</td>
<td className="border dark:border-zinc-500 px-4 py-2">{snapshot.shares}</td>
<td className="border dark:border-zinc-500 px-4 py-2">
{snapshot.danmakus}
</td>
<td className="border dark:border-zinc-500 px-4 py-2">
{snapshot.replies}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<p></p>
)}
</div>
</div>
</main>
);
}

View File

@ -1,15 +0,0 @@
import HeaderServer from "@/components/shell/HeaderServer";
import React from "react";
export default async function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<HeaderServer />
{children}
</>
);
}

View File

@ -1,206 +0,0 @@
"use client";
import { N_ARRAY } from "@/lib/const";
import { UAParser } from "ua-parser-js";
import { useEffect, useState } from "react";
import { computeVdfInWorker } from "@/lib/vdf";
import { FilledButton } from "@/components/ui/Buttons/FilledButton";
let bigintSupported = typeof BigInt !== "undefined";
function generateRandomBigInt(min: bigint, max: bigint) {
const range = max - min;
const bitLength = range.toString(2).length;
const byteLength = Math.ceil(bitLength / 8);
const mask = (1n << BigInt(bitLength)) - 1n;
let result;
do {
const randomBytes = new Uint8Array(byteLength);
crypto.getRandomValues(randomBytes);
result = 0n;
for (let i = 0; i < byteLength; i++) {
result = (result << 8n) | BigInt(randomBytes[i]);
}
result = result & mask;
} while (result > range);
return min + result;
}
function generateValidG(N: bigint) {
if (N <= 4n) throw new Error("N must be > 4");
while (true) {
const r = generateRandomBigInt(2n, N - 1n);
const g = (r * r) % N;
if (g !== 1n && g !== 0n && g !== N - 1n) {
return g;
}
}
}
export const VDFtestCard = () => {
const [browserInfo, setBrowserInfo] = useState<string | null>(null);
const [isBenchmarking, setIsBenchmarking] = useState(false);
const [benchmarkResults, setBenchmarkResults] = useState<{ N: bigint; difficulty: bigint; time: number }[]>([]);
const [currentProgress, setCurrentProgress] = useState(0);
const [currentN, setCurrentN] = useState<bigint | null>(null);
const [currentDifficulty, setCurrentDifficulty] = useState<bigint | null>(null);
const [currentTestIndex, setCurrentTestIndex] = useState(0);
const difficulties = [BigInt(20000), BigInt(200000)];
const [testCombinations, setTestCombinations] = useState<{ N: bigint; difficulty: bigint }[]>([]);
const speedSampleIndex = 1;
const [speedSample, setSpeedSample] = useState<{ N: bigint; difficulty: bigint; time: number } | undefined>(
undefined
);
useEffect(() => {
// 创建需要测试的 N 和难度的组合
const combinations: { N: bigint; difficulty: bigint }[] = [];
N_ARRAY.forEach((n) => {
difficulties.forEach((difficulty) => {
combinations.push({ N: n, difficulty });
});
});
setTestCombinations(combinations);
const ua = navigator ? navigator.userAgent : "";
const { browser } = UAParser(ua);
setBrowserInfo(browser.name + " " + browser.version);
}, []);
async function startBenchmark() {
if (testCombinations.length === 0) {
alert("No N values provided in src/const N_ARRAY.");
return;
}
setIsBenchmarking(true);
setBenchmarkResults([]);
setCurrentTestIndex(0);
async function runTest(index: number) {
if (index >= testCombinations.length) {
setIsBenchmarking(false);
setCurrentN(null);
setCurrentDifficulty(null);
return;
}
const { N, difficulty } = testCombinations[index];
const g = generateValidG(N);
try {
const { time } = await computeVdfInWorker(g, N, difficulty, (progress) => {
setCurrentProgress(progress);
setCurrentN(N);
setCurrentDifficulty(difficulty);
});
setBenchmarkResults((prevResults) => [...prevResults, { N, difficulty, time }]);
setCurrentProgress(0);
setCurrentTestIndex((prevIndex) => prevIndex + 1);
runTest(index + 1);
} catch (error) {
setIsBenchmarking(false);
setCurrentN(null);
setCurrentDifficulty(null);
}
}
runTest(0);
}
function getAccumulatedTime() {
return benchmarkResults.reduce((acc, result) => acc + result.time, 0);
}
function calculateSpeed() {
const sample = benchmarkResults[speedSampleIndex];
if (!sample) return 0;
return (Number(sample.difficulty) / sample.time) * 1000;
}
useEffect(() => {
if (benchmarkResults.length > speedSampleIndex) {
setSpeedSample(benchmarkResults[speedSampleIndex]);
}
}, [benchmarkResults]);
return (
<div className="relative mt-8 mb-12 h-auto duration-300">
<h2 className="text-2xl font-medium mb-5">VDF </h2>
{!bigintSupported ? (
<p className="text-red-500 dark:text-red-400"> BigInt</p>
) : !isBenchmarking ? (
<FilledButton onClick={startBenchmark} disabled={!bigintSupported} shape="square">
</FilledButton>
) : null}
{isBenchmarking && (
<>
<p className="mb-8">
: {currentTestIndex + 1}/{testCombinations.length}
</p>
{currentN !== null && currentDifficulty !== null && (
<>
<p className="mb-2">: {currentN.toString(2).length} </p>
<p className="mb-2">: {currentDifficulty.toLocaleString()}</p>
<div className="w-full rounded-full h-1 relative overflow-hidden">
<div
className="bg-primary dark:bg-dark-primary h-full rounded-full absolute"
style={{ width: `${currentProgress}%` }}
></div>
<div
className="bg-secondary-container dark:bg-dark-secondary-container h-full rounded-full absolute right-0"
style={{ width: `calc(${100 - currentProgress}% - 0.25rem)` }}
></div>
<div className="bg-primary dark:bg-dark-primary h-full w-1 rounded-full absolute right-0"></div>
</div>
</>
)}
</>
)}
{benchmarkResults.length > 0 && !isBenchmarking && (
<>
<h3 className="text-lg font-medium mt-4 mb-2"></h3>
<p className="mb-4 text-sm">
{(getAccumulatedTime() / 1000).toFixed(3)} . <br />
: {Math.round(calculateSpeed()).toLocaleString()} / . <br />
<span className="text-sm text-on-surface-variant dark:text-dark-on-surface-variant">
N = {speedSample?.N.toString(2).length} bits, T = {speedSample?.difficulty}{" "}
.
</span>
<br />
{browserInfo && <>{browserInfo}</>}
</p>
<table className="w-full text-sm text-left rtl:text-right mt-4">
<thead className="text-sm uppercase font-medium border-b border-outline dark:border-dark-outline">
<tr>
<th scope="col" className="px-6 py-3">
(ms)
</th>
<th scope="col" className="px-6 py-3">
N (bits)
</th>
<th scope="col" className="px-6 py-3">
T ()
</th>
</tr>
</thead>
<tbody>
{benchmarkResults.map((result) => (
<tr
key={`${result.N}-${result.difficulty}-${result.time}`}
className="border-b border-outline-variant dark:border-dark-outline-variant"
>
<td className="px-6 py-4 whitespace-nowrap">{result.time.toFixed(2)}</td>
<td className="px-6 py-4 whitespace-nowrap">{result.N.toString(2).length}</td>
<td className="px-6 py-4 whitespace-nowrap">{Number(result.difficulty)}</td>
</tr>
))}
</tbody>
</table>
</>
)}
</div>
);
};

View File

@ -1,44 +0,0 @@
import { VDFtestCard } from "./TestCard";
import HeaderServer from "@/components/shell/HeaderServer";
export default async function VdfBenchmarkPage() {
return (
<>
<HeaderServer />
<div className="md:w-2/3 lg:w-1/2 xl:w-[37%] md:mx-auto mx-6 mb-12">
<VDFtestCard />
<div>
<h2 className="text-xl font-medium leading-10"></h2>
<div className="text-on-surface-variant dark:text-dark-on-surface-variant">
<p>
<br />
VDF (Verifiable Delayed Function, )
<br />
CAPTCHA
<br />
使
使
<br />
</p>
<p>
<br />
</p>
<p>
<a href="mailto:contact@alikia2x.com">contact@alikia2x.com</a> QQ
<a href="https://qm.qq.com/q/WS8zyhlcEU">1559913735</a>
CPU
<br />
使
<br />
<br />
</p>
</div>
</div>
</div>
</>
);
}

View File

@ -1,46 +0,0 @@
"use client";
import { FilledButton } from "@/components/ui/Buttons/FilledButton";
import { Dialog, DialogButton, DialogButtonGroup, DialogHeadline, DialogSupportingText } from "@/components/ui/Dialog";
import { Portal } from "@/components/utils/Portal";
import { useRouter } from "@/i18n/navigation";
import { useState } from "react";
export const LogoutButton: React.FC = () => {
const [showDialog, setShowDialog] = useState(false);
const router = useRouter();
return (
<>
<FilledButton
shape="square"
className="mt-5 !text-on-error dark:!text-dark-on-error !bg-error dark:!bg-dark-error font-medium"
onClick={() => setShowDialog(true)}
>
</FilledButton>
<Portal>
<Dialog show={showDialog}>
<DialogHeadline></DialogHeadline>
<DialogSupportingText>退</DialogSupportingText>
<DialogButtonGroup close={() => setShowDialog(false)}>
<DialogButton onClick={() => setShowDialog(false)}></DialogButton>
<DialogButton
onClick={async () => {
try {
await fetch("/logout", {
method: "POST"
});
router.push("/");
} finally {
setShowDialog(false);
}
}}
>
</DialogButton>
</DialogButtonGroup>
</Dialog>
</Portal>
</>
);
};

View File

@ -1,64 +0,0 @@
import { getUserProfile, User } from "@/lib/userAuth";
import { notFound } from "next/navigation";
import { format } from "date-fns";
import { zhCN } from "date-fns/locale";
import { LogoutButton } from "./LogoutButton";
import { numeric } from "yup-numeric";
import { getTranslations } from "next-intl/server";
import HeaderServer from "@/components/shell/HeaderServer";
import { DateTime } from "luxon";
const uidSchema = numeric().integer().min(0);
interface SignupTimeProps {
user: User;
}
const SignupTime: React.FC<SignupTimeProps> = ({ user }: SignupTimeProps) => {
return (
<p className="mt-4">
&nbsp;
{DateTime.fromJSDate(user.createdAt).toFormat("yyyy-MM-dd HH:mm:ss")}
&nbsp;
</p>
);
};
export default async function ProfilePage({ params }: { params: Promise<{ uid: string }> }) {
const { uid } = await params;
const t = await getTranslations("profile_page");
let parsedUID: number;
try {
uidSchema.validate(uid);
parsedUID = parseInt(uid);
} catch (error) {
return notFound();
}
const user = await getUserProfile(parsedUID);
if (!user) {
return notFound();
}
const displayName = user.nickname || user.username;
const loggedIn = user.isLoggedIn;
return (
<>
<HeaderServer />
<main className="md:w-xl lg:w-2xl xl:w-3xl md:mx-auto pt-6">
<h1>
<span className="text-4xl font-extralight">{displayName}</span>
<span className="ml-2 text-on-surface-variant dark:text-dark-on-surface-variant">
UID{user.uid}
</span>
</h1>
<SignupTime user={user} />
<p className="mt-4">{t(`role.${user.role}`)}</p>
{loggedIn && <LogoutButton />}
</main>
</>
);
}

View File

@ -1,103 +0,0 @@
import { Suspense } from "react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Metadata } from "next";
import type { VideoInfoData } from "@cvsa/core";
import { DateTime } from "luxon";
const StatRow = ({ title, description }: { title: string; description?: number }) => {
return (
<div className="flex justify-between w-36">
<span>{title}</span>
<span>{description?.toLocaleString() ?? "N/A"}</span>
</div>
);
};
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const backendURL = process.env.BACKEND_URL;
const { id } = await params;
const res = await fetch(`${backendURL}/video/${id}/info`);
if (!res.ok) {
return {
title: "页面未找到 - 中 V 档案馆"
};
}
const data = await res.json();
return {
title: `${data.title} - 视频信息 - 中 V 档案馆`
};
}
const VideoInfo = async ({ id }: { id: string }) => {
const backendURL = process.env.BACKEND_URL;
const res = await fetch(`${backendURL}/video/${id}/info`);
if (!res.ok) {
return notFound();
}
const data: VideoInfoData = await res.json();
return (
<div className="w-full lg:max-w-4xl lg:mx-auto lg:p-6 px-4">
<h2 className="text-lg md:text-2xl mb-2">
<Link href={`https://www.bilibili.com/video/${data.bvid}`}>{data.title || data.bvid}</Link>
</h2>
<p className="text-sm md:text-base font-normal text-on-surface-variant dark:text-dark-on-surface-variant mb-4">
<span>
{data.bvid} · av{data.aid}
</span>
<br />
<span> {DateTime.fromSeconds(data.pubdate).toFormat("yyyy-MM-dd HH:mm:ss")}</span>
<br />
<span>{(data.stat?.view ?? 0).toLocaleString()}</span> ·{" "}
<span>{(data.stat?.danmaku ?? 0).toLocaleString()}</span>
<br />
<span>
: {data.tname}, tid{data.tid} · v2: {data.tname_v2}, tid
{data.tid_v2}
</span>
</p>
<img src={data.pic} referrerPolicy="no-referrer" className="rounded-lg" alt="Video cover" />
<h3 className="font-medium text-lg mt-6 mb-1"></h3>
<pre className="max-w-full wrap-anywhere break-all text-on-surface-variant text-sm md:text-base whitespace-pre-wrap dark:text-dark-on-surface-variant font-zh">
{data.desc}
</pre>
<div className="mb-6 mt-4">
<h2 className="mb-2 text-xl font-medium"></h2>
<div className="flex flex-col gap-1">
<StatRow title="播放" description={data.stat?.view} />
<StatRow title="点赞" description={data.stat?.like} />
<StatRow title="收藏" description={data.stat?.favorite} />
<StatRow title="硬币" description={data.stat?.coin} />
<StatRow title="评论" description={data.stat?.reply} />
<StatRow title="弹幕" description={data.stat?.danmaku} />
<StatRow title="分享" description={data.stat?.share} />
</div>
</div>
</div>
);
};
export default async function VideoPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return (
<main className="flex flex-col items-center flex-grow gap-8 mt-10 md:mt-6 relative z-0 overflow-x-auto pb-8">
<Suspense
fallback={
<main className="flex flex-col flex-grow items-center justify-center gap-8">
<h1 className="text-4xl font-extralight"></h1>
</main>
}
>
<VideoInfo id={id} />
</Suspense>
</main>
);
}

View File

@ -1,15 +0,0 @@
import HeaderServer from "@/components/shell/HeaderServer";
import React from "react";
export default async function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<>
<HeaderServer />
{children}
</>
);
}

View File

@ -1,29 +0,0 @@
import type { Metadata } from "next";
import "./[locale]/global.css";
import React from "react";
import { NextIntlClientProvider } from "next-intl";
export const metadata: Metadata = {
title: "中 V 档案馆"
};
export default async function RootLayout({
children
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-CN">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body className="min-h-screen flex flex-col">
<NextIntlClientProvider>
{children}
<div id="portal-root"></div>
</NextIntlClientProvider>
</body>
</html>
);
}

View File

@ -1,5 +0,0 @@
import { NotFound } from "@/components/utils/404";
export default async function NotFoundPage() {
return <NotFound />;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +0,0 @@
import useSWRMutation from "swr/mutation";
import { useState } from "react";
import type { CaptchaVerificationRawResponse, CaptchaSessionRawResponse } from "@cvsa/backend";
import { fetcher } from "@/lib/net";
import { computeVdfInWorker } from "@/lib/vdf";
interface UseCaptchaOptions {
backendURL: string;
route: string;
}
export function useCaptcha({ backendURL, route }: UseCaptchaOptions) {
const fullUrl = `${backendURL}/captcha/session`;
const [isUsed, setIsUsed] = useState(false);
const { trigger, data, isMutating, error } = useSWRMutation<CaptchaVerificationRawResponse, Error>(
fullUrl,
async (url: string) => {
const sessionRes = await fetcher<CaptchaSessionRawResponse>(url, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
data: { route }
});
const { g, n, t, id } = sessionRes;
if (!g || !n || !t || !id) {
throw new Error("Missing required CAPTCHA parameters");
}
const ans = await computeVdfInWorker(BigInt(g), BigInt(n), BigInt(t));
const resultUrl = new URL(`${backendURL}/captcha/${id}/result`);
resultUrl.searchParams.set("ans", ans.result.toString());
const result = await fetcher<CaptchaVerificationRawResponse>(resultUrl.toString());
setIsUsed(false);
return result;
}
);
return {
startCaptcha: trigger,
captchaResult: data,
isLoadingCaptcha: isMutating,
captchaError: error,
captchaUsed: isUsed,
setCaptchaUsedState: setIsUsed
};
}

View File

@ -1,12 +0,0 @@
import React from "react";
export const AccountIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M5.85 17.1q1.275-.975 2.85-1.537T12 15t3.3.563t2.85 1.537q.875-1.025 1.363-2.325T20 12q0-3.325-2.337-5.663T12 4T6.337 6.338T4 12q0 1.475.488 2.775T5.85 17.1M12 13q-1.475 0-2.488-1.012T8.5 9.5t1.013-2.488T12 6t2.488 1.013T15.5 9.5t-1.012 2.488T12 13m0 9q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"
></path>
</svg>
</div>
);

View File

@ -1,12 +0,0 @@
import React from "react";
export const CloseIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z"
/>
</svg>
</div>
);

View File

@ -1,12 +0,0 @@
import React from "react";
export const HomeIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1"
/>
</svg>
</div>
);

View File

@ -1,12 +0,0 @@
import React from "react";
export const InfoIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M11 17h2v-6h-2zm1-8q.425 0 .713-.288T13 8t-.288-.712T12 7t-.712.288T11 8t.288.713T12 9m0 13q-2.075 0-3.9-.788t-3.175-2.137T2.788 15.9T2 12t.788-3.9t2.137-3.175T8.1 2.788T12 2t3.9.788t3.175 2.137T21.213 8.1T22 12t-.788 3.9t-2.137 3.175t-3.175 2.138T12 22"
/>
</svg>
</div>
);

View File

@ -1,13 +0,0 @@
import React from "react";
export const LeftArrow: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg {...props} width="1em" height="1em" viewBox="0 0 10.72 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
id="path"
d="M10.64 4.34C10.69 4.23 10.72 4.11 10.72 3.97C10.72 3.79 10.67 3.65 10.59 3.55C10.55 3.51 10.5 3.46 10.46 3.44C10.39 3.4 10.31 3.38 10.22 3.38L2.16 3.38L4.57 0.97C4.61 0.9 4.7 0.73 4.7 0.63C4.7 0.6 4.7 0.57 4.7 0.52C4.67 0.4 4.6 0.28 4.5 0.16C4.39 0.06 4.29 0 4.17 -0.02C4.13 -0.04 4.09 -0.05 4.04 -0.05C3.95 -0.05 3.87 -0.02 3.81 0.01C3.76 0.04 3.73 0.06 3.7 0.09L0.22 3.58C0.07 3.72 0 3.85 0 3.97C0 4.09 0.07 4.23 0.22 4.38L3.7 7.87C3.82 7.95 3.93 8 4.04 8C4.18 7.98 4.37 7.91 4.5 7.79C4.6 7.68 4.67 7.56 4.7 7.44C4.7 7.4 4.7 7.36 4.7 7.32C4.7 7.26 4.7 7.21 4.67 7.16C4.66 7.1 4.62 7.04 4.57 7L2.16 4.59L10.22 4.59C10.3 4.59 10.37 4.57 10.43 4.54C10.49 4.51 10.59 4.4 10.64 4.34Z"
fill="currentColor"
fillOpacity="1"
fillRule="evenodd"
/>
</svg>
);

View File

@ -1,39 +0,0 @@
import React from "react";
const LoadingSpinner: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<g stroke="currentColor" strokeWidth="1">
<circle cx="12" cy="12" r="9.5" fill="none" strokeLinecap="round" strokeWidth="3">
<animate
attributeName="stroke-dasharray"
calcMode="spline"
dur="1.5s"
keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1"
keyTimes="0;0.475;0.95;1"
repeatCount="indefinite"
values="0 150;42 150;42 150;42 150"
/>
<animate
attributeName="stroke-dashoffset"
calcMode="spline"
dur="1.5s"
keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1"
keyTimes="0;0.475;0.95;1"
repeatCount="indefinite"
values="0;-16;-59;-59"
/>
</circle>
<animateTransform
attributeName="transform"
dur="2s"
repeatCount="indefinite"
type="rotate"
values="0 12 12;360 12 12"
/>
</g>
</svg>
</div>
);
export default LoadingSpinner;

View File

@ -1,12 +0,0 @@
import React from "react";
export const LoginIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M13 21q-.425 0-.712-.288T12 20t.288-.712T13 19h6V5h-6q-.425 0-.712-.288T12 4t.288-.712T13 3h6q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm-1.825-8H4q-.425 0-.712-.288T3 12t.288-.712T4 11h7.175L9.3 9.125q-.275-.275-.275-.675t.275-.7t.7-.313t.725.288L14.3 11.3q.3.3.3.7t-.3.7l-3.575 3.575q-.3.3-.712.288T9.3 16.25q-.275-.3-.262-.712t.287-.688z"
></path>
</svg>
</div>
);

View File

@ -1,15 +0,0 @@
import React from "react";
export const MenuIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg width="1em" height="1em" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
id="path"
d="M4.66 21C4.33 21 4.05 20.88 3.83 20.66C3.61 20.44 3.5 20.16 3.5 19.83C3.49 19.5 3.61 19.22 3.83 19C4.06 18.77 4.33 18.66 4.66 18.66L23.33 18.66C23.66 18.66 23.94 18.77 24.16 19C24.38 19.22 24.5 19.5 24.5 19.83C24.49 20.16 24.38 20.44 24.16 20.66C23.94 20.88 23.66 21 23.33 21L4.66 21ZM4.66 15.16C4.33 15.16 4.05 15.05 3.83 14.83C3.61 14.6 3.5 14.32 3.5 14C3.49 13.67 3.61 13.39 3.83 13.16C4.06 12.94 4.33 12.83 4.66 12.83L23.33 12.83C23.66 12.83 23.94 12.94 24.16 13.16C24.38 13.39 24.5 13.67 24.5 14C24.49 14.32 24.38 14.6 24.16 14.83C23.94 15.05 23.66 15.16 23.33 15.16L4.66 15.16ZM4.66 9.33C4.33 9.33 4.05 9.22 3.83 8.99C3.61 8.77 3.5 8.49 3.5 8.16C3.49 7.83 3.61 7.56 3.83 7.33C4.06 7.11 4.33 7 4.66 7L23.33 7C23.66 7 23.94 7.11 24.16 7.33C24.38 7.56 24.5 7.83 24.5 8.16C24.49 8.49 24.38 8.77 24.16 8.99C23.94 9.22 23.66 9.33 23.33 9.33L4.66 9.33Z"
fill="currentColor"
fillOpacity="1"
fillRule="nonzero"
/>
</svg>
</div>
);

View File

@ -1,12 +0,0 @@
import React from "react";
export const RegisterIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M15 14c-2.67 0-8 1.33-8 4v2h16v-2c0-2.67-5.33-4-8-4m-9-4V7H4v3H1v2h3v3h2v-3h3v-2m6 2a4 4 0 0 0 4-4a4 4 0 0 0-4-4a4 4 0 0 0-4 4a4 4 0 0 0 4 4"
/>
</svg>
</div>
);

View File

@ -1,12 +0,0 @@
import React from "react";
export const RightArrow: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg {...props} width="1em" height="1em" viewBox="0 0 10.72 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0.08 3.66Q0 3.82 0 4.03Q0 4.29 0.13 4.45Q0.13 4.46 0.13 4.46Q0.19 4.52 0.26 4.56Q0.36 4.62 0.49 4.62L8.56 4.62L6.15 7.03Q6.1 7.07 6.08 7.12Q6.01 7.22 6.01 7.36Q6.01 7.41 6.02 7.47Q6.06 7.66 6.22 7.83Q6.37 7.98 6.54 8.02Q6.61 8.04 6.67 8.04Q6.81 8.04 6.91 7.98Q6.97 7.95 7.01 7.9L10.5 4.42Q10.72 4.2 10.72 4.03Q10.72 3.84 10.5 3.62L7.01 0.13Q6.84 0 6.67 0Q6.64 0 6.61 0Q6.41 0.02 6.22 0.21Q6.06 0.37 6.02 0.56Q6.01 0.62 6.01 0.68Q6.01 0.76 6.04 0.84Q6.07 0.93 6.15 1L8.56 3.41L0.49 3.41Q0.38 3.41 0.29 3.46Q0.2 3.5 0.13 3.59Q0.1 3.63 0.08 3.66Z"
fill="currentColor"
fillOpacity="1"
fillRule="evenodd"
/>
</svg>
);

View File

@ -1,12 +0,0 @@
import React from "react";
export const SearchIcon: React.FC<React.HTMLAttributes<HTMLDivElement>> = (props) => (
<div {...props}>
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
<path
fill="currentColor"
d="m19.6 21l-6.3-6.3q-.75.6-1.725.95T9.5 16q-2.725 0-4.612-1.888T3 9.5t1.888-4.612T9.5 3t4.613 1.888T16 9.5q0 1.1-.35 2.075T14.7 13.3l6.3 6.3zM9.5 14q1.875 0 3.188-1.312T14 9.5t-1.312-3.187T9.5 5T6.313 6.313T5 9.5t1.313 3.188T9.5 14"
/>
</svg>
</div>
);

View File

@ -1,157 +0,0 @@
"use client";
import TitleLight from "@/public/icons/标题-浅色.svg";
import TitleDark from "@/public/icons/标题-深色.svg";
import LogoMobileLight from "@/public/icons/TitleBar Mobile Light.svg";
import LogoMobileDark from "@/public/icons/TitleBar Mobile Dark.svg";
import DarkModeImage from "@/components/utils/DarkModeImage";
import React, { useState } from "react";
import { NavigationDrawer } from "@/components/ui/NavigatinDrawer";
import { Portal } from "@/components/utils/Portal";
import { SearchBox } from "@/components/ui/SearchBox";
import { MenuIcon } from "@/components/icons/MenuIcon";
import { SearchIcon } from "@/components/icons/SearchIcon";
import { InfoIcon } from "@/components/icons/InfoIcon";
import { HomeIcon } from "@/components/icons/HomeIcon";
import { TextButton } from "@/components/ui/Buttons/TextButton";
import { Link } from "@/i18n/navigation";
import { LoginIcon } from "../icons/LoginIcon";
import { AccountIcon } from "../icons/AccountIcon";
import { User } from "@/lib/userAuth";
interface HeaderProps {
user: User | null;
}
export const HeaderDestop = ({ user }: HeaderProps) => {
return (
<div className="hidden md:flex relative top-0 left-0 w-full h-28 z-20 justify-between">
<div className="w-[305px] xl:ml-8 inline-flex items-center">
<Link href="/">
<DarkModeImage
lightSrc={TitleLight}
darkSrc={TitleDark}
alt="logo"
className="w-[305px] h-24 inline-block max-w-[15rem] lg:max-w-[305px]"
/>
</Link>
</div>
<SearchBox />
<div
className="inline-flex relative gap-6 h-full lg:right-12
text-xl font-medium items-center w-[15rem] min-w-[8rem] mr-4 lg:mr-0 lg:w-[305px] justify-end"
>
{user ? (
<Link href={`/user/${user.uid}/profile`}>{user.nickname || user.username}</Link>
) : (
<Link href="/login"></Link>
)}
<Link href="/about"></Link>
</div>
</div>
);
};
export const HeaderMobile = ({ user }: HeaderProps) => {
const [showDrawer, setShowDrawer] = useState(false);
const [showsearchBox, setShowsearchBox] = useState(false);
return (
<>
<Portal>
<NavigationDrawer show={showDrawer} onClose={() => setShowDrawer(false)}>
<div className="flex flex-col w-full gap-2">
<div className="w-full h-14 flex items-center px-4 mt-3 pl-6">
<DarkModeImage
lightSrc={LogoMobileLight}
darkSrc={LogoMobileDark}
alt="Logo"
className="w-30 h-10"
/>
</div>
<Link href="/">
<TextButton className="w-full h-14 flex px-4 justify-start" size="m">
<div className="flex items-center">
<HomeIcon className="text-2xl pr-4" />
<span></span>
</div>
</TextButton>
</Link>
<Link href="/about">
<TextButton className="w-full h-14 flex px-4 justify-start" size="m">
<div className="flex items-center">
<InfoIcon className="text-2xl pr-4" />
<span></span>
</div>
</TextButton>
</Link>
{user ? (
<Link href={`/user/${user.uid}/profile`}>
<TextButton className="w-full h-14 flex justify-start" size="m">
<div className="flex items-center w-72">
<AccountIcon className="text-2xl pr-4" />
<span>{user.nickname || user.username}</span>
</div>
</TextButton>
</Link>
) : (
<Link href="/login">
<TextButton className="w-full h-14 flex px-4 justify-start" size="m">
<div className="flex items-center w-72">
<LoginIcon className="text-2xl pr-4" />
<span></span>
</div>
</TextButton>
</Link>
)}
</div>
</NavigationDrawer>
</Portal>
<div className="md:hidden relative top-0 left-0 w-full h-16 z-20">
{!showsearchBox && (
<button
className="inline-flex absolute left-0 ml-4 h-full items-center dark:text-white text-2xl"
onClick={() => setShowDrawer(true)}
>
<MenuIcon />
</button>
)}
{!showsearchBox && (
<div className="absolute left-1/2 -translate-x-1/2 -translate-y-0.5 inline-flex h-full items-center">
<Link href="/">
<DarkModeImage
lightSrc={LogoMobileLight}
darkSrc={LogoMobileDark}
alt="Logo"
className="w-24 h-8 translate-y-[2px]"
/>
</Link>
</div>
)}
{showsearchBox && <SearchBox close={() => setShowsearchBox(false)} />}
{!showsearchBox && (
<button
className="inline-flex absolute right-0 h-full items-center mr-4"
onClick={() => setShowsearchBox(!showsearchBox)}
>
<SearchIcon className="text-[1.625rem]" />
</button>
)}
</div>
</>
);
};
export const Header = (props: HeaderProps) => {
return (
<>
<HeaderDestop {...props} />
<HeaderMobile {...props} />
</>
);
};

View File

@ -1,7 +0,0 @@
import { Header } from "@/components/shell/Header";
import { getCurrentUser } from "@/lib/userAuth";
export default async function HeaderServer() {
const user = await getCurrentUser();
return <Header user={user} />;
}

View File

@ -1,43 +0,0 @@
"use client";
import useRipple from "@/components/utils/useRipple";
interface FilledButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
size?: "xs" | "s" | "m" | "l" | "xl";
shape?: "round" | "square";
children?: React.ReactNode;
ripple?: boolean;
}
export const FilledButton = ({
children,
size = "s",
shape = "round",
className,
ripple = true,
...rest
}: FilledButtonProps) => {
let sizeClasses = "text-sm leading-5 h-10 px-4";
let shapeClasses = shape === "round" ? "rounded-full" : "rounded-xl";
if (size === "m") {
sizeClasses = "text-base leading-6 h-14 px-6";
shapeClasses = shape === "round" ? "rounded-full" : "rounded-2xl";
}
const { onMouseDown, onTouchStart } = useRipple({ ripple });
return (
<button
className={`bg-primary dark:bg-dark-primary text-on-primary dark:text-dark-on-primary duration-150 select-none
flex items-center justify-center relative overflow-hidden
${sizeClasses} ${shapeClasses} ${className}`}
{...rest}
onMouseDown={onMouseDown}
onTouchStart={onTouchStart}
>
<div className="absolute w-full h-full hover:bg-on-surface-variant/10"></div>
{children}
</button>
);
};

View File

@ -1,53 +0,0 @@
"use client";
import useRipple from "@/components/utils/useRipple";
interface TextButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
size?: "xs" | "s" | "m" | "l" | "xl";
shape?: "round" | "square";
children?: React.ReactNode;
ripple?: boolean;
ref?: React.Ref<HTMLButtonElement>;
}
export const TextButton = ({
children,
size = "s",
shape = "round",
className = "",
disabled,
ref,
ripple = true,
...rest
}: TextButtonProps) => {
let sizeClasses = "text-sm leading-5 h-10 px-4";
let shapeClasses = "rounded-full";
if (size === "m") {
sizeClasses = "text-base leading-6 h-14 px-6";
shapeClasses = shape === "round" ? "rounded-full" : "rounded-2xl";
}
const { onMouseDown, onTouchStart } = useRipple({ ripple });
return (
<button
className={`text-primary dark:text-dark-primary duration-150 select-none
flex items-center justify-center relative overflow-hidden
disabled:text-on-surface/40 disabled:dark:text-dark-on-surface/40
${sizeClasses} ${shapeClasses} ${className}`}
{...rest}
onMouseDown={onMouseDown}
onTouchStart={onTouchStart}
disabled={disabled}
ref={ref}
>
<div
className={`absolute w-full h-full enabled:hover:bg-primary/10 enabled:dark:hover:bg-dark-primary/10
${disabled && "bg-on-surface/10 dark:bg-dark-on-surface/10"}
left-0 top-0`}
></div>
{children}
</button>
);
};

View File

@ -1,252 +0,0 @@
import { motion, AnimatePresence } from "framer-motion";
import React, { useRef } from "react";
import { TextButton } from "./Buttons/TextButton";
import { useEffect, useState } from "react";
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import { useKeyboardShortcuts } from "@/components/utils/useKeyboardEvents";
import { UAParser } from "ua-parser-js";
const focusedButtonAtom = atom(-1);
export const useDisableBodyScroll = (open: boolean) => {
useEffect(() => {
if (open) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
}, [open]);
};
type OptionalChidrenProps<T = React.HTMLAttributes<HTMLElement>> = T & {
children?: React.ReactNode;
};
type HeadElementAttr = React.HTMLAttributes<HTMLHeadElement>;
type DivElementAttr = React.HTMLAttributes<HTMLDivElement>;
type ButtonElementAttr = React.HTMLAttributes<HTMLButtonElement>;
type DialogHeadlineProps = OptionalChidrenProps<HeadElementAttr>;
type DialogSupportingTextProps = OptionalChidrenProps<DivElementAttr>;
type DialogButtonGroupProps = DivElementAttr & {
children: React.ReactElement<DialogButtonProps> | React.ReactElement<DialogButtonProps>[];
close: () => void;
};
interface DialogButtonProps extends OptionalChidrenProps<ButtonElementAttr> {
onClick?: React.MouseEventHandler<HTMLButtonElement>;
index?: number;
}
interface DialogProps extends OptionalChidrenProps<DivElementAttr> {
show: boolean;
children?: React.ReactNode;
}
export const DialogHeadline: React.FC<DialogHeadlineProps> = ({
children,
className,
...rest
}: DialogHeadlineProps) => {
return (
<h2 className={"text-2xl leading-8 text-on-surface dark:text-dark-on-surface " + className || ""} {...rest}>
{children}
</h2>
);
};
export const DialogSupportingText: React.FC<DialogSupportingTextProps> = ({
children,
className,
...rest
}: DialogHeadlineProps) => {
return (
<div
className={
"mt-4 text-sm leading-5 mb-6 text-on-surface-variant dark:text-dark-on-surface-variant " + className ||
""
}
{...rest}
>
{children}
</div>
);
};
export const DialogButton: React.FC<DialogButtonProps> = ({ children, onClick, index, ...rest }: DialogButtonProps) => {
const buttonRef = useRef<HTMLButtonElement>(null);
const focusedButton = useAtomValue(focusedButtonAtom);
useEffect(() => {
if (!buttonRef.current) return;
if (focusedButton === index) buttonRef.current.focus();
}, [focusedButton]);
return (
<TextButton onClick={onClick} {...rest} ref={buttonRef}>
{children}
</TextButton>
);
};
export const DialogButtonGroup: React.FC<DialogButtonGroupProps> = ({
children,
close,
...rest
}: DialogButtonGroupProps) => {
const [focusedButton, setFocusedButton] = useAtom(focusedButtonAtom);
const count = React.Children.count(children);
useKeyboardShortcuts([
{
key: "Tab",
callback: () => {
setFocusedButton((focusedButton + 1) % count);
},
preventDefault: true
},
{
key: "Escape",
callback: close,
preventDefault: true
}
]);
return (
<div className="flex justify-end gap-2" {...rest}>
{React.Children.map(children, (child, index) => {
if (React.isValidElement<DialogButtonProps>(child) && child.type === DialogButton) {
return React.cloneElement(child, {
index: index
});
}
return child;
})}
</div>
);
};
const useCompabilityCheck = () => {
const [supported, setSupported] = useState(false);
useEffect(() => {
const parser = new UAParser(navigator.userAgent);
const result = parser.getResult();
const { name: browserName, version: browserVersion } = result.browser;
let isSupported = false;
if (!browserVersion) {
return;
}
const [major] = browserVersion.split(".").map(Number);
switch (browserName) {
case "Chromium":
isSupported = major >= 107;
break;
case "Firefox":
isSupported = major >= 66;
break;
case "Safari":
isSupported = major >= 16;
break;
default:
isSupported = false;
break;
}
setSupported(isSupported);
}, []);
return supported;
};
export const Dialog: React.FC<DialogProps> = ({ show, children, className }: DialogProps) => {
const dialogRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const setFocusedButton = useSetAtom(focusedButtonAtom);
const isSupported = useCompabilityCheck();
useEffect(() => {
if (!contentRef.current || !dialogRef.current) return;
const contentHeight = contentRef.current.offsetHeight;
const halfSize = (contentHeight + 48) / 2;
dialogRef.current.style.top = `calc(50% - ${halfSize}px)`;
if (!isSupported) {
return;
}
dialogRef.current.style.transition = "grid-template-rows cubic-bezier(0.05, 0.7, 0.1, 1.0) 0.35s";
if (show) {
dialogRef.current.style.gridTemplateRows = "1fr";
} else {
dialogRef.current.style.gridTemplateRows = "0.6fr";
}
}, [show]);
useEffect(() => {
setFocusedButton(-1);
}, [show]);
useDisableBodyScroll(show);
return (
<AnimatePresence>
{show && (
<div className="w-full h-full top-0 left-0 absolute flex justify-center">
<motion.div
className="fixed top-0 left-0 w-full h-full z-40 bg-black/20 pointer-none"
aria-hidden="true"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.35 }}
/>
<motion.div
className={`fixed min-w-[17.5rem] sm:max-w-[35rem] h-auto z-50 bg-surface-container-high
shadow-2xl shadow-shadow/15 rounded-[1.75rem] p-6 dark:bg-dark-surface-container-high mx-2
origin-top ${className} overflow-hidden grid ${isSupported && "grid-rows-[0fr]"}`}
initial={{
opacity: 0,
transform: "translateY(-24px)",
gridTemplateRows: isSupported ? undefined : "0fr"
}}
animate={{
opacity: 1,
transform: "translateY(0px)",
gridTemplateRows: isSupported ? undefined : "1fr"
}}
exit={{
opacity: 0,
transform: "translateY(-24px)",
gridTemplateRows: isSupported ? undefined : "0fr"
}}
transition={{ ease: [0.05, 0.7, 0.1, 1.0], duration: 0.35 }}
aria-modal="true"
ref={dialogRef}
>
<div className="min-h-0">
<motion.div
className="origin-top"
initial={{ opacity: 0, transform: "translateY(5px)" }}
animate={{ opacity: 1, transform: "translateY(0px)" }}
exit={{ opacity: 0, transform: "translateY(5px)" }}
transition={{
ease: [0.05, 0.7, 0.1, 1.0],
duration: 0.35
}}
ref={contentRef}
>
{children}
</motion.div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
};

View File

@ -1,62 +0,0 @@
import React, { useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
interface DrawerProps {
show?: boolean;
onClose: () => void;
children: React.ReactNode;
}
export const NavigationDrawer = ({ show = false, onClose, children }: DrawerProps) => {
const scrimRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (show && scrimRef.current && event.target === scrimRef.current) {
onClose();
}
};
window.addEventListener("click", handleOutsideClick);
return () => {
window.removeEventListener("click", handleOutsideClick);
};
}, [show, onClose]);
return (
<AnimatePresence>
{show && (
<>
{/* Scrim - Fade in/out */}
<motion.div
ref={scrimRef}
className="fixed top-0 left-0 w-full h-full z-40 bg-black/10"
aria-hidden="true"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
onClick={onClose}
/>
{/* Drawer - Slide from left */}
<motion.div
className="fixed top-0 left-0 h-full bg-surface-container-low dark:bg-dark-surface-container-low
z-50 rounded-r-2xl"
style={{ width: "min(22.5rem, 70vw)" }}
initial={{ x: -500, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -500, opacity: 0 }}
transition={{ duration: 0.25, ease: ["easeOut", "easeOut"] }}
role="dialog"
aria-modal="true"
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
};
export default NavigationDrawer;

View File

@ -1,92 +0,0 @@
import React, { useState, useRef, useCallback } from "react";
import { SearchIcon } from "@/components/icons/SearchIcon";
import { CloseIcon } from "@/components/icons/CloseIcon";
interface SearchBoxProps {
close?: () => void;
}
export const SearchBox: React.FC<SearchBoxProps> = ({ close = () => {} }) => {
const [inputValue, setInputValue] = useState("");
const inputElement = useRef<HTMLInputElement>(null);
const search = useCallback((query: string) => {
if (query.trim()) {
window.location.href = `/song/${query.trim()}/info`;
}
}, []);
const handleInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value);
}, []);
const handleKeyDown = useCallback(
(event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === "Enter") {
event.preventDefault();
search(inputValue);
}
},
[inputValue, search]
);
const handleClear = useCallback(() => {
setInputValue("");
close();
}, [close]);
return (
<div
className="absolute md:relative left-0 h-full mr-0 inline-flex items-center w-full px-4
md:px-0 md:w-full xl:max-w-[50rem] md:mx-4"
>
<div
className="w-full h-10 lg:h-12 px-4 rounded-full bg-surface-container-high
dark:bg-dark-surface-container-high backdrop-blur-lg flex justify-between md:px-5"
>
<button className="w-6" onClick={() => search(inputValue)}>
<SearchIcon
className="h-full inline-flex items-center text-[1.5rem]
text-on-surface-variant dark:text-dark-on-surface-variant"
/>
</button>
<div className="md:hidden flex-grow px-4 top-0 h-full">
<input
ref={inputElement}
value={inputValue}
onChange={handleInputChange}
type="search"
placeholder="搜索"
autoComplete="off"
autoCapitalize="none"
autoCorrect="off"
className="bg-transparent h-full w-full focus:outline-none"
onKeyDown={handleKeyDown}
autoFocus={true}
/>
</div>
<div className="hidden md:block flex-grow px-4 top-0 h-full">
<input
ref={inputElement}
value={inputValue}
onChange={handleInputChange}
type="search"
placeholder="搜索"
autoComplete="off"
autoCapitalize="none"
autoCorrect="off"
className="bg-transparent h-full w-full focus:outline-none"
onKeyDown={handleKeyDown}
/>
</div>
<button
className={`w-6 duration-100 ${inputValue ? "md:opacity-100" : "md:opacity-0"}`}
onClick={handleClear}
>
<CloseIcon className="h-full w-6 inline-flex items-center text-[1.5rem] text-on-surface-variant dark:text-dark-on-surface-variant" />
</button>
</div>
</div>
);
};

Some files were not shown because too many files have changed in this diff Show More