Compare commits
3 Commits
b40d24721c
...
5ac952ec13
Author | SHA1 | Date | |
---|---|---|---|
5ac952ec13 | |||
2cf5923b28 | |||
75973c72ee |
14
packages/backend/middleware/cors.ts
Normal file
14
packages/backend/middleware/cors.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { cors } from "hono/cors";
|
||||
import { Context, Next } from "hono";
|
||||
|
||||
export const corsMiddleware = async (c: Context, next: Next) => {
|
||||
if (c.req.path.startsWith("/user") || c.req.path.startsWith("/login")) {
|
||||
const corsMiddlewareHandler = cors({
|
||||
origin: c.req.header("Origin"),
|
||||
credentials: true
|
||||
});
|
||||
return corsMiddlewareHandler(c, next);
|
||||
}
|
||||
const corsMiddlewareHandler = cors();
|
||||
return corsMiddlewareHandler(c, next);
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
import { Hono } from "hono";
|
||||
import { Variables } from "hono/types";
|
||||
import { bodyLimitForPing } from "./bodyLimits.ts";
|
||||
import { pingHandler } from "routes/ping";
|
||||
import { registerRateLimiter } from "./rateLimiters.ts";
|
||||
import { preetifyResponse } from "./preetifyResponse.ts";
|
||||
import { logger } from "./logger.ts";
|
||||
import { timing } from "hono/timing";
|
||||
import { contentType } from "./contentType.ts";
|
||||
import { captchaMiddleware } from "./captcha.ts";
|
||||
import { cors } from "hono/cors";
|
||||
|
||||
export function configureMiddleWares(app: Hono<{ Variables: Variables }>) {
|
||||
app.use("*", async (c, next) => {
|
||||
if (c.req.path === "/user") {
|
||||
const corsMiddlewareHandler = cors({
|
||||
origin: c.req.header("Origin"),
|
||||
credentials: true
|
||||
});
|
||||
return corsMiddlewareHandler(c, next);
|
||||
}
|
||||
const corsMiddlewareHandler = cors();
|
||||
return corsMiddlewareHandler(c, next);
|
||||
});
|
||||
|
||||
app.use("*", contentType);
|
||||
app.use(timing());
|
||||
app.use("*", preetifyResponse);
|
||||
app.use("*", logger({}));
|
||||
|
||||
app.post("/user", registerRateLimiter);
|
||||
app.post("/user", captchaMiddleware);
|
||||
app.all("/ping", bodyLimitForPing, ...pingHandler);
|
||||
}
|
105
packages/backend/routes/login/session/POST.ts
Normal file
105
packages/backend/routes/login/session/POST.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings, BlankEnv } from "hono/types";
|
||||
import { ErrorResponse, LoginResponse } from "src/schema";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { sqlCred } from "@core/db/dbNew";
|
||||
import { object, string, ValidationError } from "yup";
|
||||
import { setCookie } from "hono/cookie";
|
||||
import Argon2id from "@rabbit-company/argon2id";
|
||||
import { createLoginSession } from "routes/user/POST";
|
||||
import { UserType } from "@core/db/schema";
|
||||
|
||||
const LoginBodySchema = object({
|
||||
username: string().trim().required("Username is required").max(50, "Username cannot exceed 50 characters"),
|
||||
password: string().required("Password is required")
|
||||
});
|
||||
|
||||
export const loginHandler = createHandlers(
|
||||
async (c: Context<BlankEnv & { Bindings: Bindings }, "/user/session/:id">) => {
|
||||
try {
|
||||
const body = await LoginBodySchema.validate(await c.req.json());
|
||||
const { username, password: submittedPassword } = body;
|
||||
|
||||
const result = await sqlCred<UserType[]>`
|
||||
SELECT *
|
||||
FROM users
|
||||
WHERE username = ${username}
|
||||
`;
|
||||
|
||||
if (result.length === 0) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: `User does not exist.`,
|
||||
errors: [`User ${username} does not exist.`],
|
||||
code: "ENTITY_NOT_FOUND"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
}
|
||||
|
||||
const storedPassword = result[0].password;
|
||||
const uid = result[0].id;
|
||||
const nickname = result[0].nickname;
|
||||
const role = result[0].role;
|
||||
|
||||
const passwordAreSame = await Argon2id.verify(storedPassword, submittedPassword);
|
||||
|
||||
if (!passwordAreSame) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Incorrect password.",
|
||||
errors: [],
|
||||
i18n: {
|
||||
key: "backend.error.incorrect_password"
|
||||
},
|
||||
code: "INVALID_CREDENTIALS"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 401);
|
||||
}
|
||||
|
||||
const sessionID = await createLoginSession(uid, c);
|
||||
|
||||
const response: LoginResponse = {
|
||||
uid: uid,
|
||||
username: username,
|
||||
nickname: nickname,
|
||||
role: role,
|
||||
token: sessionID
|
||||
};
|
||||
|
||||
const A_YEAR = 365 * 86400;
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
setCookie(c, "session_id", sessionID, {
|
||||
path: "/",
|
||||
maxAge: A_YEAR,
|
||||
domain: process.env.DOMAIN,
|
||||
secure: isDev ? true : true,
|
||||
sameSite: isDev ? "None" : "Lax",
|
||||
httpOnly: true
|
||||
});
|
||||
|
||||
return c.json<LoginResponse>(response, 200);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid registration data.",
|
||||
errors: e.errors,
|
||||
code: "INVALID_PAYLOAD"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid JSON payload.",
|
||||
errors: [e.message],
|
||||
code: "INVALID_FORMAT"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Unknown error.",
|
||||
errors: [(e as Error).message],
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
75
packages/backend/routes/session/[id]/DELETE.ts
Normal file
75
packages/backend/routes/session/[id]/DELETE.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { Context } from "hono";
|
||||
import { Bindings, BlankEnv } from "hono/types";
|
||||
import { ErrorResponse } from "src/schema";
|
||||
import { createHandlers } from "src/utils.ts";
|
||||
import { sqlCred } from "@core/db/dbNew";
|
||||
import { object, string, ValidationError } from "yup";
|
||||
import { setCookie } from "hono/cookie";
|
||||
|
||||
const loginSessionExists = async (sessionID: string) => {
|
||||
const result = await sqlCred`
|
||||
SELECT 1
|
||||
FROM login_sessions
|
||||
WHERE id = ${sessionID}
|
||||
`;
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
export const logoutHandler = createHandlers(async (c: Context<BlankEnv & { Bindings: Bindings }, "/session/:id">) => {
|
||||
try {
|
||||
const session_id = c.req.param("id");
|
||||
|
||||
const exists = loginSessionExists(session_id);
|
||||
|
||||
if (!exists) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Cannot found given session_id.",
|
||||
errors: [`Session ${session_id} not found`],
|
||||
code: "ENTITY_NOT_FOUND"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 404);
|
||||
}
|
||||
|
||||
await sqlCred`
|
||||
UPDATE login_sessions
|
||||
SET deactivated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ${session_id}
|
||||
`;
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
setCookie(c, "session_id", "", {
|
||||
path: "/",
|
||||
maxAge: 0,
|
||||
domain: process.env.DOMAIN,
|
||||
secure: isDev ? true : true,
|
||||
sameSite: isDev ? "None" : "Lax",
|
||||
httpOnly: true
|
||||
});
|
||||
|
||||
return c.body(null, 204);
|
||||
} catch (e) {
|
||||
if (e instanceof ValidationError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid registration data.",
|
||||
errors: e.errors,
|
||||
code: "INVALID_PAYLOAD"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else if (e instanceof SyntaxError) {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Invalid JSON payload.",
|
||||
errors: [e.message],
|
||||
code: "INVALID_FORMAT"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 400);
|
||||
} else {
|
||||
const response: ErrorResponse<string> = {
|
||||
message: "Unknown error.",
|
||||
errors: [(e as Error).message],
|
||||
code: "UNKNOWN_ERROR"
|
||||
};
|
||||
return c.json<ErrorResponse<string>>(response, 500);
|
||||
}
|
||||
}
|
||||
});
|
1
packages/backend/routes/session/index.ts
Normal file
1
packages/backend/routes/session/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./[id]/DELETE";
|
@ -26,13 +26,13 @@ export const userExists = async (username: string) => {
|
||||
return result.length > 0;
|
||||
};
|
||||
|
||||
const createLoginSession = async (uid: number, ua?: string, ip?: string): Promise<string> => {
|
||||
const ip_address = ip || null;
|
||||
const user_agent = ua || null;
|
||||
export const createLoginSession = async (uid: number, c: Context): Promise<string> => {
|
||||
const ipAddress = getUserIP(c) || null;
|
||||
const userAgent = c.req.header("User-Agent") || null;
|
||||
const id = generateRandomId(24);
|
||||
await sqlCred`
|
||||
INSERT INTO login_sessions (id, uid, expire_at, ip_address, user_agent)
|
||||
VALUES (${id}, ${uid}, CURRENT_TIMESTAMP + INTERVAL '1 year', ${ip_address}, ${user_agent})
|
||||
VALUES (${id}, ${uid}, CURRENT_TIMESTAMP + INTERVAL '1 year', ${ipAddress}, ${userAgent})
|
||||
`;
|
||||
return id;
|
||||
};
|
||||
@ -93,7 +93,7 @@ export const registerHandler = createHandlers(async (c: ContextType) => {
|
||||
return c.json<ErrorResponse<string>>(response, 500);
|
||||
}
|
||||
|
||||
const sessionID = await createLoginSession(uid, c.req.header("User-Agent"), getUserIP(c));
|
||||
const sessionID = await createLoginSession(uid, c);
|
||||
|
||||
const response: SignUpResponse = {
|
||||
username: username,
|
||||
|
@ -2,7 +2,7 @@ import { Hono } from "hono";
|
||||
import type { TimingVariables } from "hono/timing";
|
||||
import { startServer } from "./startServer.ts";
|
||||
import { configureRoutes } from "./routing.ts";
|
||||
import { configureMiddleWares } from "middleware";
|
||||
import { configureMiddleWares } from "./middleware.ts";
|
||||
import { notFoundRoute } from "routes/404.ts";
|
||||
|
||||
type Variables = TimingVariables;
|
||||
|
24
packages/backend/src/middleware.ts
Normal file
24
packages/backend/src/middleware.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Hono } from "hono";
|
||||
import { timing } from "hono/timing";
|
||||
import { Variables } from "hono/types";
|
||||
import { pingHandler } from "routes/ping";
|
||||
import { logger } from "middleware/logger.ts";
|
||||
import { corsMiddleware } from "@/middleware/cors";
|
||||
import { contentType } from "middleware/contentType.ts";
|
||||
import { captchaMiddleware } from "middleware/captcha.ts";
|
||||
import { bodyLimitForPing } from "middleware/bodyLimits.ts";
|
||||
import { registerRateLimiter } from "middleware/rateLimiters.ts";
|
||||
import { preetifyResponse } from "middleware/preetifyResponse.ts";
|
||||
|
||||
export function configureMiddleWares(app: Hono<{ Variables: Variables }>) {
|
||||
app.use("*", corsMiddleware);
|
||||
|
||||
app.use("*", contentType);
|
||||
app.use(timing());
|
||||
app.use("*", preetifyResponse);
|
||||
app.use("*", logger({}));
|
||||
|
||||
app.post("/user", registerRateLimiter);
|
||||
app.post("/user", captchaMiddleware);
|
||||
app.all("/ping", bodyLimitForPing, ...pingHandler);
|
||||
}
|
@ -7,6 +7,8 @@ import { Variables } from "hono/types";
|
||||
import { createCaptchaSessionHandler, verifyChallengeHandler } from "routes/captcha";
|
||||
import { getCaptchaDifficultyHandler } from "routes/captcha/difficulty/GET.ts";
|
||||
import { getVideosHanlder } from "@/routes/videos";
|
||||
import { loginHandler } from "@/routes/login/session/POST";
|
||||
import { logoutHandler } from "@/routes/session";
|
||||
|
||||
export function configureRoutes(app: Hono<{ Variables: Variables }>) {
|
||||
app.get("/", ...rootHandler);
|
||||
@ -17,6 +19,10 @@ export function configureRoutes(app: Hono<{ Variables: Variables }>) {
|
||||
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
|
||||
app.get("/video/:id/info", ...videoInfoHandler);
|
||||
|
||||
app.post("/login/session", ...loginHandler);
|
||||
|
||||
app.delete("/session/:id", ...logoutHandler);
|
||||
|
||||
app.post("/user", ...registerHandler);
|
||||
app.get("/user/session/:id", ...getUserByLoginSessionHandler);
|
||||
|
||||
|
8
packages/backend/src/schema.d.ts
vendored
8
packages/backend/src/schema.d.ts
vendored
@ -38,6 +38,14 @@ interface CaptchaSessionRawResponse {
|
||||
t: number;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
uid: number;
|
||||
username: string;
|
||||
nickname: string | null;
|
||||
role: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface SignUpResponse {
|
||||
username: string;
|
||||
token: string;
|
||||
|
4
packages/core/net/bilibili.d.ts
vendored
4
packages/core/net/bilibili.d.ts
vendored
@ -38,6 +38,10 @@ interface VideoInfoData {
|
||||
ctime: number;
|
||||
desc: string;
|
||||
desc_v2: string;
|
||||
tname: string;
|
||||
tid: number;
|
||||
tid_v2: number;
|
||||
tname_v2: string;
|
||||
state: number;
|
||||
duration: number;
|
||||
owner: {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cvsa/core",
|
||||
"private": false,
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.7",
|
||||
"scripts": {
|
||||
"test": "bun --env-file=.env.test run vitest",
|
||||
"build": "bun build ./index.ts --target node --outdir ./dist"
|
||||
|
1
packages/core/types.d.ts
vendored
1
packages/core/types.d.ts
vendored
@ -1,2 +1,3 @@
|
||||
export * from "./db/schema";
|
||||
export * from "./index";
|
||||
export * from "./net/bilibili";
|
||||
|
3
packages/next/app/[locale]/LICENSE/content.css
Normal file
3
packages/next/app/[locale]/LICENSE/content.css
Normal file
@ -0,0 +1,3 @@
|
||||
p {
|
||||
word-break: break-all;
|
||||
}
|
17
packages/next/app/[locale]/LICENSE/page.tsx
Normal file
17
packages/next/app/[locale]/LICENSE/page.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -2,11 +2,11 @@ import type { Metadata } from "next";
|
||||
import "./global.css";
|
||||
import React from "react";
|
||||
import { routing } from "@/i18n/routing";
|
||||
import { NextIntlClientProvider, hasLocale } from "next-intl";
|
||||
import { hasLocale } from "next-intl";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "中V档案馆"
|
||||
title: "中 V 档案馆"
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
@ -20,19 +20,5 @@ export default async function RootLayout({
|
||||
if (!hasLocale(routing.locales, locale)) {
|
||||
notFound();
|
||||
}
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>中V档案馆</title>
|
||||
</head>
|
||||
<body className="min-h-screen flex flex-col">
|
||||
<NextIntlClientProvider>
|
||||
{children}
|
||||
<div id="portal-root"></div>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
125
packages/next/app/[locale]/login/LoginForm.tsx
Normal file
125
packages/next/app/[locale]/login/LoginForm.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
"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;
|
42
packages/next/app/[locale]/login/page.tsx
Normal file
42
packages/next/app/[locale]/login/page.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
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>
|
||||
);
|
||||
}
|
148
packages/next/app/[locale]/login/request.tsx
Normal file
148
packages/next/app/[locale]/login/request.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
60
packages/next/app/[locale]/logout/route.ts
Normal file
60
packages/next/app/[locale]/logout/route.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import { Header } from "@/components/shell/Header";
|
||||
import { getCurrentUser } from "@/lib/userAuth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function ProfilePage() {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
if (!user) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header user={user} />
|
||||
</>
|
||||
);
|
||||
}
|
@ -11,7 +11,7 @@ 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 "./ErrorDialog";
|
||||
import { ErrorDialog } from "@/components/utils/ErrorDialog";
|
||||
import { ApiRequestError } from "@/lib/net";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@ -56,7 +56,7 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
|
||||
if (typeof item === "string") {
|
||||
return item;
|
||||
}
|
||||
return t(`yup_errors.${item.key}`, { ...item.values, field: path ? t(path) : "" });
|
||||
return t(`${item.key}`, { ...item.values, field: path ? t(path) : "" });
|
||||
};
|
||||
|
||||
const register = async () => {
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 (
|
||||
@ -10,10 +11,10 @@ export default function SignupPage() {
|
||||
md:bg-surface-container md:dark:bg-dark-surface-container"
|
||||
>
|
||||
<p className="mb-2">
|
||||
<a href="/">
|
||||
<Link href="/">
|
||||
<LeftArrow className="inline -translate-y-0.5 scale-90 mr-1" aria-hidden="true" />
|
||||
首页
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
<h1 className="text-5xl leading-[4rem] font-extralight">欢迎</h1>
|
||||
<p className="mt-2 md:mt-3">
|
||||
@ -28,10 +29,10 @@ export default function SignupPage() {
|
||||
</p>
|
||||
<p className="mt-4 mb-7">
|
||||
已有账户?
|
||||
<a href="/login">
|
||||
<Link href="/login">
|
||||
<span>登录</span>
|
||||
<RightArrow className="text-xs inline -translate-y-0.5 ml-1" aria-hidden="true" />
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
<SignUpForm backendURL={process.env.NEXT_PUBLIC_BACKEND_URL ?? ""} />
|
||||
</div>
|
||||
|
@ -3,8 +3,19 @@ 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 "./ErrorDialog";
|
||||
import { string, object, ValidationError } from "yup";
|
||||
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;
|
||||
|
@ -6,6 +6,7 @@ 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";
|
||||
|
||||
const MetadataRow = ({ title, desc }: { title: string; desc: string | number | undefined | null }) => {
|
||||
if (!desc) return <></>;
|
||||
@ -19,6 +20,21 @@ const MetadataRow = ({ title, desc }: { title: string; desc: string | number | u
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Header } from "@/components/shell/Header";
|
||||
import { getCurrentUser } from "@/lib/userAuth";
|
||||
import HeaderServer from "@/components/shell/HeaderServer";
|
||||
import React from "react";
|
||||
|
||||
export default async function RootLayout({
|
||||
@ -7,10 +6,9 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const user = await getCurrentUser();
|
||||
return (
|
||||
<>
|
||||
<Header user={user} />
|
||||
<HeaderServer />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { Header } from "@/components/shell/Header";
|
||||
import { getCurrentUser } from "@/lib/userAuth";
|
||||
import { VDFtestCard } from "./TestCard";
|
||||
import HeaderServer from "@/components/shell/HeaderServer";
|
||||
|
||||
export default async function VdfBenchmarkPage() {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header user={user} />
|
||||
<HeaderServer />
|
||||
<div className="md:w-2/3 lg:w-1/2 xl:w-[37%] md:mx-auto mx-6 mb-12">
|
||||
<VDFtestCard />
|
||||
<div>
|
||||
|
@ -0,0 +1,46 @@
|
||||
"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>
|
||||
</>
|
||||
);
|
||||
};
|
65
packages/next/app/[locale]/user/[uid]/profile/page.tsx
Normal file
65
packages/next/app/[locale]/user/[uid]/profile/page.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
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";
|
||||
|
||||
const uidSchema = numeric().integer().min(0);
|
||||
|
||||
interface SignupTimeProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
const SignupTime: React.FC<SignupTimeProps> = ({ user }: SignupTimeProps) => {
|
||||
return (
|
||||
<p className="mt-4">
|
||||
于
|
||||
{format(new Date(user.createdAt), "yyyy-MM-dd HH:mm:ss", {
|
||||
locale: zhCN
|
||||
})}
|
||||
注册。
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -2,6 +2,8 @@ import { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { format } from "date-fns";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Metadata } from "next";
|
||||
import type { VideoInfoData } from "@cvsa/core";
|
||||
|
||||
const StatRow = ({ title, description }: { title: string; description?: number }) => {
|
||||
return (
|
||||
@ -12,6 +14,21 @@ const StatRow = ({ title, description }: { title: string; description?: number }
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@ -21,7 +38,7 @@ const VideoInfo = async ({ id }: { id: string }) => {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const data: VideoInfoData = await res.json();
|
||||
|
||||
return (
|
||||
<div className="w-full lg:max-w-4xl lg:mx-auto lg:p-6 px-4">
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Header } from "@/components/shell/Header";
|
||||
import { getCurrentUser } from "@/lib/userAuth";
|
||||
import HeaderServer from "@/components/shell/HeaderServer";
|
||||
import React from "react";
|
||||
|
||||
export default async function RootLayout({
|
||||
@ -7,10 +6,9 @@ export default async function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const user = await getCurrentUser();
|
||||
return (
|
||||
<>
|
||||
<Header user={user} />
|
||||
<HeaderServer />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
@ -4,7 +4,7 @@ import React from "react";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "中V档案馆"
|
||||
title: "中 V 档案馆"
|
||||
};
|
||||
|
||||
export default async function RootLayout({
|
||||
@ -17,7 +17,6 @@ export default async function RootLayout({
|
||||
<head>
|
||||
<meta charSet="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>中V档案馆</title>
|
||||
</head>
|
||||
<body className="min-h-screen flex flex-col">
|
||||
<NextIntlClientProvider>
|
||||
|
@ -5,7 +5,7 @@
|
||||
"name": "next",
|
||||
"dependencies": {
|
||||
"@cvsa/backend": "^0.5.3",
|
||||
"@cvsa/core": "^0.0.5",
|
||||
"@cvsa/core": "^0.0.7",
|
||||
"@mdx-js/loader": "^3.1.0",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@next/mdx": "^15.3.3",
|
||||
@ -15,13 +15,16 @@
|
||||
"framer-motion": "^12.15.0",
|
||||
"fumadocs-mdx": "^11.6.6",
|
||||
"i18next": "^25.2.1",
|
||||
"jotai": "^2.12.5",
|
||||
"next": "^15.3.3",
|
||||
"next-intl": "^4.1.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"swr": "^2.3.3",
|
||||
"ua-parser-js": "^2.0.3",
|
||||
"yup": "^1.6.1",
|
||||
"yup-numeric": "^0.5.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
@ -44,7 +47,7 @@
|
||||
|
||||
"@cvsa/backend": ["@cvsa/backend@0.5.3", "", { "dependencies": { "@rabbit-company/argon2id": "^2.1.0", "hono": "^4.7.8", "hono-rate-limiter": "^0.4.2", "ioredis": "^5.6.1", "limiter": "^3.0.0", "postgres": "^3.4.5", "rate-limit-redis": "^4.2.0", "yup": "^1.6.1", "zod": "^3.24.3" } }, "sha512-RzGjarU2TOzD6/d6qikE4xd/ZqNQl3jOYtgfJg5kbWFuiXnOgEC9QBTi+adzjmaWFrcpuYck6ooWpg4eT3s43g=="],
|
||||
|
||||
"@cvsa/core": ["@cvsa/core@0.0.5", "", { "dependencies": { "@koshnic/ratelimit": "^1.0.3", "chalk": "^5.4.1", "ioredis": "^5.6.1", "logform": "^2.7.0", "postgres": "^3.4.5", "winston": "^3.17.0" } }, "sha512-YmsLDF6+hkqPm68BLgpbpB7RTlwKAjHu08TlU5+PdtNfGwYVcJx4fy+jnGEo8tCv68CvBYqoHXRN5Cr4OYo5oQ=="],
|
||||
"@cvsa/core": ["@cvsa/core@0.0.7", "", { "dependencies": { "@koshnic/ratelimit": "^1.0.3", "chalk": "^5.4.1", "ioredis": "^5.6.1", "logform": "^2.7.0", "postgres": "^3.4.5", "winston": "^3.17.0" } }, "sha512-j2Ksg+ZquHqKPew1JZxw0Q9yckFnzdd8y+DnmVT+OW18j+pKcduB9j0qqBywQGHxGuDYVOGLiPlf+IBXfqQWTg=="],
|
||||
|
||||
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
|
||||
|
||||
@ -162,6 +165,8 @@
|
||||
|
||||
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
||||
|
||||
"@jridgewell/source-map": ["@jridgewell/source-map@0.3.6", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" } }, "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||
@ -258,12 +263,18 @@
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="],
|
||||
|
||||
"@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
|
||||
|
||||
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
|
||||
|
||||
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
||||
|
||||
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
|
||||
@ -286,12 +297,52 @@
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="],
|
||||
|
||||
"@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="],
|
||||
|
||||
"@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="],
|
||||
|
||||
"@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="],
|
||||
|
||||
"@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="],
|
||||
|
||||
"@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="],
|
||||
|
||||
"@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="],
|
||||
|
||||
"@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="],
|
||||
|
||||
"@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="],
|
||||
|
||||
"@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="],
|
||||
|
||||
"@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="],
|
||||
|
||||
"@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="],
|
||||
|
||||
"@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="],
|
||||
|
||||
"@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="],
|
||||
|
||||
"@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="],
|
||||
|
||||
"@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="],
|
||||
|
||||
"@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||
|
||||
"ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="],
|
||||
|
||||
"ansi-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
@ -316,6 +367,10 @@
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.0", "", {}, "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
|
||||
@ -326,6 +381,10 @@
|
||||
|
||||
"browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="],
|
||||
|
||||
"browserslist": ["browserslist@4.25.0", "", { "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
@ -358,6 +417,8 @@
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||
|
||||
"cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="],
|
||||
@ -380,6 +441,8 @@
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
|
||||
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
||||
@ -426,8 +489,12 @@
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.165", "", {}, "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="],
|
||||
|
||||
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
@ -438,6 +505,8 @@
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
|
||||
@ -454,8 +523,14 @@
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
"estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="],
|
||||
|
||||
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
|
||||
|
||||
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
|
||||
@ -474,6 +549,8 @@
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="],
|
||||
|
||||
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="],
|
||||
@ -482,6 +559,12 @@
|
||||
|
||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
|
||||
|
||||
"fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||
|
||||
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||
@ -532,6 +615,8 @@
|
||||
|
||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
@ -616,10 +701,20 @@
|
||||
|
||||
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
||||
|
||||
"jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
|
||||
"jotai": ["jotai@2.12.5", "", { "peerDependencies": { "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@types/react", "react"] }, "sha512-G8m32HW3lSmcz/4mbqx0hgJIQ0ekndKWiYP7kWVKi0p6saLXdSoye+FZiOFyonnd7Q482LCzm8sMDl7Ar1NWDw=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||
|
||||
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||
@ -648,6 +743,10 @@
|
||||
|
||||
"limiter": ["limiter@3.0.0", "", {}, "sha512-hev7DuXojsTFl2YwyzUJMDnZ/qBDd3yZQLSH3aD4tdL1cqfc3TMnoecEJtWFaQFdErZsKoFMBTxF/FBSkgDbEg=="],
|
||||
|
||||
"loader-runner": ["loader-runner@4.3.0", "", {}, "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg=="],
|
||||
|
||||
"loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||
@ -708,6 +807,8 @@
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
|
||||
|
||||
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
|
||||
@ -802,12 +903,16 @@
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
|
||||
|
||||
"next": ["next@15.3.3", "", { "dependencies": { "@next/env": "15.3.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.3", "@next/swc-darwin-x64": "15.3.3", "@next/swc-linux-arm64-gnu": "15.3.3", "@next/swc-linux-arm64-musl": "15.3.3", "@next/swc-linux-x64-gnu": "15.3.3", "@next/swc-linux-x64-musl": "15.3.3", "@next/swc-win32-arm64-msvc": "15.3.3", "@next/swc-win32-x64-msvc": "15.3.3", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw=="],
|
||||
|
||||
"next-intl": ["next-intl@4.1.0", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", "negotiator": "^1.0.0", "use-intl": "^4.1.0" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-JNJRjc7sdnfUxhZmGcvzDszZ60tQKrygV/VLsgzXhnJDxQPn1cN2rVpc53adA1SvBJwPK2O6Sc6b4gYSILjCzw=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
@ -852,6 +957,8 @@
|
||||
|
||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||
|
||||
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||
|
||||
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
|
||||
@ -862,6 +969,8 @@
|
||||
|
||||
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
|
||||
|
||||
"raw-loader": ["raw-loader@4.0.2", "", { "dependencies": { "loader-utils": "^2.0.0", "schema-utils": "^3.0.0" }, "peerDependencies": { "webpack": "^4.0.0 || ^5.0.0" } }, "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
@ -910,6 +1019,8 @@
|
||||
|
||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
@ -920,6 +1031,8 @@
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="],
|
||||
|
||||
"scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
|
||||
|
||||
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||
@ -952,6 +1065,8 @@
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||
|
||||
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
@ -992,6 +1107,10 @@
|
||||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"terser": ["terser@5.41.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-H406eLPXpZbAX14+B8psIuvIr8+3c+2hkuYzpMkoE0ij+NdsVATbA78vb8neA/eqrj7rywa2pIkdmWRsXW6wmw=="],
|
||||
|
||||
"terser-webpack-plugin": ["terser-webpack-plugin@5.3.14", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw=="],
|
||||
|
||||
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||
|
||||
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
|
||||
@ -1046,6 +1165,10 @@
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
|
||||
|
||||
"use-intl": ["use-intl@4.1.0", "", { "dependencies": { "@formatjs/fast-memoize": "^2.2.0", "@schummar/icu-type-parser": "1.21.5", "intl-messageformat": "^10.5.14" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" } }, "sha512-mQvDYFvoGn+bm/PWvlQOtluKCknsQ5a9F1Cj0hMfBjMBVTwnOqLPd6srhjvVdEQEQFVyHM1PfyifKqKYb11M9Q=="],
|
||||
@ -1062,8 +1185,14 @@
|
||||
|
||||
"vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="],
|
||||
|
||||
"watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"webpack": ["webpack@5.99.9", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.14.0", "browserslist": "^4.24.0", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg=="],
|
||||
|
||||
"webpack-sources": ["webpack-sources@3.3.2", "", {}, "sha512-ykKKus8lqlgXX/1WjudpIEjqsafjOTcOJqxnAbMLAu/KCsDCJ6GBtvscewvTkrn24HsnvFwrSCbenFrhtcCsAA=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
|
||||
@ -1090,6 +1219,8 @@
|
||||
|
||||
"yup": ["yup@1.6.1", "", { "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", "toposort": "^2.0.2", "type-fest": "^2.19.0" } }, "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA=="],
|
||||
|
||||
"yup-numeric": ["yup-numeric@0.5.0", "", { "dependencies": { "typescript": "^5.4.2" }, "peerDependencies": { "bignumber.js": "^9.1.2", "yup": "^1.3.3" } }, "sha512-IrkLyIY0jLwtomVArrjV1Sv2YHOC715UdRPA7WfAJ0upARXLtmnmzszlPQeEoUxtSb3E9mrF8DoFgiQcRkxOLA=="],
|
||||
|
||||
"zod": ["zod@3.25.46", "", {}, "sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ=="],
|
||||
|
||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||
@ -1110,10 +1241,14 @@
|
||||
|
||||
"accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"colorspace/color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
|
||||
|
||||
"esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
|
||||
|
||||
"express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"fumadocs-core/@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.1", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg=="],
|
||||
@ -1132,12 +1267,20 @@
|
||||
|
||||
"send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils": ["schema-utils@4.3.2", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ=="],
|
||||
|
||||
"type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"webpack/schema-utils": ["schema-utils@4.3.2", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ=="],
|
||||
|
||||
"yargs-unparser/is-plain-obj": ["is-plain-obj@2.1.0", "", {}, "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="],
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"colorspace/color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
|
||||
|
||||
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
@ -1150,10 +1293,22 @@
|
||||
|
||||
"send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"webpack/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"webpack/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="],
|
||||
|
||||
"colorspace/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||
|
||||
"mocha/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"terser-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"webpack/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
}
|
||||
}
|
||||
|
12
packages/next/components/icons/AccountIcon.tsx
Normal file
12
packages/next/components/icons/AccountIcon.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
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>
|
||||
);
|
12
packages/next/components/icons/LoginIcon.tsx
Normal file
12
packages/next/components/icons/LoginIcon.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
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>
|
||||
);
|
@ -8,7 +8,6 @@ import DarkModeImage from "@/components/utils/DarkModeImage";
|
||||
import React, { useState } from "react";
|
||||
import { NavigationDrawer } from "@/components/ui/NavigatinDrawer";
|
||||
import { Portal } from "@/components/utils/Portal";
|
||||
import { RegisterIcon } from "@/components/icons/RegisterIcon";
|
||||
import { SearchBox } from "@/components/ui/SearchBox";
|
||||
import { MenuIcon } from "@/components/icons/MenuIcon";
|
||||
import { SearchIcon } from "@/components/icons/SearchIcon";
|
||||
@ -16,24 +15,26 @@ 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 type { UserResponse } from "@cvsa/backend";
|
||||
import { LoginIcon } from "../icons/LoginIcon";
|
||||
import { AccountIcon } from "../icons/AccountIcon";
|
||||
import { User } from "@/lib/userAuth";
|
||||
|
||||
interface HeaderProps {
|
||||
user: UserResponse | null;
|
||||
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">
|
||||
<a href="/">
|
||||
<Link href="/">
|
||||
<DarkModeImage
|
||||
lightSrc={TitleLight}
|
||||
darkSrc={TitleDark}
|
||||
alt="logo"
|
||||
className="w-[305px] h-24 inline-block max-w-[15rem] lg:max-w-[305px]"
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<SearchBox />
|
||||
@ -43,12 +44,12 @@ export const HeaderDestop = ({ user }: HeaderProps) => {
|
||||
text-xl font-medium items-center w-[15rem] min-w-[8rem] mr-4 lg:mr-0 lg:w-[305px] justify-end"
|
||||
>
|
||||
{user ? (
|
||||
<Link href="/my/profile">{user.nickname || user.username}</Link>
|
||||
<Link href={`/user/${user.uid}/profile`}>{user.nickname || user.username}</Link>
|
||||
) : (
|
||||
<Link href="/signup">注册</Link>
|
||||
<Link href="/login">登录</Link>
|
||||
)}
|
||||
|
||||
<a href="/about">关于</a>
|
||||
<Link href="/about">关于</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -89,14 +90,25 @@ export const HeaderMobile = ({ user }: HeaderProps) => {
|
||||
</TextButton>
|
||||
</Link>
|
||||
|
||||
<Link href="/signup">
|
||||
<TextButton className="w-full h-14 flex px-4 justify-start" size="m">
|
||||
<div className="flex items-center">
|
||||
<RegisterIcon 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>
|
||||
|
7
packages/next/components/shell/HeaderServer.tsx
Normal file
7
packages/next/components/shell/HeaderServer.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { Header } from "@/components/shell/Header";
|
||||
import { getCurrentUser } from "@/lib/userAuth";
|
||||
|
||||
export default async function HeaderServer() {
|
||||
const user = await getCurrentUser();
|
||||
return <Header user={user} />;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import useRipple from "@/components/utils/useRipple";
|
||||
|
||||
interface FilledButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
|
@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import useRipple from "@/components/utils/useRipple";
|
||||
|
||||
interface TextButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@ -5,13 +7,16 @@ interface TextButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
shape?: "round" | "square";
|
||||
children?: React.ReactNode;
|
||||
ripple?: boolean;
|
||||
ref?: React.Ref<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export const TextButton = ({
|
||||
children,
|
||||
size = "s",
|
||||
shape = "round",
|
||||
className,
|
||||
className = "",
|
||||
disabled,
|
||||
ref,
|
||||
ripple = true,
|
||||
...rest
|
||||
}: TextButtonProps) => {
|
||||
@ -29,12 +34,19 @@ export const TextButton = ({
|
||||
<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 hover:bg-primary/10"></div>
|
||||
<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>
|
||||
);
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { TextButton } from "./Buttons/TextButton";
|
||||
import { useEffect } from "react";
|
||||
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(() => {
|
||||
@ -23,10 +28,14 @@ type ButtonElementAttr = React.HTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
type DialogHeadlineProps = OptionalChidrenProps<HeadElementAttr>;
|
||||
type DialogSupportingTextProps = OptionalChidrenProps<DivElementAttr>;
|
||||
type DialogButtonGroupProps = 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;
|
||||
@ -63,46 +72,178 @@ export const DialogSupportingText: React.FC<DialogSupportingTextProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const DialogButton: React.FC<DialogButtonProps> = ({ children, onClick, ...rest }: DialogButtonProps) => {
|
||||
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}>
|
||||
<TextButton onClick={onClick} {...rest} ref={buttonRef}>
|
||||
{children}
|
||||
</TextButton>
|
||||
);
|
||||
};
|
||||
|
||||
export const DialogButtonGroup: React.FC<DialogButtonGroupProps> = ({ children, ...rest }: DialogButtonGroupProps) => {
|
||||
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}>
|
||||
{children}
|
||||
{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 items-center justify-center">
|
||||
<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.3 }}
|
||||
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 ${className}`}
|
||||
initial={{ opacity: 0.5, transform: "scale(1.1)" }}
|
||||
animate={{ opacity: 1, transform: "scale(1)" }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ ease: [0.31, 0.69, 0.3, 1.02], duration: 0.3 }}
|
||||
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}
|
||||
>
|
||||
{children}
|
||||
<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>
|
||||
)}
|
||||
|
@ -58,7 +58,7 @@ export const SearchBox: React.FC<SearchBoxProps> = ({ close = () => {} }) => {
|
||||
type="search"
|
||||
placeholder="搜索"
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
className="bg-transparent h-full w-full focus:outline-none"
|
||||
onKeyDown={handleKeyDown}
|
||||
@ -73,7 +73,7 @@ export const SearchBox: React.FC<SearchBoxProps> = ({ close = () => {} }) => {
|
||||
type="search"
|
||||
placeholder="搜索"
|
||||
autoComplete="off"
|
||||
autoCapitalize="off"
|
||||
autoCapitalize="none"
|
||||
autoCorrect="off"
|
||||
className="bg-transparent h-full w-full focus:outline-none"
|
||||
onKeyDown={handleKeyDown}
|
||||
|
31
packages/next/components/utils/useKeyboardEvents.tsx
Normal file
31
packages/next/components/utils/useKeyboardEvents.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { useEffect, useCallback } from "react";
|
||||
|
||||
export type KeyboardShortcut = {
|
||||
key: string;
|
||||
callback: () => void;
|
||||
preventDefault?: boolean;
|
||||
};
|
||||
|
||||
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]): void {
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
shortcuts.forEach((shortcut) => {
|
||||
if (event.key === shortcut.key) {
|
||||
if (shortcut.preventDefault) {
|
||||
event.preventDefault();
|
||||
}
|
||||
shortcut.callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
[shortcuts]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [handleKeyDown]);
|
||||
}
|
661
packages/next/content/LICENSE.txt
Normal file
661
packages/next/content/LICENSE.txt
Normal file
@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
7694
packages/next/content/THIRD-PARTY-LICENSES.txt
Normal file
7694
packages/next/content/THIRD-PARTY-LICENSES.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,8 +7,16 @@
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"nickname": "昵称",
|
||||
"profile_page": {
|
||||
"role": {
|
||||
"ADMIN": "管理员",
|
||||
"USER": "普通用户",
|
||||
"OWNER": "所有者"
|
||||
}
|
||||
},
|
||||
"backend": {
|
||||
"error": {
|
||||
"incorrect_password": "密码错误。",
|
||||
"captcha_failed": "无法完成安全验证。",
|
||||
"user_exists": "用户名 “{username}” 已被占用。",
|
||||
"user_not_found_after_register": "我们的服务器出现错误:找不到名为'{username}'的用户。请联系我们的<support>支持团队</support>,反馈此问题。",
|
||||
|
@ -1,19 +1,56 @@
|
||||
import { UserType, sqlCred } from "@cvsa/core";
|
||||
import { UserProfile } from "../userAuth";
|
||||
|
||||
export const getUserBySession = async (sessionID: string) => {
|
||||
const users = await sqlCred<UserType[]>`
|
||||
SELECT u.*
|
||||
FROM users u
|
||||
JOIN login_sessions ls ON u.id = ls.uid
|
||||
WHERE ls.id = ${sessionID};
|
||||
SELECT user_id as id, username, nickname, "role", user_created_at as created_at
|
||||
FROM get_user_by_session_func(${sessionID});
|
||||
`;
|
||||
|
||||
if (users.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
return {
|
||||
uid: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
role: user.role
|
||||
role: user.role,
|
||||
createdAt: user.created_at
|
||||
};
|
||||
};
|
||||
|
||||
export const queryUserProfile = async (uid: number, sessionID?: string): Promise<UserProfile | null> => {
|
||||
interface Result extends UserType {
|
||||
logged_in: boolean;
|
||||
}
|
||||
const users = await sqlCred<Result[]>`
|
||||
SELECT
|
||||
u.id, u.username, u.nickname, u."role", u.created_at,
|
||||
CASE
|
||||
WHEN (ls.uid IS NOT NULL AND ls.deactivated_at IS NULL AND ls.expire_at > NOW()) THEN TRUE
|
||||
ELSE FALSE
|
||||
END AS logged_in
|
||||
FROM
|
||||
users u
|
||||
LEFT JOIN
|
||||
login_sessions ls ON ls.uid = u.id AND ls.id = ${sessionID || ""}
|
||||
WHERE
|
||||
u.id = ${uid};
|
||||
`;
|
||||
|
||||
if (users.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
return {
|
||||
uid: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
role: user.role,
|
||||
createdAt: user.created_at,
|
||||
isLoggedIn: user.logged_in
|
||||
};
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import axios, { AxiosRequestConfig, AxiosError, Method } from "axios";
|
||||
import axios, { AxiosRequestConfig, AxiosError, Method, AxiosResponse } from "axios";
|
||||
|
||||
export class ApiRequestError extends Error {
|
||||
public code: number | undefined;
|
||||
@ -21,10 +21,20 @@ const httpMethods = {
|
||||
patch: axios.patch
|
||||
};
|
||||
|
||||
export function fetcher(url: string): Promise<unknown>;
|
||||
export function fetcher<JSON = unknown>(
|
||||
url: string,
|
||||
init?: Omit<AxiosRequestConfig, "method"> & { method?: Exclude<HttpMethod, "DELETE"> }
|
||||
): Promise<JSON>;
|
||||
export function fetcher(
|
||||
url: string,
|
||||
init: Omit<AxiosRequestConfig, "method"> & { method: "DELETE" }
|
||||
): Promise<AxiosResponse>;
|
||||
|
||||
export async function fetcher<JSON = unknown>(
|
||||
url: string,
|
||||
init?: Omit<AxiosRequestConfig, "method"> & { method?: HttpMethod }
|
||||
): Promise<JSON> {
|
||||
): Promise<JSON | AxiosResponse<any, any>> {
|
||||
const { method = "get", data, ...config } = init || {};
|
||||
|
||||
const fullConfig: AxiosRequestConfig = {
|
||||
@ -38,6 +48,9 @@ export async function fetcher<JSON = unknown>(
|
||||
if (["post", "patch", "put"].includes(m)) {
|
||||
const response = await httpMethods[m](url, data, fullConfig);
|
||||
return response.data;
|
||||
} else if (m === "delete") {
|
||||
const response = await axios.delete(url, fullConfig);
|
||||
return response;
|
||||
} else {
|
||||
const response = await httpMethods[m](url, fullConfig);
|
||||
return response.data;
|
||||
|
@ -1,17 +1,43 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { getUserBySession } from "@/lib/db/user";
|
||||
import type { UserResponse } from "@cvsa/backend";
|
||||
import { getUserBySession, queryUserProfile } from "@/lib/db/user";
|
||||
|
||||
export async function getCurrentUser(): Promise<UserResponse | null> {
|
||||
export interface User {
|
||||
uid: number;
|
||||
username: string;
|
||||
nickname: string | null;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface UserProfile extends User {
|
||||
isLoggedIn: boolean;
|
||||
}
|
||||
|
||||
export async function getCurrentUser(): Promise<User | null> {
|
||||
const cookieStore = await cookies();
|
||||
const sessionID = cookieStore.get("session_id");
|
||||
|
||||
if (!sessionID) return null;
|
||||
|
||||
try {
|
||||
const user = await getUserBySession(sessionID.value);
|
||||
|
||||
return user ?? null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUserProfile(uid: number): Promise<UserProfile | null> {
|
||||
const cookieStore = await cookies();
|
||||
const sessionID = cookieStore.get("session_id");
|
||||
|
||||
try {
|
||||
const user = await queryUserProfile(uid, sessionID?.value);
|
||||
|
||||
return user ?? null;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,21 @@ const nextConfig: NextConfig = {
|
||||
experimental: {
|
||||
externalDir: true
|
||||
},
|
||||
turbopack: {
|
||||
rules: {
|
||||
"*.txt": {
|
||||
loaders: ["raw-loader"],
|
||||
as: "*.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
webpack(config: import("webpack").Configuration) {
|
||||
config.module?.rules?.push({
|
||||
test: /\.txt/i,
|
||||
use: "raw-loader"
|
||||
});
|
||||
return config;
|
||||
},
|
||||
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"]
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next",
|
||||
"version": "2.8.0",
|
||||
"version": "2.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 7400",
|
||||
@ -11,7 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@cvsa/backend": "^0.5.3",
|
||||
"@cvsa/core": "^0.0.5",
|
||||
"@cvsa/core": "^0.0.7",
|
||||
"@mdx-js/loader": "^3.1.0",
|
||||
"@mdx-js/react": "^3.1.0",
|
||||
"@next/mdx": "^15.3.3",
|
||||
@ -21,13 +21,16 @@
|
||||
"framer-motion": "^12.15.0",
|
||||
"fumadocs-mdx": "^11.6.6",
|
||||
"i18next": "^25.2.1",
|
||||
"jotai": "^2.12.5",
|
||||
"next": "^15.3.3",
|
||||
"next-intl": "^4.1.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"swr": "^2.3.3",
|
||||
"ua-parser-js": "^2.0.3",
|
||||
"yup": "^1.6.1"
|
||||
"yup": "^1.6.1",
|
||||
"yup-numeric": "^0.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
|
Loading…
Reference in New Issue
Block a user