Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
92c3c8eefe | |||
497ea031d8 | |||
39ca394a56 | |||
0bd1771f35 | |||
328c73c209 | |||
5ac952ec13 | |||
2cf5923b28 | |||
75973c72ee | |||
b40d24721c | |||
0a6ecc6314 | |||
b4a0320e3e | |||
8cf9395354 | |||
1e8d28e194 | |||
c0340677a1 | |||
54a2de0a11 | |||
2c83b79881 | |||
1a20d5afe0 | |||
2b0497c83a | |||
3bc72720d1 | |||
1ff71ab241 | |||
cf7a285f57 | |||
79a37d927a |
17
package.json
17
package.json
@ -1,12 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "cvsa",
|
"name": "cvsa",
|
||||||
"version": "2.13.22",
|
"version": "3.15.34",
|
||||||
"private": true,
|
"private": false,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/frontend",
|
"packages/frontend",
|
||||||
"packages/core",
|
"packages/core",
|
||||||
"packages/backend",
|
"packages/backend",
|
||||||
"packages/crawler"
|
"packages/crawler"
|
||||||
]
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"arg": "^5.0.2",
|
||||||
|
"postgres": "^3.4.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.2.15",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
|
"vitest": "^3.1.2",
|
||||||
|
"vitest-tsconfig-paths": "^3.4.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
packages/backend/db/latestSnapshots.ts
Normal file
13
packages/backend/db/latestSnapshots.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { sql } from "@core/db/dbNew";
|
||||||
|
import type { LatestSnapshotType } from "@core/db/schema.d.ts";
|
||||||
|
|
||||||
|
export async function getVideosInViewsRange(minViews: number, maxViews: number) {
|
||||||
|
return sql<LatestSnapshotType[]>`
|
||||||
|
SELECT *
|
||||||
|
FROM latest_video_snapshot
|
||||||
|
WHERE views >= ${minViews}
|
||||||
|
AND views <= ${maxViews}
|
||||||
|
ORDER BY views DESC
|
||||||
|
LIMIT 5000
|
||||||
|
`;
|
||||||
|
}
|
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 { Context, 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);
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@cvsa/backend",
|
"name": "@cvsa/backend",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "0.5.3",
|
"version": "0.6.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"dev": "NODE_ENV=development bun run --hot src/main.ts",
|
"dev": "NODE_ENV=development bun run --hot src/main.ts",
|
||||||
|
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;
|
return result.length > 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createLoginSession = async (uid: number, ua?: string, ip?: string): Promise<string> => {
|
export const createLoginSession = async (uid: number, c: Context): Promise<string> => {
|
||||||
const ip_address = ip || null;
|
const ipAddress = getUserIP(c) || null;
|
||||||
const user_agent = ua || null;
|
const userAgent = c.req.header("User-Agent") || null;
|
||||||
const id = generateRandomId(24);
|
const id = generateRandomId(24);
|
||||||
await sqlCred`
|
await sqlCred`
|
||||||
INSERT INTO login_sessions (id, uid, expire_at, ip_address, user_agent)
|
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;
|
return id;
|
||||||
};
|
};
|
||||||
@ -93,7 +93,7 @@ export const registerHandler = createHandlers(async (c: ContextType) => {
|
|||||||
return c.json<ErrorResponse<string>>(response, 500);
|
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 = {
|
const response: SignUpResponse = {
|
||||||
username: username,
|
username: username,
|
||||||
|
65
packages/backend/routes/videos/GET.ts
Normal file
65
packages/backend/routes/videos/GET.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import type { Context } from "hono";
|
||||||
|
import { createHandlers } from "src/utils.ts";
|
||||||
|
import type { BlankEnv, BlankInput } from "hono/types";
|
||||||
|
import { number, object, ValidationError } from "yup";
|
||||||
|
import { ErrorResponse } from "src/schema";
|
||||||
|
import { startTime, endTime } from "hono/timing";
|
||||||
|
import { getVideosInViewsRange } from "@/db/latestSnapshots";
|
||||||
|
|
||||||
|
const SnapshotQueryParamsSchema = object({
|
||||||
|
min_views: number().integer().optional().positive(),
|
||||||
|
max_views: number().integer().optional().positive()
|
||||||
|
});
|
||||||
|
|
||||||
|
type ContextType = Context<BlankEnv, "/videos", BlankInput>;
|
||||||
|
|
||||||
|
export const getVideosHanlder = createHandlers(async (c: ContextType) => {
|
||||||
|
startTime(c, "parse", "Parse the request");
|
||||||
|
try {
|
||||||
|
const queryParams = await SnapshotQueryParamsSchema.validate(c.req.query());
|
||||||
|
const { min_views, max_views } = queryParams;
|
||||||
|
|
||||||
|
if (!min_views && !max_views) {
|
||||||
|
const response: ErrorResponse<string> = {
|
||||||
|
code: "INVALID_QUERY_PARAMS",
|
||||||
|
message: "Invalid query parameters",
|
||||||
|
errors: ["Must provide one of these query parameters: min_views, max_views"]
|
||||||
|
};
|
||||||
|
return c.json<ErrorResponse<string>>(response, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
endTime(c, "parse");
|
||||||
|
|
||||||
|
startTime(c, "db", "Query the database");
|
||||||
|
|
||||||
|
const minViews = min_views ? min_views : 0;
|
||||||
|
const maxViews = max_views ? max_views : 2147483647;
|
||||||
|
|
||||||
|
const result = await getVideosInViewsRange(minViews, maxViews);
|
||||||
|
|
||||||
|
endTime(c, "db");
|
||||||
|
|
||||||
|
const rows = result.map((row) => ({
|
||||||
|
...row,
|
||||||
|
aid: Number(row.aid)
|
||||||
|
}));
|
||||||
|
|
||||||
|
return c.json(rows);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
if (e instanceof ValidationError) {
|
||||||
|
const response: ErrorResponse<string> = {
|
||||||
|
code: "INVALID_QUERY_PARAMS",
|
||||||
|
message: "Invalid query parameters",
|
||||||
|
errors: e.errors
|
||||||
|
};
|
||||||
|
return c.json<ErrorResponse<string>>(response, 400);
|
||||||
|
} else {
|
||||||
|
const response: ErrorResponse<unknown> = {
|
||||||
|
code: "UNKNOWN_ERROR",
|
||||||
|
message: "Unhandled error",
|
||||||
|
errors: [e]
|
||||||
|
};
|
||||||
|
return c.json<ErrorResponse<unknown>>(response, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
1
packages/backend/routes/videos/index.ts
Normal file
1
packages/backend/routes/videos/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./GET.ts";
|
@ -2,7 +2,7 @@ import { Hono } from "hono";
|
|||||||
import type { TimingVariables } from "hono/timing";
|
import type { TimingVariables } from "hono/timing";
|
||||||
import { startServer } from "./startServer.ts";
|
import { startServer } from "./startServer.ts";
|
||||||
import { configureRoutes } from "./routing.ts";
|
import { configureRoutes } from "./routing.ts";
|
||||||
import { configureMiddleWares } from "middleware";
|
import { configureMiddleWares } from "./middleware.ts";
|
||||||
import { notFoundRoute } from "routes/404.ts";
|
import { notFoundRoute } from "routes/404.ts";
|
||||||
|
|
||||||
type Variables = TimingVariables;
|
type Variables = TimingVariables;
|
||||||
@ -15,4 +15,4 @@ configureRoutes(app);
|
|||||||
|
|
||||||
await startServer(app);
|
await startServer(app);
|
||||||
|
|
||||||
export const VERSION = "0.5.2";
|
export const VERSION = "0.6.0";
|
||||||
|
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);
|
||||||
|
}
|
@ -6,14 +6,23 @@ import { Hono } from "hono";
|
|||||||
import { Variables } from "hono/types";
|
import { Variables } from "hono/types";
|
||||||
import { createCaptchaSessionHandler, verifyChallengeHandler } from "routes/captcha";
|
import { createCaptchaSessionHandler, verifyChallengeHandler } from "routes/captcha";
|
||||||
import { getCaptchaDifficultyHandler } from "routes/captcha/difficulty/GET.ts";
|
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 }>) {
|
export function configureRoutes(app: Hono<{ Variables: Variables }>) {
|
||||||
app.get("/", ...rootHandler);
|
app.get("/", ...rootHandler);
|
||||||
app.all("/ping", ...pingHandler);
|
app.all("/ping", ...pingHandler);
|
||||||
|
|
||||||
|
app.get("/videos", ...getVideosHanlder);
|
||||||
|
|
||||||
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
|
app.get("/video/:id/snapshots", ...getSnapshotsHanlder);
|
||||||
app.get("/video/:id/info", ...videoInfoHandler);
|
app.get("/video/:id/info", ...videoInfoHandler);
|
||||||
|
|
||||||
|
app.post("/login/session", ...loginHandler);
|
||||||
|
|
||||||
|
app.delete("/session/:id", ...logoutHandler);
|
||||||
|
|
||||||
app.post("/user", ...registerHandler);
|
app.post("/user", ...registerHandler);
|
||||||
app.get("/user/session/:id", ...getUserByLoginSessionHandler);
|
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;
|
t: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
uid: number;
|
||||||
|
username: string;
|
||||||
|
nickname: string | null;
|
||||||
|
role: string;
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SignUpResponse {
|
export interface SignUpResponse {
|
||||||
username: string;
|
username: string;
|
||||||
token: string;
|
token: string;
|
||||||
|
9
packages/core/db/schema.d.ts
vendored
9
packages/core/db/schema.d.ts
vendored
@ -8,7 +8,7 @@ export interface BiliUserType {
|
|||||||
|
|
||||||
export interface VideoSnapshotType {
|
export interface VideoSnapshotType {
|
||||||
id: number;
|
id: number;
|
||||||
created_at: string;
|
created_at: Date;
|
||||||
views: number;
|
views: number;
|
||||||
coins: number;
|
coins: number;
|
||||||
likes: number;
|
likes: number;
|
||||||
@ -35,9 +35,9 @@ export interface SnapshotScheduleType {
|
|||||||
id: number;
|
id: number;
|
||||||
aid: number;
|
aid: number;
|
||||||
type?: string;
|
type?: string;
|
||||||
created_at: string;
|
created_at: Date;
|
||||||
started_at?: string;
|
started_at?: Date;
|
||||||
finished_at?: string;
|
finished_at?: Date;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +48,7 @@ export interface UserType {
|
|||||||
password: string;
|
password: string;
|
||||||
unq_id: string;
|
unq_id: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
created_at: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BiliVideoMetadataType {
|
export interface BiliVideoMetadataType {
|
||||||
|
4
packages/core/net/bilibili.d.ts
vendored
4
packages/core/net/bilibili.d.ts
vendored
@ -38,6 +38,10 @@ interface VideoInfoData {
|
|||||||
ctime: number;
|
ctime: number;
|
||||||
desc: string;
|
desc: string;
|
||||||
desc_v2: string;
|
desc_v2: string;
|
||||||
|
tname: string;
|
||||||
|
tid: number;
|
||||||
|
tid_v2: number;
|
||||||
|
tname_v2: string;
|
||||||
state: number;
|
state: number;
|
||||||
duration: number;
|
duration: number;
|
||||||
owner: {
|
owner: {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@cvsa/core",
|
"name": "@cvsa/core",
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "0.0.5",
|
"version": "0.0.10",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun --env-file=.env.test run vitest",
|
"test": "bun --env-file=.env.test run vitest",
|
||||||
"build": "bun build ./index.ts --target node --outdir ./dist"
|
"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 "./db/schema";
|
||||||
export * from "./index";
|
export * from "./index";
|
||||||
|
export * from "./net/bilibili";
|
||||||
|
@ -30,7 +30,7 @@ export async function insertVideoLabel(sql: Psql, aid: number, label: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getVideoInfoFromAllData(sql: Psql, aid: number) {
|
export async function getVideoInfoFromAllData(sql: Psql, aid: number) {
|
||||||
const rows = await sql<BiliVideoMetadataType[]>`
|
const rows = await sql<AllDataType[]>`
|
||||||
SELECT * FROM bilibili_metadata WHERE aid = ${aid}
|
SELECT * FROM bilibili_metadata WHERE aid = ${aid}
|
||||||
`;
|
`;
|
||||||
const row = rows[0];
|
const row = rows[0];
|
||||||
|
@ -10,19 +10,19 @@ export async function getVideosNearMilestone(sql: Psql) {
|
|||||||
WHERE
|
WHERE
|
||||||
(views >= 50000 AND views < 100000) OR
|
(views >= 50000 AND views < 100000) OR
|
||||||
(views >= 900000 AND views < 1000000) OR
|
(views >= 900000 AND views < 1000000) OR
|
||||||
(views >= 9900000 AND views < 10000000)
|
(views >= CEIL(views::float/1000000::float)*1000000-100000 AND views < CEIL(views::float/1000000::float)*1000000)
|
||||||
UNION
|
UNION
|
||||||
SELECT ls.*
|
SELECT ls.*
|
||||||
FROM latest_video_snapshot ls
|
FROM latest_video_snapshot ls
|
||||||
WHERE
|
WHERE
|
||||||
(views >= 90000 AND views < 100000) OR
|
(views >= 90000 AND views < 100000) OR
|
||||||
(views >= 900000 AND views < 1000000) OR
|
(views >= 900000 AND views < 1000000) OR
|
||||||
(views >= 9900000 AND views < 10000000)
|
(views >= CEIL(views::float/1000000::float)*1000000-100000 AND views < CEIL(views::float/1000000::float)*1000000)
|
||||||
`;
|
`;
|
||||||
return queryResult.map((row) => {
|
return queryResult.map((row) => {
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
aid: Number(row.aid),
|
aid: Number(row.aid)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -40,7 +40,7 @@ export async function getLatestVideoSnapshot(sql: Psql, aid: number): Promise<nu
|
|||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
aid: Number(row.aid),
|
aid: Number(row.aid),
|
||||||
time: new Date(row.time).getTime(),
|
time: new Date(row.time).getTime()
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
}
|
}
|
||||||
|
@ -63,18 +63,6 @@ export async function snapshotScheduleExists(sql: Psql, id: number) {
|
|||||||
return rows.length > 0;
|
return rows.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function videoHasActiveSchedule(sql: Psql, aid: number) {
|
|
||||||
const rows = await sql<{ status: string }[]>`
|
|
||||||
SELECT status
|
|
||||||
FROM snapshot_schedule
|
|
||||||
WHERE aid = ${aid}
|
|
||||||
AND (status = 'pending'
|
|
||||||
OR status = 'processing'
|
|
||||||
)
|
|
||||||
`
|
|
||||||
return rows.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function videoHasActiveScheduleWithType(sql: Psql, aid: number, type: string) {
|
export async function videoHasActiveScheduleWithType(sql: Psql, aid: number, type: string) {
|
||||||
const rows = await sql<{ status: string }[]>`
|
const rows = await sql<{ status: string }[]>`
|
||||||
SELECT status FROM snapshot_schedule
|
SELECT status FROM snapshot_schedule
|
||||||
@ -91,7 +79,7 @@ export async function videoHasProcessingSchedule(sql: Psql, aid: number) {
|
|||||||
FROM snapshot_schedule
|
FROM snapshot_schedule
|
||||||
WHERE aid = ${aid}
|
WHERE aid = ${aid}
|
||||||
AND status = 'processing'
|
AND status = 'processing'
|
||||||
`
|
`;
|
||||||
return rows.length > 0;
|
return rows.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +90,7 @@ export async function bulkGetVideosWithoutProcessingSchedules(sql: Psql, aids: n
|
|||||||
WHERE aid = ANY(${aids})
|
WHERE aid = ANY(${aids})
|
||||||
AND status != 'processing'
|
AND status != 'processing'
|
||||||
GROUP BY aid
|
GROUP BY aid
|
||||||
`
|
`;
|
||||||
return rows.map((row) => Number(row.aid));
|
return rows.map((row) => Number(row.aid));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,7 +182,8 @@ export async function scheduleSnapshot(
|
|||||||
aid: number,
|
aid: number,
|
||||||
type: string,
|
type: string,
|
||||||
targetTime: number,
|
targetTime: number,
|
||||||
force: boolean = false
|
force: boolean = false,
|
||||||
|
adjustTime: boolean = true
|
||||||
) {
|
) {
|
||||||
let adjustedTime = new Date(targetTime);
|
let adjustedTime = new Date(targetTime);
|
||||||
const hashActiveSchedule = await videoHasActiveScheduleWithType(sql, aid, type);
|
const hashActiveSchedule = await videoHasActiveScheduleWithType(sql, aid, type);
|
||||||
@ -216,7 +205,7 @@ export async function scheduleSnapshot(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hashActiveSchedule && !force) return;
|
if (hashActiveSchedule && !force) return;
|
||||||
if (type !== "milestone" && type !== "new") {
|
if (type !== "milestone" && type !== "new" && adjustTime) {
|
||||||
adjustedTime = await adjustSnapshotTime(new Date(targetTime), 2000, redis);
|
adjustedTime = await adjustSnapshotTime(new Date(targetTime), 2000, redis);
|
||||||
}
|
}
|
||||||
logger.log(`Scheduled snapshot for ${aid} at ${adjustedTime.toISOString()}`, "mq", "fn:scheduleSnapshot");
|
logger.log(`Scheduled snapshot for ${aid} at ${adjustedTime.toISOString()}`, "mq", "fn:scheduleSnapshot");
|
||||||
@ -236,10 +225,11 @@ export async function bulkScheduleSnapshot(
|
|||||||
aids: number[],
|
aids: number[],
|
||||||
type: string,
|
type: string,
|
||||||
targetTime: number,
|
targetTime: number,
|
||||||
force: boolean = false
|
force: boolean = false,
|
||||||
|
adjustTime: boolean = true
|
||||||
) {
|
) {
|
||||||
for (const aid of aids) {
|
for (const aid of aids) {
|
||||||
await scheduleSnapshot(sql, aid, type, targetTime, force);
|
await scheduleSnapshot(sql, aid, type, targetTime, force, adjustTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,23 +282,23 @@ export async function adjustSnapshotTime(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getSnapshotsInNextSecond(sql: Psql) {
|
export async function getSnapshotsInNextSecond(sql: Psql) {
|
||||||
const rows = await sql<SnapshotScheduleType[]>`
|
return sql<SnapshotScheduleType[]>`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM snapshot_schedule
|
FROM snapshot_schedule
|
||||||
WHERE started_at <= NOW() + INTERVAL '1 seconds' AND status = 'pending' AND type != 'normal'
|
WHERE started_at <= NOW() + INTERVAL '1 seconds'
|
||||||
ORDER BY
|
AND status = 'pending'
|
||||||
CASE
|
AND type != 'normal'
|
||||||
|
ORDER BY CASE
|
||||||
WHEN type = 'milestone' THEN 0
|
WHEN type = 'milestone' THEN 0
|
||||||
ELSE 1
|
ELSE 1
|
||||||
END,
|
END,
|
||||||
started_at
|
started_at
|
||||||
LIMIT 10;
|
LIMIT 10;
|
||||||
`
|
`;
|
||||||
return rows;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBulkSnapshotsInNextSecond(sql: Psql) {
|
export async function getBulkSnapshotsInNextSecond(sql: Psql) {
|
||||||
const rows = await sql<SnapshotScheduleType[]>`
|
return sql<SnapshotScheduleType[]>`
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM snapshot_schedule
|
FROM snapshot_schedule
|
||||||
WHERE (started_at <= NOW() + INTERVAL '15 seconds')
|
WHERE (started_at <= NOW() + INTERVAL '15 seconds')
|
||||||
@ -320,38 +310,34 @@ export async function getBulkSnapshotsInNextSecond(sql: Psql) {
|
|||||||
END,
|
END,
|
||||||
started_at
|
started_at
|
||||||
LIMIT 1000;
|
LIMIT 1000;
|
||||||
`
|
`;
|
||||||
return rows;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setSnapshotStatus(sql: Psql, id: number, status: string) {
|
export async function setSnapshotStatus(sql: Psql, id: number, status: string) {
|
||||||
return await sql`
|
return sql`
|
||||||
UPDATE snapshot_schedule SET status = ${status} WHERE id = ${id}
|
UPDATE snapshot_schedule
|
||||||
|
SET status = ${status}
|
||||||
|
WHERE id = ${id}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bulkSetSnapshotStatus(sql: Psql, ids: number[], status: string) {
|
export async function bulkSetSnapshotStatus(sql: Psql, ids: number[], status: string) {
|
||||||
return await sql`
|
return sql`
|
||||||
UPDATE snapshot_schedule SET status = ${status} WHERE id = ANY(${ids})
|
UPDATE snapshot_schedule
|
||||||
|
SET status = ${status}
|
||||||
|
WHERE id = ANY (${ids})
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getVideosWithoutActiveSnapshotSchedule(sql: Psql) {
|
export async function getVideosWithoutActiveSnapshotScheduleByType(sql: Psql, type: string) {
|
||||||
const rows = await sql<{ aid: string }[]>`
|
const rows = await sql<{ aid: string }[]>`
|
||||||
SELECT s.aid
|
SELECT s.aid
|
||||||
FROM songs s
|
FROM songs s
|
||||||
LEFT JOIN snapshot_schedule ss ON s.aid = ss.aid AND (ss.status = 'pending' OR ss.status = 'processing')
|
LEFT JOIN snapshot_schedule ss ON
|
||||||
|
s.aid = ss.aid AND
|
||||||
|
(ss.status = 'pending' OR ss.status = 'processing') AND
|
||||||
|
ss.type = ${type}
|
||||||
WHERE ss.aid IS NULL
|
WHERE ss.aid IS NULL
|
||||||
`;
|
`;
|
||||||
return rows.map((r) => Number(r.aid));
|
return rows.map((r) => Number(r.aid));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllVideosWithoutActiveSnapshotSchedule(psql: Psql) {
|
|
||||||
const rows = await psql<{ aid: number }[]>`
|
|
||||||
SELECT s.aid
|
|
||||||
FROM bilibili_metadata s
|
|
||||||
LEFT JOIN snapshot_schedule ss ON s.aid = ss.aid AND (ss.status = 'pending' OR ss.status = 'processing')
|
|
||||||
WHERE ss.aid IS NULL
|
|
||||||
`
|
|
||||||
return rows.map((r) => Number(r.aid));
|
|
||||||
}
|
|
||||||
|
@ -16,8 +16,8 @@ class AkariProto extends AIManager {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.models = {
|
this.models = {
|
||||||
"classifier": onnxClassifierPath,
|
classifier: onnxClassifierPath,
|
||||||
"embedding": onnxEmbeddingPath,
|
embedding: onnxEmbeddingPath
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ class AkariProto extends AIManager {
|
|||||||
|
|
||||||
const { input_ids } = await tokenizer(texts, {
|
const { input_ids } = await tokenizer(texts, {
|
||||||
add_special_tokens: false,
|
add_special_tokens: false,
|
||||||
return_tensor: false,
|
return_tensor: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const cumsum = (arr: number[]): number[] =>
|
const cumsum = (arr: number[]): number[] =>
|
||||||
@ -66,9 +66,9 @@ class AkariProto extends AIManager {
|
|||||||
|
|
||||||
const inputs = {
|
const inputs = {
|
||||||
input_ids: new ort.Tensor("int64", new BigInt64Array(flattened_input_ids.map(BigInt)), [
|
input_ids: new ort.Tensor("int64", new BigInt64Array(flattened_input_ids.map(BigInt)), [
|
||||||
flattened_input_ids.length,
|
flattened_input_ids.length
|
||||||
]),
|
]),
|
||||||
offsets: new ort.Tensor("int64", new BigInt64Array(offsets.map(BigInt)), [offsets.length]),
|
offsets: new ort.Tensor("int64", new BigInt64Array(offsets.map(BigInt)), [offsets.length])
|
||||||
};
|
};
|
||||||
|
|
||||||
const { embeddings } = await session.run(inputs);
|
const { embeddings } = await session.run(inputs);
|
||||||
@ -77,21 +77,14 @@ class AkariProto extends AIManager {
|
|||||||
|
|
||||||
private async runClassification(embeddings: number[]): Promise<number[]> {
|
private async runClassification(embeddings: number[]): Promise<number[]> {
|
||||||
const session = this.getModelSession("classifier");
|
const session = this.getModelSession("classifier");
|
||||||
const inputTensor = new ort.Tensor(
|
const inputTensor = new ort.Tensor(Float32Array.from(embeddings), [1, 3, 1024]);
|
||||||
Float32Array.from(embeddings),
|
|
||||||
[1, 3, 1024],
|
|
||||||
);
|
|
||||||
|
|
||||||
const { logits } = await session.run({ channel_features: inputTensor });
|
const { logits } = await session.run({ channel_features: inputTensor });
|
||||||
return this.softmax(logits.data as Float32Array);
|
return this.softmax(logits.data as Float32Array);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async classifyVideo(title: string, description: string, tags: string, aid?: number): Promise<number> {
|
public async classifyVideo(title: string, description: string, tags: string, aid?: number): Promise<number> {
|
||||||
const embeddings = await this.getJinaEmbeddings1024([
|
const embeddings = await this.getJinaEmbeddings1024([title, description, tags]);
|
||||||
title,
|
|
||||||
description,
|
|
||||||
tags,
|
|
||||||
]);
|
|
||||||
const probabilities = await this.runClassification(embeddings);
|
const probabilities = await this.runClassification(embeddings);
|
||||||
if (aid) {
|
if (aid) {
|
||||||
logger.log(`Prediction result for aid: ${aid}: [${probabilities.map((p) => p.toFixed(5))}]`, "ml");
|
logger.log(`Prediction result for aid: ${aid}: [${probabilities.map((p) => p.toFixed(5))}]`, "ml");
|
||||||
|
@ -6,8 +6,7 @@ export class AIManager {
|
|||||||
public sessions: { [key: string]: ort.InferenceSession } = {};
|
public sessions: { [key: string]: ort.InferenceSession } = {};
|
||||||
public models: { [key: string]: string } = {};
|
public models: { [key: string]: string } = {};
|
||||||
|
|
||||||
constructor() {
|
constructor() {}
|
||||||
}
|
|
||||||
|
|
||||||
public async init() {
|
public async init() {
|
||||||
const modelKeys = Object.keys(this.models);
|
const modelKeys = Object.keys(this.models);
|
||||||
|
@ -1,11 +1,28 @@
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
import { getAllVideosWithoutActiveSnapshotSchedule, scheduleSnapshot } from "db/snapshotSchedule.ts";
|
import { getVideosWithoutActiveSnapshotScheduleByType, scheduleSnapshot } from "db/snapshotSchedule.ts";
|
||||||
import logger from "@core/log/logger.ts";
|
import logger from "@core/log/logger.ts";
|
||||||
import { lockManager } from "@core/mq/lockManager.ts";
|
import { lockManager } from "@core/mq/lockManager.ts";
|
||||||
import { getLatestVideoSnapshot } from "db/snapshot.ts";
|
import { getLatestVideoSnapshot } from "db/snapshot.ts";
|
||||||
import { HOUR, MINUTE } from "@core/const/time.ts";
|
import { MINUTE } from "@core/const/time.ts";
|
||||||
import { sql } from "@core/db/dbNew";
|
import { sql } from "@core/db/dbNew";
|
||||||
|
|
||||||
|
function getNextSaturdayMidnightTimestamp(): number {
|
||||||
|
const now = new Date();
|
||||||
|
const currentDay = now.getDay();
|
||||||
|
|
||||||
|
let daysUntilNextSaturday = (6 - currentDay + 7) % 7;
|
||||||
|
|
||||||
|
if (daysUntilNextSaturday === 0) {
|
||||||
|
daysUntilNextSaturday = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSaturday = new Date(now);
|
||||||
|
nextSaturday.setDate(nextSaturday.getDate() + daysUntilNextSaturday);
|
||||||
|
nextSaturday.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
return nextSaturday.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
export const archiveSnapshotsWorker = async (_job: Job) => {
|
export const archiveSnapshotsWorker = async (_job: Job) => {
|
||||||
try {
|
try {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
@ -14,21 +31,22 @@ export const archiveSnapshotsWorker = async (_job: Job) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await lockManager.acquireLock("dispatchArchiveSnapshots", 30 * 60);
|
await lockManager.acquireLock("dispatchArchiveSnapshots", 30 * 60);
|
||||||
const aids = await getAllVideosWithoutActiveSnapshotSchedule(sql);
|
const aids = await getVideosWithoutActiveSnapshotScheduleByType(sql, "archive");
|
||||||
for (const rawAid of aids) {
|
for (const rawAid of aids) {
|
||||||
const aid = Number(rawAid);
|
const aid = Number(rawAid);
|
||||||
const latestSnapshot = await getLatestVideoSnapshot(sql, aid);
|
const latestSnapshot = await getLatestVideoSnapshot(sql, aid);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const lastSnapshotedAt = latestSnapshot?.time ?? now;
|
const lastSnapshotedAt = latestSnapshot?.time ?? now;
|
||||||
const interval = 168;
|
const nextSatMidnight = getNextSaturdayMidnightTimestamp();
|
||||||
|
const interval = nextSatMidnight - now;
|
||||||
logger.log(
|
logger.log(
|
||||||
`Scheduled archive snapshot for aid ${aid} in ${interval} hours.`,
|
`Scheduled archive snapshot for aid ${aid} in ${interval} hours.`,
|
||||||
"mq",
|
"mq",
|
||||||
"fn:archiveSnapshotsWorker"
|
"fn:archiveSnapshotsWorker"
|
||||||
);
|
);
|
||||||
const targetTime = lastSnapshotedAt + interval * HOUR;
|
const targetTime = lastSnapshotedAt + interval;
|
||||||
await scheduleSnapshot(sql, aid, "archive", targetTime);
|
await scheduleSnapshot(sql, aid, "archive", targetTime);
|
||||||
if (now - startedAt > 250 * MINUTE) {
|
if (now - startedAt > 30 * MINUTE) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export const classifyVideoWorker = async (job: Job) => {
|
|||||||
|
|
||||||
await job.updateData({
|
await job.updateData({
|
||||||
...job.data,
|
...job.data,
|
||||||
label: label,
|
label: label
|
||||||
});
|
});
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
@ -46,19 +46,19 @@ export const classifyVideosWorker = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await lockManager.acquireLock("classifyVideos");
|
await lockManager.acquireLock("classifyVideos", 5 * 60);
|
||||||
|
|
||||||
const videos = await getUnlabelledVideos(sql);
|
const videos = await getUnlabelledVideos(sql);
|
||||||
logger.log(`Found ${videos.length} unlabelled videos`);
|
logger.log(`Found ${videos.length} unlabelled videos`);
|
||||||
|
|
||||||
let i = 0;
|
const startTime = new Date().getTime();
|
||||||
for (const aid of videos) {
|
for (const aid of videos) {
|
||||||
if (i > 200) {
|
const now = new Date().getTime();
|
||||||
|
if (now - startTime > 4.2 * MINUTE) {
|
||||||
await lockManager.releaseLock("classifyVideos");
|
await lockManager.releaseLock("classifyVideos");
|
||||||
return 10000 + i;
|
return 1;
|
||||||
}
|
}
|
||||||
await ClassifyVideoQueue.add("classifyVideo", { aid: Number(aid) });
|
await ClassifyVideoQueue.add("classifyVideo", { aid: Number(aid) });
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
await lockManager.releaseLock("classifyVideos");
|
await lockManager.releaseLock("classifyVideos");
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
import { collectSongs } from "mq/task/collectSongs.ts";
|
import { collectSongs } from "mq/task/collectSongs.ts";
|
||||||
|
|
||||||
export const collectSongsWorker = async (_job: Job): Promise<void> =>{
|
export const collectSongsWorker = async (_job: Job): Promise<void> => {
|
||||||
await collectSongs();
|
await collectSongs();
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
@ -16,8 +16,8 @@ export const dispatchMilestoneSnapshotsWorker = async (_job: Job) => {
|
|||||||
if (eta > 144) continue;
|
if (eta > 144) continue;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const scheduledNextSnapshotDelay = eta * HOUR;
|
const scheduledNextSnapshotDelay = eta * HOUR;
|
||||||
const maxInterval = 1 * HOUR;
|
const maxInterval = 1.2 * HOUR;
|
||||||
const minInterval = 1 * SECOND;
|
const minInterval = 2 * SECOND;
|
||||||
const delay = truncate(scheduledNextSnapshotDelay, minInterval, maxInterval);
|
const delay = truncate(scheduledNextSnapshotDelay, minInterval, maxInterval);
|
||||||
const targetTime = now + delay;
|
const targetTime = now + delay;
|
||||||
await scheduleSnapshot(sql, aid, "milestone", targetTime);
|
await scheduleSnapshot(sql, aid, "milestone", targetTime);
|
||||||
@ -25,5 +25,5 @@ export const dispatchMilestoneSnapshotsWorker = async (_job: Job) => {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e as Error, "mq", "fn:dispatchMilestoneSnapshotsWorker");
|
logger.error(e as Error, "mq", "fn:dispatchMilestoneSnapshotsWorker");
|
||||||
};
|
}
|
||||||
}
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
import { getLatestVideoSnapshot } from "db/snapshot.ts";
|
import { getLatestVideoSnapshot } from "db/snapshot.ts";
|
||||||
import { truncate } from "utils/truncate.ts";
|
import { truncate } from "utils/truncate.ts";
|
||||||
import { getVideosWithoutActiveSnapshotSchedule, scheduleSnapshot } from "db/snapshotSchedule.ts";
|
import { getVideosWithoutActiveSnapshotScheduleByType, scheduleSnapshot } from "db/snapshotSchedule.ts";
|
||||||
import logger from "@core/log/logger.ts";
|
import logger from "@core/log/logger.ts";
|
||||||
import { HOUR, MINUTE, WEEK } from "@core/const/time.ts";
|
import { HOUR, MINUTE, WEEK } from "@core/const/time.ts";
|
||||||
import { lockManager } from "@core/mq/lockManager.ts";
|
import { lockManager } from "@core/mq/lockManager.ts";
|
||||||
@ -17,7 +17,7 @@ export const dispatchRegularSnapshotsWorker = async (_job: Job): Promise<void> =
|
|||||||
}
|
}
|
||||||
await lockManager.acquireLock("dispatchRegularSnapshots", 30 * 60);
|
await lockManager.acquireLock("dispatchRegularSnapshots", 30 * 60);
|
||||||
|
|
||||||
const aids = await getVideosWithoutActiveSnapshotSchedule(sql);
|
const aids = await getVideosWithoutActiveSnapshotScheduleByType(sql, "normal");
|
||||||
for (const rawAid of aids) {
|
for (const rawAid of aids) {
|
||||||
const aid = Number(rawAid);
|
const aid = Number(rawAid);
|
||||||
const latestSnapshot = await getLatestVideoSnapshot(sql, aid);
|
const latestSnapshot = await getLatestVideoSnapshot(sql, aid);
|
||||||
|
@ -2,6 +2,6 @@ import { sql } from "@core/db/dbNew";
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
import { queueLatestVideos } from "mq/task/queueLatestVideo.ts";
|
import { queueLatestVideos } from "mq/task/queueLatestVideo.ts";
|
||||||
|
|
||||||
export const getLatestVideosWorker = async (_job: Job): Promise<void> =>{
|
export const getLatestVideosWorker = async (_job: Job): Promise<void> => {
|
||||||
await queueLatestVideos(sql);
|
await queueLatestVideos(sql);
|
||||||
}
|
};
|
||||||
|
@ -10,4 +10,4 @@ export const getVideoInfoWorker = async (job: Job): Promise<void> => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await insertVideoInfo(sql, aid);
|
await insertVideoInfo(sql, aid);
|
||||||
}
|
};
|
||||||
|
@ -5,15 +5,15 @@ import {
|
|||||||
getBulkSnapshotsInNextSecond,
|
getBulkSnapshotsInNextSecond,
|
||||||
getSnapshotsInNextSecond,
|
getSnapshotsInNextSecond,
|
||||||
setSnapshotStatus,
|
setSnapshotStatus,
|
||||||
videoHasProcessingSchedule,
|
videoHasProcessingSchedule
|
||||||
} from "db/snapshotSchedule.ts";
|
} from "db/snapshotSchedule.ts";
|
||||||
import logger from "@core/log/logger.ts";
|
import logger from "@core/log/logger.ts";
|
||||||
import { SnapshotQueue } from "mq/index.ts";
|
import { SnapshotQueue } from "mq/index.ts";
|
||||||
import { sql } from "@core/db/dbNew";
|
import { sql } from "@core/db/dbNew";
|
||||||
|
|
||||||
const priorityMap: { [key: string]: number } = {
|
const priorityMap: { [key: string]: number } = {
|
||||||
"milestone": 1,
|
milestone: 1,
|
||||||
"normal": 3,
|
normal: 3
|
||||||
};
|
};
|
||||||
|
|
||||||
export const bulkSnapshotTickWorker = async (_job: Job) => {
|
export const bulkSnapshotTickWorker = async (_job: Job) => {
|
||||||
@ -35,12 +35,16 @@ export const bulkSnapshotTickWorker = async (_job: Job) => {
|
|||||||
created_at: schedule.created_at,
|
created_at: schedule.created_at,
|
||||||
started_at: schedule.started_at,
|
started_at: schedule.started_at,
|
||||||
finished_at: schedule.finished_at,
|
finished_at: schedule.finished_at,
|
||||||
status: schedule.status,
|
status: schedule.status
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
await SnapshotQueue.add("bulkSnapshotVideo", {
|
await SnapshotQueue.add(
|
||||||
schedules: schedulesData,
|
"bulkSnapshotVideo",
|
||||||
}, { priority: 3 });
|
{
|
||||||
|
schedules: schedulesData
|
||||||
|
},
|
||||||
|
{ priority: 3 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return `OK`;
|
return `OK`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -61,11 +65,15 @@ export const snapshotTickWorker = async (_job: Job) => {
|
|||||||
}
|
}
|
||||||
const aid = Number(schedule.aid);
|
const aid = Number(schedule.aid);
|
||||||
await setSnapshotStatus(sql, schedule.id, "processing");
|
await setSnapshotStatus(sql, schedule.id, "processing");
|
||||||
await SnapshotQueue.add("snapshotVideo", {
|
await SnapshotQueue.add(
|
||||||
|
"snapshotVideo",
|
||||||
|
{
|
||||||
aid: Number(aid),
|
aid: Number(aid),
|
||||||
id: Number(schedule.id),
|
id: Number(schedule.id),
|
||||||
type: schedule.type ?? "normal",
|
type: schedule.type ?? "normal"
|
||||||
}, { priority });
|
},
|
||||||
|
{ priority }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return `OK`;
|
return `OK`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -76,5 +84,5 @@ export const snapshotTickWorker = async (_job: Job) => {
|
|||||||
export const closetMilestone = (views: number) => {
|
export const closetMilestone = (views: number) => {
|
||||||
if (views < 100000) return 100000;
|
if (views < 100000) return 100000;
|
||||||
if (views < 1000000) return 1000000;
|
if (views < 1000000) return 1000000;
|
||||||
return 10000000;
|
return Math.ceil(views / 1000000) * 1000000;
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import { Job } from "bullmq";
|
import { Job } from "bullmq";
|
||||||
import { scheduleSnapshot, setSnapshotStatus, snapshotScheduleExists } from "db/snapshotSchedule.ts";
|
import { getLatestSnapshot, scheduleSnapshot, setSnapshotStatus, snapshotScheduleExists } from "db/snapshotSchedule.ts";
|
||||||
import logger from "@core/log/logger.ts";
|
import logger from "@core/log/logger.ts";
|
||||||
import { HOUR, MINUTE, SECOND } from "@core/const/time.ts";
|
import { HOUR, MINUTE, SECOND } from "@core/const/time.ts";
|
||||||
import { lockManager } from "@core/mq/lockManager.ts";
|
|
||||||
import { getBiliVideoStatus, setBiliVideoStatus } from "../../db/bilibili_metadata.ts";
|
import { getBiliVideoStatus, setBiliVideoStatus } from "../../db/bilibili_metadata.ts";
|
||||||
import { insertVideoSnapshot } from "mq/task/getVideoStats.ts";
|
import { insertVideoSnapshot } from "mq/task/getVideoStats.ts";
|
||||||
import { getSongsPublihsedAt } from "db/songs.ts";
|
import { getSongsPublihsedAt } from "db/songs.ts";
|
||||||
import { getAdjustedShortTermETA } from "mq/scheduling.ts";
|
import { getAdjustedShortTermETA } from "mq/scheduling.ts";
|
||||||
import { NetSchedulerError } from "@core/net/delegate.ts";
|
import { NetSchedulerError } from "@core/net/delegate.ts";
|
||||||
import { sql } from "@core/db/dbNew.ts";
|
import { sql } from "@core/db/dbNew.ts";
|
||||||
|
import { closetMilestone } from "./snapshotTick.ts";
|
||||||
|
|
||||||
const snapshotTypeToTaskMap: { [key: string]: string } = {
|
const snapshotTypeToTaskMap: { [key: string]: string } = {
|
||||||
"milestone": "snapshotMilestoneVideo",
|
milestone: "snapshotMilestoneVideo",
|
||||||
"normal": "snapshotVideo",
|
normal: "snapshotVideo",
|
||||||
"new": "snapshotMilestoneVideo",
|
new: "snapshotMilestoneVideo"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const snapshotVideoWorker = async (job: Job): Promise<void> => {
|
export const snapshotVideoWorker = async (job: Job): Promise<void> => {
|
||||||
@ -22,6 +22,7 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
|
|||||||
const type = job.data.type;
|
const type = job.data.type;
|
||||||
const task = snapshotTypeToTaskMap[type] ?? "snapshotVideo";
|
const task = snapshotTypeToTaskMap[type] ?? "snapshotVideo";
|
||||||
const retryInterval = type === "milestone" ? 5 * SECOND : 2 * MINUTE;
|
const retryInterval = type === "milestone" ? 5 * SECOND : 2 * MINUTE;
|
||||||
|
const latestSnapshot = await getLatestSnapshot(sql, aid);
|
||||||
try {
|
try {
|
||||||
const exists = await snapshotScheduleExists(sql, id);
|
const exists = await snapshotScheduleExists(sql, id);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@ -32,7 +33,7 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`Video ${aid} has status ${status} in the database. Abort snapshoting.`,
|
`Video ${aid} has status ${status} in the database. Abort snapshoting.`,
|
||||||
"mq",
|
"mq",
|
||||||
"fn:dispatchRegularSnapshotsWorker",
|
"fn:dispatchRegularSnapshotsWorker"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -44,7 +45,7 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`Bilibili return status ${status} when snapshoting for ${aid}.`,
|
`Bilibili return status ${status} when snapshoting for ${aid}.`,
|
||||||
"mq",
|
"mq",
|
||||||
"fn:dispatchRegularSnapshotsWorker",
|
"fn:dispatchRegularSnapshotsWorker"
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -52,7 +53,7 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
|
|||||||
if (type === "new") {
|
if (type === "new") {
|
||||||
const publihsedAt = await getSongsPublihsedAt(sql, aid);
|
const publihsedAt = await getSongsPublihsedAt(sql, aid);
|
||||||
const timeSincePublished = stat.time - publihsedAt!;
|
const timeSincePublished = stat.time - publihsedAt!;
|
||||||
const viewsPerHour = stat.views / timeSincePublished * HOUR;
|
const viewsPerHour = (stat.views / timeSincePublished) * HOUR;
|
||||||
if (timeSincePublished > 48 * HOUR) {
|
if (timeSincePublished > 48 * HOUR) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -72,46 +73,41 @@ export const snapshotVideoWorker = async (job: Job): Promise<void> => {
|
|||||||
await scheduleSnapshot(sql, aid, type, Date.now() + intervalMins * MINUTE, true);
|
await scheduleSnapshot(sql, aid, type, Date.now() + intervalMins * MINUTE, true);
|
||||||
}
|
}
|
||||||
if (type !== "milestone") return;
|
if (type !== "milestone") return;
|
||||||
|
const alreadyAchievedMilestone = stat.views > closetMilestone(latestSnapshot.views);
|
||||||
|
if (alreadyAchievedMilestone) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const eta = await getAdjustedShortTermETA(sql, aid);
|
const eta = await getAdjustedShortTermETA(sql, aid);
|
||||||
if (eta > 144) {
|
if (eta > 144) {
|
||||||
const etaHoursString = eta.toFixed(2) + " hrs";
|
const etaHoursString = eta.toFixed(2) + " hrs";
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`ETA (${etaHoursString}) too long for milestone snapshot. aid: ${aid}.`,
|
`ETA (${etaHoursString}) too long for milestone snapshot. aid: ${aid}.`,
|
||||||
"mq",
|
"mq",
|
||||||
"fn:dispatchRegularSnapshotsWorker",
|
"fn:snapshotVideoWorker"
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const targetTime = now + eta * HOUR;
|
const targetTime = now + eta * HOUR;
|
||||||
await scheduleSnapshot(sql, aid, type, targetTime);
|
await scheduleSnapshot(sql, aid, type, targetTime);
|
||||||
await setSnapshotStatus(sql, id, "completed");
|
await setSnapshotStatus(sql, id, "completed");
|
||||||
return;
|
return;
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
if (e instanceof NetSchedulerError && e.code === "NO_PROXY_AVAILABLE") {
|
if (e instanceof NetSchedulerError && e.code === "NO_PROXY_AVAILABLE") {
|
||||||
logger.warn(
|
logger.warn(`No available proxy for aid ${job.data.aid}.`, "mq", "fn:snapshotVideoWorker");
|
||||||
`No available proxy for aid ${job.data.aid}.`,
|
|
||||||
"mq",
|
|
||||||
"fn:takeSnapshotForVideoWorker",
|
|
||||||
);
|
|
||||||
await setSnapshotStatus(sql, id, "no_proxy");
|
await setSnapshotStatus(sql, id, "no_proxy");
|
||||||
await scheduleSnapshot(sql, aid, type, Date.now() + retryInterval);
|
await scheduleSnapshot(sql, aid, type, Date.now() + retryInterval, false, true);
|
||||||
return;
|
return;
|
||||||
}
|
} else if (e instanceof NetSchedulerError && e.code === "ALICLOUD_PROXY_ERR") {
|
||||||
else if (e instanceof NetSchedulerError && e.code === "ALICLOUD_PROXY_ERR") {
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Failed to proxy request for aid ${job.data.aid}: ${e.message}`,
|
`Failed to proxy request for aid ${job.data.aid}: ${e.message}`,
|
||||||
"mq",
|
"mq",
|
||||||
"fn:takeSnapshotForVideoWorker",
|
"fn:snapshotVideoWorker"
|
||||||
);
|
);
|
||||||
await setSnapshotStatus(sql, id, "failed");
|
await setSnapshotStatus(sql, id, "failed");
|
||||||
await scheduleSnapshot(sql, aid, type, Date.now() + retryInterval);
|
await scheduleSnapshot(sql, aid, type, Date.now() + retryInterval);
|
||||||
}
|
}
|
||||||
logger.error(e as Error, "mq", "fn:takeSnapshotForVideoWorker");
|
logger.error(e as Error, "mq", "fn:snapshotVideoWorker");
|
||||||
await setSnapshotStatus(sql, id, "failed");
|
await setSnapshotStatus(sql, id, "failed");
|
||||||
}
|
}
|
||||||
finally {
|
|
||||||
await lockManager.releaseLock("dispatchRegularSnapshots");
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
bulkScheduleSnapshot,
|
bulkScheduleSnapshot,
|
||||||
bulkSetSnapshotStatus,
|
bulkSetSnapshotStatus,
|
||||||
scheduleSnapshot,
|
scheduleSnapshot,
|
||||||
snapshotScheduleExists,
|
snapshotScheduleExists
|
||||||
} from "db/snapshotSchedule.ts";
|
} from "db/snapshotSchedule.ts";
|
||||||
import { bulkGetVideoStats } from "net/bulkGetVideoStats.ts";
|
import { bulkGetVideoStats } from "net/bulkGetVideoStats.ts";
|
||||||
import logger from "@core/log/logger.ts";
|
import logger from "@core/log/logger.ts";
|
||||||
@ -55,7 +55,7 @@ export const takeBulkSnapshotForVideosWorker = async (job: Job) => {
|
|||||||
${shares},
|
${shares},
|
||||||
${favorites}
|
${favorites}
|
||||||
)
|
)
|
||||||
`
|
`;
|
||||||
|
|
||||||
logger.log(`Taken snapshot for video ${aid} in bulk.`, "net", "fn:takeBulkSnapshotForVideosWorker");
|
logger.log(`Taken snapshot for video ${aid} in bulk.`, "net", "fn:takeBulkSnapshotForVideosWorker");
|
||||||
}
|
}
|
||||||
@ -72,13 +72,16 @@ export const takeBulkSnapshotForVideosWorker = async (job: Job) => {
|
|||||||
return `DONE`;
|
return `DONE`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof NetSchedulerError && e.code === "NO_PROXY_AVAILABLE") {
|
if (e instanceof NetSchedulerError && e.code === "NO_PROXY_AVAILABLE") {
|
||||||
logger.warn(
|
logger.warn(`No available proxy for bulk request now.`, "mq", "fn:takeBulkSnapshotForVideosWorker");
|
||||||
`No available proxy for bulk request now.`,
|
|
||||||
"mq",
|
|
||||||
"fn:takeBulkSnapshotForVideosWorker",
|
|
||||||
);
|
|
||||||
await bulkSetSnapshotStatus(sql, ids, "no_proxy");
|
await bulkSetSnapshotStatus(sql, ids, "no_proxy");
|
||||||
await bulkScheduleSnapshot(sql, aidsToFetch, "normal", Date.now() + 20 * MINUTE * Math.random());
|
await bulkScheduleSnapshot(
|
||||||
|
sql,
|
||||||
|
aidsToFetch,
|
||||||
|
"normal",
|
||||||
|
Date.now() + 20 * MINUTE * Math.random(),
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.error(e as Error, "mq", "fn:takeBulkSnapshotForVideosWorker");
|
logger.error(e as Error, "mq", "fn:takeBulkSnapshotForVideosWorker");
|
||||||
|
@ -62,8 +62,8 @@ export async function initMQ() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await SnapshotQueue.upsertJobScheduler("dispatchArchiveSnapshots", {
|
await SnapshotQueue.upsertJobScheduler("dispatchArchiveSnapshots", {
|
||||||
every: 6 * HOUR,
|
every: 2 * HOUR,
|
||||||
immediately: true
|
immediately: false
|
||||||
});
|
});
|
||||||
|
|
||||||
await SnapshotQueue.upsertJobScheduler("scheduleCleanup", {
|
await SnapshotQueue.upsertJobScheduler("scheduleCleanup", {
|
||||||
|
@ -33,7 +33,7 @@ export const getAdjustedShortTermETA = async (sql: Psql, aid: number) => {
|
|||||||
if (!snapshotsEnough) return 0;
|
if (!snapshotsEnough) return 0;
|
||||||
|
|
||||||
const currentTimestamp = new Date().getTime();
|
const currentTimestamp = new Date().getTime();
|
||||||
const timeIntervals = [3 * MINUTE, 20 * MINUTE, 1 * HOUR, 3 * HOUR, 6 * HOUR, 72 * HOUR];
|
const timeIntervals = [3 * MINUTE, 20 * MINUTE, HOUR, 3 * HOUR, 6 * HOUR, 72 * HOUR];
|
||||||
const DELTA = 0.00001;
|
const DELTA = 0.00001;
|
||||||
let minETAHours = Infinity;
|
let minETAHours = Infinity;
|
||||||
|
|
||||||
|
@ -25,5 +25,5 @@ export async function insertIntoSongs(sql: Psql, aid: number) {
|
|||||||
(SELECT duration FROM bilibili_metadata WHERE aid = ${aid})
|
(SELECT duration FROM bilibili_metadata WHERE aid = ${aid})
|
||||||
)
|
)
|
||||||
ON CONFLICT DO NOTHING
|
ON CONFLICT DO NOTHING
|
||||||
`
|
`;
|
||||||
}
|
}
|
||||||
|
@ -18,9 +18,9 @@ export async function insertVideoInfo(sql: Psql, aid: number) {
|
|||||||
const bvid = data.View.bvid;
|
const bvid = data.View.bvid;
|
||||||
const desc = data.View.desc;
|
const desc = data.View.desc;
|
||||||
const uid = data.View.owner.mid;
|
const uid = data.View.owner.mid;
|
||||||
const tags = data.Tags
|
const tags = data.Tags.filter((tag) => !["old_channel", "topic"].indexOf(tag.tag_type))
|
||||||
.filter((tag) => !["old_channel", "topic"].indexOf(tag.tag_type))
|
.map((tag) => tag.tag_name)
|
||||||
.map((tag) => tag.tag_name).join(",");
|
.join(",");
|
||||||
const title = data.View.title;
|
const title = data.View.title;
|
||||||
const published_at = formatTimestampToPsql(data.View.pubdate * SECOND + 8 * HOUR);
|
const published_at = formatTimestampToPsql(data.View.pubdate * SECOND + 8 * HOUR);
|
||||||
const duration = data.View.duration;
|
const duration = data.View.duration;
|
||||||
@ -55,7 +55,7 @@ export async function insertVideoInfo(sql: Psql, aid: number) {
|
|||||||
${stat.share},
|
${stat.share},
|
||||||
${stat.favorite}
|
${stat.favorite}
|
||||||
)
|
)
|
||||||
`
|
`;
|
||||||
|
|
||||||
logger.log(`Inserted video metadata for aid: ${aid}`, "mq");
|
logger.log(`Inserted video metadata for aid: ${aid}`, "mq");
|
||||||
await ClassifyVideoQueue.add("classifyVideo", { aid });
|
await ClassifyVideoQueue.add("classifyVideo", { aid });
|
||||||
|
@ -24,11 +24,7 @@ export interface SnapshotNumber {
|
|||||||
* - The native `fetch` function threw an error: with error code `FETCH_ERROR`
|
* - The native `fetch` function threw an error: with error code `FETCH_ERROR`
|
||||||
* - The alicloud-fc threw an error: with error code `ALICLOUD_FC_ERROR`
|
* - The alicloud-fc threw an error: with error code `ALICLOUD_FC_ERROR`
|
||||||
*/
|
*/
|
||||||
export async function insertVideoSnapshot(
|
export async function insertVideoSnapshot(sql: Psql, aid: number, task: string): Promise<number | SnapshotNumber> {
|
||||||
sql: Psql,
|
|
||||||
aid: number,
|
|
||||||
task: string,
|
|
||||||
): Promise<number | SnapshotNumber> {
|
|
||||||
const data = await getVideoInfo(aid, task);
|
const data = await getVideoInfo(aid, task);
|
||||||
if (typeof data == "number") {
|
if (typeof data == "number") {
|
||||||
return data;
|
return data;
|
||||||
@ -45,7 +41,7 @@ export async function insertVideoSnapshot(
|
|||||||
await sql`
|
await sql`
|
||||||
INSERT INTO video_snapshot (aid, views, danmakus, replies, likes, coins, shares, favorites)
|
INSERT INTO video_snapshot (aid, views, danmakus, replies, likes, coins, shares, favorites)
|
||||||
VALUES (${aid}, ${views}, ${danmakus}, ${replies}, ${likes}, ${coins}, ${shares}, ${favorites})
|
VALUES (${aid}, ${views}, ${danmakus}, ${replies}, ${likes}, ${coins}, ${shares}, ${favorites})
|
||||||
`
|
`;
|
||||||
|
|
||||||
logger.log(`Taken snapshot for video ${aid}.`, "net", "fn:insertVideoSnapshot");
|
logger.log(`Taken snapshot for video ${aid}.`, "net", "fn:insertVideoSnapshot");
|
||||||
|
|
||||||
@ -58,6 +54,6 @@ export async function insertVideoSnapshot(
|
|||||||
coins,
|
coins,
|
||||||
shares,
|
shares,
|
||||||
favorites,
|
favorites,
|
||||||
time,
|
time
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,7 @@ import logger from "@core/log/logger.ts";
|
|||||||
import { LatestVideosQueue } from "mq/index.ts";
|
import { LatestVideosQueue } from "mq/index.ts";
|
||||||
import type { Psql } from "@core/db/psql.d.ts";
|
import type { Psql } from "@core/db/psql.d.ts";
|
||||||
|
|
||||||
export async function queueLatestVideos(
|
export async function queueLatestVideos(sql: Psql): Promise<number | null> {
|
||||||
sql: Psql,
|
|
||||||
): Promise<number | null> {
|
|
||||||
let page = 1;
|
let page = 1;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const videosFound = new Set();
|
const videosFound = new Set();
|
||||||
@ -26,14 +24,18 @@ export async function queueLatestVideos(
|
|||||||
if (videoExists) {
|
if (videoExists) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
await LatestVideosQueue.add("getVideoInfo", { aid }, {
|
await LatestVideosQueue.add(
|
||||||
|
"getVideoInfo",
|
||||||
|
{ aid },
|
||||||
|
{
|
||||||
delay,
|
delay,
|
||||||
attempts: 100,
|
attempts: 100,
|
||||||
backoff: {
|
backoff: {
|
||||||
type: "fixed",
|
type: "fixed",
|
||||||
delay: SECOND * 5,
|
delay: SECOND * 5
|
||||||
},
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
videosFound.add(aid);
|
videosFound.add(aid);
|
||||||
allExists = false;
|
allExists = false;
|
||||||
delay += Math.random() * SECOND * 1.5;
|
delay += Math.random() * SECOND * 1.5;
|
||||||
@ -42,7 +44,7 @@ export async function queueLatestVideos(
|
|||||||
logger.log(
|
logger.log(
|
||||||
`Page ${page} crawled, total: ${videosFound.size}/${i} videos added/observed.`,
|
`Page ${page} crawled, total: ${videosFound.size}/${i} videos added/observed.`,
|
||||||
"net",
|
"net",
|
||||||
"fn:queueLatestVideos()",
|
"fn:queueLatestVideos()"
|
||||||
);
|
);
|
||||||
if (allExists) {
|
if (allExists) {
|
||||||
return 0;
|
return 0;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { findClosestSnapshot, findSnapshotBefore, getLatestSnapshot } from "db/snapshotSchedule.ts";
|
import { findClosestSnapshot, findSnapshotBefore, getLatestSnapshot } from "db/snapshotSchedule.ts";
|
||||||
import { HOUR } from "@core/const/time.ts";
|
import { HOUR } from "@core/const/time.ts";
|
||||||
import type { Psql } from "@core/db/psql";
|
import type { Psql } from "@core/db/psql.d.ts";
|
||||||
|
|
||||||
export const getRegularSnapshotInterval = async (sql: Psql, aid: number) => {
|
export const getRegularSnapshotInterval = async (sql: Psql, aid: number) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
@ -2,11 +2,7 @@ import { sql } from "@core/db/dbNew";
|
|||||||
import logger from "@core/log/logger.ts";
|
import logger from "@core/log/logger.ts";
|
||||||
|
|
||||||
export async function removeAllTimeoutSchedules() {
|
export async function removeAllTimeoutSchedules() {
|
||||||
logger.log(
|
logger.log("Too many timeout schedules, directly removing these schedules...", "mq", "fn:scheduleCleanupWorker");
|
||||||
"Too many timeout schedules, directly removing these schedules...",
|
|
||||||
"mq",
|
|
||||||
"fn:scheduleCleanupWorker",
|
|
||||||
);
|
|
||||||
return await sql`
|
return await sql`
|
||||||
DELETE FROM snapshot_schedule
|
DELETE FROM snapshot_schedule
|
||||||
WHERE status IN ('pending', 'processing')
|
WHERE status IN ('pending', 'processing')
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "crawler",
|
"name": "crawler",
|
||||||
|
"version": "1.3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "bun --env-file=.env.test run vitest",
|
"test": "bun --env-file=.env.test run vitest",
|
||||||
"worker:main": "bun run ./src/worker.ts",
|
"worker:main": "bun run ./src/worker.ts",
|
||||||
@ -7,7 +8,8 @@
|
|||||||
"worker:filter": "bun run ./build/filterWorker.js",
|
"worker:filter": "bun run ./build/filterWorker.js",
|
||||||
"adder": "bun run ./src/jobAdder.ts",
|
"adder": "bun run ./src/jobAdder.ts",
|
||||||
"bullui": "bun run ./src/bullui.ts",
|
"bullui": "bun run ./src/bullui.ts",
|
||||||
"all": "bun run concurrently --restart-tries -1 'bun run worker:main' 'bun run adder' 'bun run bullui' 'bun run worker:filter'"
|
"all": "bun run concurrently --restart-tries -1 'bun run worker:main' 'bun run adder' 'bun run worker:filter'",
|
||||||
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2"
|
"concurrently": "^9.1.2"
|
||||||
|
@ -6,7 +6,6 @@ await Bun.build({
|
|||||||
target: "node"
|
target: "node"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const file = Bun.file("./build/filterWorker.js");
|
const file = Bun.file("./build/filterWorker.js");
|
||||||
const code = await file.text();
|
const code = await file.text();
|
||||||
|
|
||||||
|
@ -11,9 +11,9 @@ createBullBoard({
|
|||||||
queues: [
|
queues: [
|
||||||
new BullMQAdapter(LatestVideosQueue),
|
new BullMQAdapter(LatestVideosQueue),
|
||||||
new BullMQAdapter(ClassifyVideoQueue),
|
new BullMQAdapter(ClassifyVideoQueue),
|
||||||
new BullMQAdapter(SnapshotQueue),
|
new BullMQAdapter(SnapshotQueue)
|
||||||
],
|
],
|
||||||
serverAdapter: serverAdapter,
|
serverAdapter: serverAdapter
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
@ -12,8 +12,8 @@ const shutdown = async (signal: string) => {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||||
|
|
||||||
await Akari.init();
|
await Akari.init();
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ const filterWorker = new Worker(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ connection: redis as ConnectionOptions, concurrency: 2, removeOnComplete: { count: 1000 } },
|
{ connection: redis as ConnectionOptions, concurrency: 2, removeOnComplete: { count: 1000 } }
|
||||||
);
|
);
|
||||||
|
|
||||||
filterWorker.on("active", () => {
|
filterWorker.on("active", () => {
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
scheduleCleanupWorker,
|
scheduleCleanupWorker,
|
||||||
snapshotTickWorker,
|
snapshotTickWorker,
|
||||||
snapshotVideoWorker,
|
snapshotVideoWorker,
|
||||||
takeBulkSnapshotForVideosWorker,
|
takeBulkSnapshotForVideosWorker
|
||||||
} from "mq/exec/executors.ts";
|
} from "mq/exec/executors.ts";
|
||||||
import { redis } from "@core/db/redis.ts";
|
import { redis } from "@core/db/redis.ts";
|
||||||
import logger from "@core/log/logger.ts";
|
import logger from "@core/log/logger.ts";
|
||||||
@ -37,8 +37,8 @@ const shutdown = async (signal: string) => {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('SIGINT', () => shutdown('SIGINT'));
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
||||||
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
||||||
|
|
||||||
const latestVideoWorker = new Worker(
|
const latestVideoWorker = new Worker(
|
||||||
"latestVideos",
|
"latestVideos",
|
||||||
@ -58,8 +58,8 @@ const latestVideoWorker = new Worker(
|
|||||||
connection: redis as ConnectionOptions,
|
connection: redis as ConnectionOptions,
|
||||||
concurrency: 6,
|
concurrency: 6,
|
||||||
removeOnComplete: { count: 1440 },
|
removeOnComplete: { count: 1440 },
|
||||||
removeOnFail: { count: 0 },
|
removeOnFail: { count: 0 }
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
latestVideoWorker.on("active", () => {
|
latestVideoWorker.on("active", () => {
|
||||||
@ -95,7 +95,7 @@ const snapshotWorker = new Worker(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ connection: redis as ConnectionOptions, concurrency: 50, removeOnComplete: { count: 2000 } },
|
{ connection: redis as ConnectionOptions, concurrency: 50, removeOnComplete: { count: 2000 } }
|
||||||
);
|
);
|
||||||
|
|
||||||
snapshotWorker.on("error", (err) => {
|
snapshotWorker.on("error", (err) => {
|
||||||
|
@ -56,26 +56,26 @@ const databasePreparationQuery = `
|
|||||||
CREATE INDEX idx_snapshot_schedule_status ON snapshot_schedule USING btree (status);
|
CREATE INDEX idx_snapshot_schedule_status ON snapshot_schedule USING btree (status);
|
||||||
CREATE INDEX idx_snapshot_schedule_type ON snapshot_schedule USING btree (type);
|
CREATE INDEX idx_snapshot_schedule_type ON snapshot_schedule USING btree (type);
|
||||||
CREATE UNIQUE INDEX snapshot_schedule_pkey ON snapshot_schedule USING btree (id);
|
CREATE UNIQUE INDEX snapshot_schedule_pkey ON snapshot_schedule USING btree (id);
|
||||||
`
|
`;
|
||||||
|
|
||||||
const cleanUpQuery = `
|
const cleanUpQuery = `
|
||||||
DROP SEQUENCE IF EXISTS "snapshot_schedule_id_seq" CASCADE;
|
DROP SEQUENCE IF EXISTS "snapshot_schedule_id_seq" CASCADE;
|
||||||
DROP TABLE IF EXISTS "snapshot_schedule" CASCADE;
|
DROP TABLE IF EXISTS "snapshot_schedule" CASCADE;
|
||||||
`
|
`;
|
||||||
|
|
||||||
async function testMocking() {
|
async function testMocking() {
|
||||||
await sql.begin(async tx => {
|
await sql.begin(async (tx) => {
|
||||||
await tx.unsafe(cleanUpQuery).simple();
|
await tx.unsafe(cleanUpQuery).simple();
|
||||||
await tx.unsafe(databasePreparationQuery).simple();
|
await tx.unsafe(databasePreparationQuery).simple();
|
||||||
|
|
||||||
await tx`
|
await tx`
|
||||||
INSERT INTO snapshot_schedule
|
INSERT INTO snapshot_schedule
|
||||||
${sql(mockSnapshotSchedules, 'aid', 'created_at', 'finished_at', 'id', 'started_at', 'status', 'type')}
|
${sql(mockSnapshotSchedules, "aid", "created_at", "finished_at", "id", "started_at", "status", "type")}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await tx`
|
await tx`
|
||||||
ROLLBACK;
|
ROLLBACK;
|
||||||
`
|
`;
|
||||||
|
|
||||||
await tx.unsafe(cleanUpQuery).simple();
|
await tx.unsafe(cleanUpQuery).simple();
|
||||||
return;
|
return;
|
||||||
@ -83,26 +83,26 @@ async function testMocking() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testBulkSetSnapshotStatus() {
|
async function testBulkSetSnapshotStatus() {
|
||||||
return await sql.begin(async tx => {
|
return await sql.begin(async (tx) => {
|
||||||
await tx.unsafe(cleanUpQuery).simple();
|
await tx.unsafe(cleanUpQuery).simple();
|
||||||
await tx.unsafe(databasePreparationQuery).simple();
|
await tx.unsafe(databasePreparationQuery).simple();
|
||||||
|
|
||||||
await tx`
|
await tx`
|
||||||
INSERT INTO snapshot_schedule
|
INSERT INTO snapshot_schedule
|
||||||
${sql(mockSnapshotSchedules, 'aid', 'created_at', 'finished_at', 'id', 'started_at', 'status', 'type')}
|
${sql(mockSnapshotSchedules, "aid", "created_at", "finished_at", "id", "started_at", "status", "type")}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ids = [1, 2, 3];
|
const ids = [1, 2, 3];
|
||||||
|
|
||||||
await bulkSetSnapshotStatus(tx, ids, 'pending')
|
await bulkSetSnapshotStatus(tx, ids, "pending");
|
||||||
|
|
||||||
const rows = tx<{status: string}[]>`
|
const rows = tx<{ status: string }[]>`
|
||||||
SELECT status FROM snapshot_schedule WHERE id = 1;
|
SELECT status FROM snapshot_schedule WHERE id = 1;
|
||||||
`.execute();
|
`.execute();
|
||||||
|
|
||||||
await tx`
|
await tx`
|
||||||
ROLLBACK;
|
ROLLBACK;
|
||||||
`
|
`;
|
||||||
|
|
||||||
await tx.unsafe(cleanUpQuery).simple();
|
await tx.unsafe(cleanUpQuery).simple();
|
||||||
return rows;
|
return rows;
|
||||||
@ -116,5 +116,5 @@ test("data mocking works", async () => {
|
|||||||
|
|
||||||
test("bulkSetSnapshotStatus core logic works smoothly", async () => {
|
test("bulkSetSnapshotStatus core logic works smoothly", async () => {
|
||||||
const rows = await testBulkSetSnapshotStatus();
|
const rows = await testBulkSetSnapshotStatus();
|
||||||
expect(rows.every(item => item.status === 'pending')).toBe(true);
|
expect(rows.every((item) => item.status === "pending")).toBe(true);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export function formatTimestampToPsql(timestamp: number) {
|
export function formatTimestampToPsql(timestamp: number) {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
return date.toISOString().slice(0, 23).replace("T", " ");
|
return date.toISOString().slice(0, 23).replace("T", " ") + "+08";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseTimestampFromPsql(timestamp: string) {
|
export function parseTimestampFromPsql(timestamp: string) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tsconfigPaths()]
|
plugins: [tsconfigPaths()]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"version": "1.9.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
|
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 "./global.css";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { routing } from "@/i18n/routing";
|
import { routing } from "@/i18n/routing";
|
||||||
import { NextIntlClientProvider, hasLocale } from "next-intl";
|
import { hasLocale } from "next-intl";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "中V档案馆"
|
title: "中 V 档案馆"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
@ -20,19 +20,5 @@ export default async function RootLayout({
|
|||||||
if (!hasLocale(routing.locales, locale)) {
|
if (!hasLocale(routing.locales, locale)) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
return (
|
return <>{children}</>;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
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 useSWRMutation from "swr/mutation";
|
||||||
import { requestSignUp } from "./request";
|
import { requestSignUp } from "./request";
|
||||||
import { FilledButton } from "@/components/ui/Buttons/FilledButton";
|
import { FilledButton } from "@/components/ui/Buttons/FilledButton";
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "@/components/utils/ErrorDialog";
|
||||||
import { ApiRequestError } from "@/lib/net";
|
import { ApiRequestError } from "@/lib/net";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ const SignUpForm: React.FC<RegistrationFormProps> = ({ backendURL }) => {
|
|||||||
if (typeof item === "string") {
|
if (typeof item === "string") {
|
||||||
return item;
|
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 () => {
|
const register = async () => {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { LeftArrow } from "@/components/icons/LeftArrow";
|
import { LeftArrow } from "@/components/icons/LeftArrow";
|
||||||
import { RightArrow } from "@/components/icons/RightArrow";
|
import { RightArrow } from "@/components/icons/RightArrow";
|
||||||
import SignUpForm from "./SignUpForm";
|
import SignUpForm from "./SignUpForm";
|
||||||
|
import { Link } from "@/i18n/navigation";
|
||||||
|
|
||||||
export default function SignupPage() {
|
export default function SignupPage() {
|
||||||
return (
|
return (
|
||||||
@ -10,10 +11,10 @@ export default function SignupPage() {
|
|||||||
md:bg-surface-container md:dark:bg-dark-surface-container"
|
md:bg-surface-container md:dark:bg-dark-surface-container"
|
||||||
>
|
>
|
||||||
<p className="mb-2">
|
<p className="mb-2">
|
||||||
<a href="/">
|
<Link href="/">
|
||||||
<LeftArrow className="inline -translate-y-0.5 scale-90 mr-1" aria-hidden="true" />
|
<LeftArrow className="inline -translate-y-0.5 scale-90 mr-1" aria-hidden="true" />
|
||||||
首页
|
首页
|
||||||
</a>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<h1 className="text-5xl leading-[4rem] font-extralight">欢迎</h1>
|
<h1 className="text-5xl leading-[4rem] font-extralight">欢迎</h1>
|
||||||
<p className="mt-2 md:mt-3">
|
<p className="mt-2 md:mt-3">
|
||||||
@ -28,10 +29,10 @@ export default function SignupPage() {
|
|||||||
</p>
|
</p>
|
||||||
<p className="mt-4 mb-7">
|
<p className="mt-4 mb-7">
|
||||||
已有账户?
|
已有账户?
|
||||||
<a href="/login">
|
<Link href="/login">
|
||||||
<span>登录</span>
|
<span>登录</span>
|
||||||
<RightArrow className="text-xs inline -translate-y-0.5 ml-1" aria-hidden="true" />
|
<RightArrow className="text-xs inline -translate-y-0.5 ml-1" aria-hidden="true" />
|
||||||
</a>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<SignUpForm backendURL={process.env.NEXT_PUBLIC_BACKEND_URL ?? ""} />
|
<SignUpForm backendURL={process.env.NEXT_PUBLIC_BACKEND_URL ?? ""} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,8 +3,19 @@ import { ApiRequestError, fetcher } from "@/lib/net";
|
|||||||
import type { CaptchaVerificationRawResponse, ErrorResponse, SignUpResponse } from "@cvsa/backend";
|
import type { CaptchaVerificationRawResponse, ErrorResponse, SignUpResponse } from "@cvsa/backend";
|
||||||
import { Link } from "@/i18n/navigation";
|
import { Link } from "@/i18n/navigation";
|
||||||
import { LocalizedMessage } from "./SignUpForm";
|
import { LocalizedMessage } from "./SignUpForm";
|
||||||
import { ErrorDialog } from "./ErrorDialog";
|
import { ErrorDialog } from "@/components/utils/ErrorDialog";
|
||||||
import { string, object, ValidationError } from "yup";
|
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 {
|
interface SignUpFormData {
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -6,6 +6,8 @@ import { getVideoMetadata } from "@/lib/db/bilibili_metadata/getVideoMetadata";
|
|||||||
import { aidExists as idExists } from "@/lib/db/bilibili_metadata/aidExists";
|
import { aidExists as idExists } from "@/lib/db/bilibili_metadata/aidExists";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { BiliVideoMetadataType, VideoSnapshotType } from "@cvsa/core";
|
import { BiliVideoMetadataType, VideoSnapshotType } from "@cvsa/core";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
const MetadataRow = ({ title, desc }: { title: string; desc: string | number | undefined | null }) => {
|
const MetadataRow = ({ title, desc }: { title: string; desc: string | number | undefined | null }) => {
|
||||||
if (!desc) return <></>;
|
if (!desc) return <></>;
|
||||||
@ -19,6 +21,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 }> }) {
|
export default async function VideoInfoPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
let videoInfo: BiliVideoMetadataType | null = null;
|
let videoInfo: BiliVideoMetadataType | null = null;
|
||||||
@ -88,18 +105,16 @@ export default async function VideoInfoPage({ params }: { params: Promise<{ id:
|
|||||||
title="发布时间"
|
title="发布时间"
|
||||||
desc={
|
desc={
|
||||||
videoInfo.published_at
|
videoInfo.published_at
|
||||||
? format(new Date(videoInfo.published_at), "yyyy-MM-dd HH:mm:ss", {
|
? DateTime.fromJSDate(videoInfo.published_at).toFormat(
|
||||||
locale: zhCN
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
})
|
)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<MetadataRow title="时长 (秒)" desc={videoInfo.duration} />
|
<MetadataRow title="时长 (秒)" desc={videoInfo.duration} />
|
||||||
<MetadataRow
|
<MetadataRow
|
||||||
title="创建时间"
|
title="创建时间"
|
||||||
desc={format(new Date(videoInfo.created_at), "yyyy-MM-dd HH:mm:ss", {
|
desc={DateTime.fromJSDate(videoInfo.created_at).toFormat("yyyy-MM-dd HH:mm:ss")}
|
||||||
locale: zhCN
|
|
||||||
})}
|
|
||||||
/>
|
/>
|
||||||
<MetadataRow title="封面" desc={videoInfo?.cover_url} />
|
<MetadataRow title="封面" desc={videoInfo?.cover_url} />
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -126,11 +141,11 @@ export default async function VideoInfoPage({ params }: { params: Promise<{ id:
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{snapshots.map((snapshot) => (
|
{snapshots.map((snapshot) => (
|
||||||
<tr key={snapshot.created_at}>
|
<tr key={snapshot.id}>
|
||||||
<td className="border dark:border-zinc-500 px-4 py-2">
|
<td className="border dark:border-zinc-500 px-4 py-2">
|
||||||
{format(new Date(snapshot.created_at), "yyyy-MM-dd HH:mm:ss", {
|
{DateTime.fromJSDate(snapshot.created_at).toFormat(
|
||||||
locale: zhCN
|
"yyyy-MM-dd HH:mm:ss"
|
||||||
})}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="border dark:border-zinc-500 px-4 py-2">{snapshot.views}</td>
|
<td className="border dark:border-zinc-500 px-4 py-2">{snapshot.views}</td>
|
||||||
<td className="border dark:border-zinc-500 px-4 py-2">{snapshot.coins}</td>
|
<td className="border dark:border-zinc-500 px-4 py-2">{snapshot.coins}</td>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Header } from "@/components/shell/Header";
|
import HeaderServer from "@/components/shell/HeaderServer";
|
||||||
import { getCurrentUser } from "@/lib/userAuth";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
@ -7,10 +6,9 @@ export default async function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const user = await getCurrentUser();
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header user={user} />
|
<HeaderServer />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
import { Header } from "@/components/shell/Header";
|
|
||||||
import { getCurrentUser } from "@/lib/userAuth";
|
|
||||||
import { VDFtestCard } from "./TestCard";
|
import { VDFtestCard } from "./TestCard";
|
||||||
|
import HeaderServer from "@/components/shell/HeaderServer";
|
||||||
|
|
||||||
export default async function VdfBenchmarkPage() {
|
export default async function VdfBenchmarkPage() {
|
||||||
const user = await getCurrentUser();
|
|
||||||
|
|
||||||
return (
|
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">
|
<div className="md:w-2/3 lg:w-1/2 xl:w-[37%] md:mx-auto mx-6 mb-12">
|
||||||
<VDFtestCard />
|
<VDFtestCard />
|
||||||
<div>
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
64
packages/next/app/[locale]/user/[uid]/profile/page.tsx
Normal file
64
packages/next/app/[locale]/user/[uid]/profile/page.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { getUserProfile, User } from "@/lib/userAuth";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { zhCN } from "date-fns/locale";
|
||||||
|
import { LogoutButton } from "./LogoutButton";
|
||||||
|
import { numeric } from "yup-numeric";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import HeaderServer from "@/components/shell/HeaderServer";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
const uidSchema = numeric().integer().min(0);
|
||||||
|
|
||||||
|
interface SignupTimeProps {
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignupTime: React.FC<SignupTimeProps> = ({ user }: SignupTimeProps) => {
|
||||||
|
return (
|
||||||
|
<p className="mt-4">
|
||||||
|
于
|
||||||
|
{DateTime.fromJSDate(user.createdAt).toFormat("yyyy-MM-dd HH:mm:ss")}
|
||||||
|
注册。
|
||||||
|
</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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
import type { VideoInfoData } from "@cvsa/core";
|
||||||
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
const StatRow = ({ title, description }: { title: string; description?: number }) => {
|
const StatRow = ({ title, description }: { title: string; description?: number }) => {
|
||||||
return (
|
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 VideoInfo = async ({ id }: { id: string }) => {
|
||||||
const backendURL = process.env.BACKEND_URL;
|
const backendURL = process.env.BACKEND_URL;
|
||||||
|
|
||||||
@ -21,7 +38,7 @@ const VideoInfo = async ({ id }: { id: string }) => {
|
|||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data: VideoInfoData = await res.json();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full lg:max-w-4xl lg:mx-auto lg:p-6 px-4">
|
<div className="w-full lg:max-w-4xl lg:mx-auto lg:p-6 px-4">
|
||||||
@ -34,7 +51,7 @@ const VideoInfo = async ({ id }: { id: string }) => {
|
|||||||
{data.bvid} · av{data.aid}
|
{data.bvid} · av{data.aid}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span>发布于 {format(new Date(data.pubdate * 1000), "yyyy-MM-dd HH:mm:ss")}</span>
|
<span>发布于 {DateTime.fromSeconds(data.pubdate).toFormat("yyyy-MM-dd HH:mm:ss")}</span>
|
||||||
<br />
|
<br />
|
||||||
<span>播放:{(data.stat?.view ?? 0).toLocaleString()}</span> ·{" "}
|
<span>播放:{(data.stat?.view ?? 0).toLocaleString()}</span> ·{" "}
|
||||||
<span>弹幕:{(data.stat?.danmaku ?? 0).toLocaleString()}</span>
|
<span>弹幕:{(data.stat?.danmaku ?? 0).toLocaleString()}</span>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Header } from "@/components/shell/Header";
|
import HeaderServer from "@/components/shell/HeaderServer";
|
||||||
import { getCurrentUser } from "@/lib/userAuth";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
@ -7,10 +6,9 @@ export default async function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const user = await getCurrentUser();
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header user={user} />
|
<HeaderServer />
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "中V档案馆"
|
title: "中 V 档案馆"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
@ -17,7 +17,6 @@ export default async function RootLayout({
|
|||||||
<head>
|
<head>
|
||||||
<meta charSet="UTF-8" />
|
<meta charSet="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>中V档案馆</title>
|
|
||||||
</head>
|
</head>
|
||||||
<body className="min-h-screen flex flex-col">
|
<body className="min-h-screen flex flex-col">
|
||||||
<NextIntlClientProvider>
|
<NextIntlClientProvider>
|
||||||
|
@ -5,23 +5,28 @@
|
|||||||
"name": "next",
|
"name": "next",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cvsa/backend": "^0.5.3",
|
"@cvsa/backend": "^0.5.3",
|
||||||
"@cvsa/core": "^0.0.5",
|
"@cvsa/core": "0.0.10",
|
||||||
"@mdx-js/loader": "^3.1.0",
|
"@mdx-js/loader": "^3.1.0",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
"@next/mdx": "^15.3.3",
|
"@next/mdx": "^15.3.3",
|
||||||
|
"@types/luxon": "^3.6.2",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^12.15.0",
|
"framer-motion": "^12.15.0",
|
||||||
"fumadocs-mdx": "^11.6.6",
|
"fumadocs-mdx": "^11.6.6",
|
||||||
"i18next": "^25.2.1",
|
"i18next": "^25.2.1",
|
||||||
|
"jotai": "^2.12.5",
|
||||||
|
"luxon": "^3.6.1",
|
||||||
"next": "^15.3.3",
|
"next": "^15.3.3",
|
||||||
"next-intl": "^4.1.0",
|
"next-intl": "^4.1.0",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
"ua-parser-js": "^2.0.3",
|
"ua-parser-js": "^2.0.3",
|
||||||
"yup": "^1.6.1",
|
"yup": "^1.6.1",
|
||||||
|
"yup-numeric": "^0.5.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.8",
|
"@tailwindcss/postcss": "^4.1.8",
|
||||||
@ -44,7 +49,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/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.10", "", { "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-8gjSNRyLZcybLiFSUZFPc4nJsLQ7YO8lZSAEFJidyUA3a6CbB/UUC4G5jqWyWJ7xDA39w7szqpbVYKX3fb6W3g=="],
|
||||||
|
|
||||||
"@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=="],
|
"@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 +167,8 @@
|
|||||||
|
|
||||||
"@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="],
|
"@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/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=="],
|
"@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 +265,20 @@
|
|||||||
|
|
||||||
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
"@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": ["@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/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/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/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
|
||||||
|
|
||||||
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
|
"@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=="],
|
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
|
||||||
@ -286,12 +301,52 @@
|
|||||||
|
|
||||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
"@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=="],
|
"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": ["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=="],
|
"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-colors": ["ansi-colors@4.1.3", "", {}, "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
@ -316,6 +371,10 @@
|
|||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"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=="],
|
"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=="],
|
"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 +385,10 @@
|
|||||||
|
|
||||||
"browser-stdout": ["browser-stdout@1.3.1", "", {}, "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="],
|
"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=="],
|
"busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="],
|
||||||
|
|
||||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
@ -358,6 +421,8 @@
|
|||||||
|
|
||||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
"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=="],
|
"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=="],
|
"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 +445,8 @@
|
|||||||
|
|
||||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
"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=="],
|
"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=="],
|
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
||||||
@ -426,8 +493,12 @@
|
|||||||
|
|
||||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
"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=="],
|
"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=="],
|
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||||
|
|
||||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||||
@ -438,6 +509,8 @@
|
|||||||
|
|
||||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
"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-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=="],
|
"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 +527,14 @@
|
|||||||
|
|
||||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
"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=="],
|
"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-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=="],
|
"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 +553,8 @@
|
|||||||
|
|
||||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
"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": ["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=="],
|
"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 +563,12 @@
|
|||||||
|
|
||||||
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
|
"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=="],
|
"fdir": ["fdir@6.4.5", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw=="],
|
||||||
|
|
||||||
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
||||||
@ -532,6 +619,8 @@
|
|||||||
|
|
||||||
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"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=="],
|
"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=="],
|
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||||
@ -616,10 +705,20 @@
|
|||||||
|
|
||||||
"is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
|
||||||
|
|
||||||
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
||||||
@ -648,6 +747,10 @@
|
|||||||
|
|
||||||
"limiter": ["limiter@3.0.0", "", {}, "sha512-hev7DuXojsTFl2YwyzUJMDnZ/qBDd3yZQLSH3aD4tdL1cqfc3TMnoecEJtWFaQFdErZsKoFMBTxF/FBSkgDbEg=="],
|
"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=="],
|
"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=="],
|
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
|
||||||
@ -664,6 +767,8 @@
|
|||||||
|
|
||||||
"lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
|
"lru-cache": ["lru-cache@11.1.0", "", {}, "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A=="],
|
||||||
|
|
||||||
|
"luxon": ["luxon@3.6.1", "", {}, "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||||
|
|
||||||
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
||||||
@ -708,6 +813,8 @@
|
|||||||
|
|
||||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
"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": ["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=="],
|
"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 +909,16 @@
|
|||||||
|
|
||||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
"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": ["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=="],
|
"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-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=="],
|
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
@ -852,6 +963,8 @@
|
|||||||
|
|
||||||
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
|
"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=="],
|
"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=="],
|
"randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="],
|
||||||
@ -862,6 +975,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-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": ["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=="],
|
"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 +1025,8 @@
|
|||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"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=="],
|
"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=="],
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
@ -920,6 +1037,8 @@
|
|||||||
|
|
||||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
"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=="],
|
"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=="],
|
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
|
||||||
@ -952,6 +1071,8 @@
|
|||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"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=="],
|
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
|
||||||
|
|
||||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||||
@ -992,6 +1113,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=="],
|
"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=="],
|
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||||
|
|
||||||
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
|
"tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="],
|
||||||
@ -1046,6 +1171,10 @@
|
|||||||
|
|
||||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
"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-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=="],
|
"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 +1191,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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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 +1225,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": ["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=="],
|
"zod": ["zod@3.25.46", "", {}, "sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ=="],
|
||||||
|
|
||||||
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
|
||||||
@ -1110,10 +1247,14 @@
|
|||||||
|
|
||||||
"accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"fumadocs-core/@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.1", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg=="],
|
||||||
@ -1132,12 +1273,20 @@
|
|||||||
|
|
||||||
"send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||||
@ -1150,10 +1299,22 @@
|
|||||||
|
|
||||||
"send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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 React, { useState } from "react";
|
||||||
import { NavigationDrawer } from "@/components/ui/NavigatinDrawer";
|
import { NavigationDrawer } from "@/components/ui/NavigatinDrawer";
|
||||||
import { Portal } from "@/components/utils/Portal";
|
import { Portal } from "@/components/utils/Portal";
|
||||||
import { RegisterIcon } from "@/components/icons/RegisterIcon";
|
|
||||||
import { SearchBox } from "@/components/ui/SearchBox";
|
import { SearchBox } from "@/components/ui/SearchBox";
|
||||||
import { MenuIcon } from "@/components/icons/MenuIcon";
|
import { MenuIcon } from "@/components/icons/MenuIcon";
|
||||||
import { SearchIcon } from "@/components/icons/SearchIcon";
|
import { SearchIcon } from "@/components/icons/SearchIcon";
|
||||||
@ -16,24 +15,26 @@ import { InfoIcon } from "@/components/icons/InfoIcon";
|
|||||||
import { HomeIcon } from "@/components/icons/HomeIcon";
|
import { HomeIcon } from "@/components/icons/HomeIcon";
|
||||||
import { TextButton } from "@/components/ui/Buttons/TextButton";
|
import { TextButton } from "@/components/ui/Buttons/TextButton";
|
||||||
import { Link } from "@/i18n/navigation";
|
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 {
|
interface HeaderProps {
|
||||||
user: UserResponse | null;
|
user: User | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderDestop = ({ user }: HeaderProps) => {
|
export const HeaderDestop = ({ user }: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="hidden md:flex relative top-0 left-0 w-full h-28 z-20 justify-between">
|
<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">
|
<div className="w-[305px] xl:ml-8 inline-flex items-center">
|
||||||
<a href="/">
|
<Link href="/">
|
||||||
<DarkModeImage
|
<DarkModeImage
|
||||||
lightSrc={TitleLight}
|
lightSrc={TitleLight}
|
||||||
darkSrc={TitleDark}
|
darkSrc={TitleDark}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
className="w-[305px] h-24 inline-block max-w-[15rem] lg:max-w-[305px]"
|
className="w-[305px] h-24 inline-block max-w-[15rem] lg:max-w-[305px]"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SearchBox />
|
<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"
|
text-xl font-medium items-center w-[15rem] min-w-[8rem] mr-4 lg:mr-0 lg:w-[305px] justify-end"
|
||||||
>
|
>
|
||||||
{user ? (
|
{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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -89,14 +90,25 @@ export const HeaderMobile = ({ user }: HeaderProps) => {
|
|||||||
</TextButton>
|
</TextButton>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link href="/signup">
|
{user ? (
|
||||||
<TextButton className="w-full h-14 flex px-4 justify-start" size="m">
|
<Link href={`/user/${user.uid}/profile`}>
|
||||||
<div className="flex items-center">
|
<TextButton className="w-full h-14 flex justify-start" size="m">
|
||||||
<RegisterIcon className="text-2xl pr-4" />
|
<div className="flex items-center w-72">
|
||||||
<span>注册</span>
|
<AccountIcon className="text-2xl pr-4" />
|
||||||
|
<span>{user.nickname || user.username}</span>
|
||||||
</div>
|
</div>
|
||||||
</TextButton>
|
</TextButton>
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
</NavigationDrawer>
|
</NavigationDrawer>
|
||||||
</Portal>
|
</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";
|
import useRipple from "@/components/utils/useRipple";
|
||||||
|
|
||||||
interface FilledButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface FilledButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import useRipple from "@/components/utils/useRipple";
|
import useRipple from "@/components/utils/useRipple";
|
||||||
|
|
||||||
interface TextButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface TextButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
@ -5,13 +7,16 @@ interface TextButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
|
|||||||
shape?: "round" | "square";
|
shape?: "round" | "square";
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
ripple?: boolean;
|
ripple?: boolean;
|
||||||
|
ref?: React.Ref<HTMLButtonElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TextButton = ({
|
export const TextButton = ({
|
||||||
children,
|
children,
|
||||||
size = "s",
|
size = "s",
|
||||||
shape = "round",
|
shape = "round",
|
||||||
className,
|
className = "",
|
||||||
|
disabled,
|
||||||
|
ref,
|
||||||
ripple = true,
|
ripple = true,
|
||||||
...rest
|
...rest
|
||||||
}: TextButtonProps) => {
|
}: TextButtonProps) => {
|
||||||
@ -29,12 +34,19 @@ export const TextButton = ({
|
|||||||
<button
|
<button
|
||||||
className={`text-primary dark:text-dark-primary duration-150 select-none
|
className={`text-primary dark:text-dark-primary duration-150 select-none
|
||||||
flex items-center justify-center relative overflow-hidden
|
flex items-center justify-center relative overflow-hidden
|
||||||
|
disabled:text-on-surface/40 disabled:dark:text-dark-on-surface/40
|
||||||
${sizeClasses} ${shapeClasses} ${className}`}
|
${sizeClasses} ${shapeClasses} ${className}`}
|
||||||
{...rest}
|
{...rest}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onTouchStart={onTouchStart}
|
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}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { TextButton } from "./Buttons/TextButton";
|
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) => {
|
export const useDisableBodyScroll = (open: boolean) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -23,10 +28,14 @@ type ButtonElementAttr = React.HTMLAttributes<HTMLButtonElement>;
|
|||||||
|
|
||||||
type DialogHeadlineProps = OptionalChidrenProps<HeadElementAttr>;
|
type DialogHeadlineProps = OptionalChidrenProps<HeadElementAttr>;
|
||||||
type DialogSupportingTextProps = OptionalChidrenProps<DivElementAttr>;
|
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> {
|
interface DialogButtonProps extends OptionalChidrenProps<ButtonElementAttr> {
|
||||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
index?: number;
|
||||||
}
|
}
|
||||||
interface DialogProps extends OptionalChidrenProps<DivElementAttr> {
|
interface DialogProps extends OptionalChidrenProps<DivElementAttr> {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@ -63,48 +72,180 @@ 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 (
|
return (
|
||||||
<TextButton onClick={onClick} {...rest}>
|
<TextButton onClick={onClick} {...rest} ref={buttonRef}>
|
||||||
{children}
|
{children}
|
||||||
</TextButton>
|
</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 (
|
return (
|
||||||
<div className="flex justify-end gap-2" {...rest}>
|
<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>
|
</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) => {
|
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);
|
useDisableBodyScroll(show);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{show && (
|
{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
|
<motion.div
|
||||||
className="fixed top-0 left-0 w-full h-full z-40 bg-black/20 pointer-none"
|
className="fixed top-0 left-0 w-full h-full z-40 bg-black/20 pointer-none"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.35 }}
|
||||||
/>
|
/>
|
||||||
<motion.div
|
<motion.div
|
||||||
className={`fixed min-w-[17.5rem] sm:max-w-[35rem] h-auto z-50 bg-surface-container-high
|
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}`}
|
shadow-2xl shadow-shadow/15 rounded-[1.75rem] p-6 dark:bg-dark-surface-container-high mx-2
|
||||||
initial={{ opacity: 0.5, transform: "scale(1.1)" }}
|
origin-top ${className} overflow-hidden grid ${isSupported && "grid-rows-[0fr]"}`}
|
||||||
animate={{ opacity: 1, transform: "scale(1)" }}
|
initial={{
|
||||||
exit={{ opacity: 0 }}
|
opacity: 0,
|
||||||
transition={{ ease: [0.31, 0.69, 0.3, 1.02], duration: 0.3 }}
|
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"
|
aria-modal="true"
|
||||||
|
ref={dialogRef}
|
||||||
|
>
|
||||||
|
<div className="min-h-0">
|
||||||
|
<motion.div
|
||||||
|
className="origin-top"
|
||||||
|
initial={{ opacity: 0, transform: "translateY(5px)" }}
|
||||||
|
animate={{ opacity: 1, transform: "translateY(0px)" }}
|
||||||
|
exit={{ opacity: 0, transform: "translateY(5px)" }}
|
||||||
|
transition={{
|
||||||
|
ease: [0.05, 0.7, 0.1, 1.0],
|
||||||
|
duration: 0.35
|
||||||
|
}}
|
||||||
|
ref={contentRef}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
);
|
);
|
||||||
|
@ -58,7 +58,7 @@ export const SearchBox: React.FC<SearchBoxProps> = ({ close = () => {} }) => {
|
|||||||
type="search"
|
type="search"
|
||||||
placeholder="搜索"
|
placeholder="搜索"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="none"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
className="bg-transparent h-full w-full focus:outline-none"
|
className="bg-transparent h-full w-full focus:outline-none"
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
@ -73,7 +73,7 @@ export const SearchBox: React.FC<SearchBoxProps> = ({ close = () => {} }) => {
|
|||||||
type="search"
|
type="search"
|
||||||
placeholder="搜索"
|
placeholder="搜索"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="none"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
className="bg-transparent h-full w-full focus:outline-none"
|
className="bg-transparent h-full w-full focus:outline-none"
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
@ -14,7 +14,7 @@ export const ErrorDialog: React.FC<ErrorDialogProps> = ({ children, closeDialog,
|
|||||||
<>
|
<>
|
||||||
<DialogHeadline>{errorCode ? t(errorCode) : "错误"}</DialogHeadline>
|
<DialogHeadline>{errorCode ? t(errorCode) : "错误"}</DialogHeadline>
|
||||||
<DialogSupportingText>{children}</DialogSupportingText>
|
<DialogSupportingText>{children}</DialogSupportingText>
|
||||||
<DialogButtonGroup>
|
<DialogButtonGroup close={closeDialog}>
|
||||||
<DialogButton onClick={closeDialog}>关闭</DialogButton>
|
<DialogButton onClick={closeDialog}>关闭</DialogButton>
|
||||||
</DialogButtonGroup>
|
</DialogButtonGroup>
|
||||||
</>
|
</>
|
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": "用户名",
|
"username": "用户名",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"nickname": "昵称",
|
"nickname": "昵称",
|
||||||
|
"profile_page": {
|
||||||
|
"role": {
|
||||||
|
"ADMIN": "管理员",
|
||||||
|
"USER": "普通用户",
|
||||||
|
"OWNER": "所有者"
|
||||||
|
}
|
||||||
|
},
|
||||||
"backend": {
|
"backend": {
|
||||||
"error": {
|
"error": {
|
||||||
|
"incorrect_password": "密码错误。",
|
||||||
"captcha_failed": "无法完成安全验证。",
|
"captcha_failed": "无法完成安全验证。",
|
||||||
"user_exists": "用户名 “{username}” 已被占用。",
|
"user_exists": "用户名 “{username}” 已被占用。",
|
||||||
"user_not_found_after_register": "我们的服务器出现错误:找不到名为'{username}'的用户。请联系我们的<support>支持团队</support>,反馈此问题。",
|
"user_not_found_after_register": "我们的服务器出现错误:找不到名为'{username}'的用户。请联系我们的<support>支持团队</support>,反馈此问题。",
|
||||||
|
@ -1,19 +1,56 @@
|
|||||||
import { UserType, sqlCred } from "@cvsa/core";
|
import { UserType, sqlCred } from "@cvsa/core";
|
||||||
|
import { UserProfile } from "../userAuth";
|
||||||
|
|
||||||
export const getUserBySession = async (sessionID: string) => {
|
export const getUserBySession = async (sessionID: string) => {
|
||||||
const users = await sqlCred<UserType[]>`
|
const users = await sqlCred<UserType[]>`
|
||||||
SELECT u.*
|
SELECT user_id as id, username, nickname, "role", user_created_at as created_at
|
||||||
FROM users u
|
FROM get_user_by_session_func(${sessionID});
|
||||||
JOIN login_sessions ls ON u.id = ls.uid
|
|
||||||
WHERE ls.id = ${sessionID};
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (users.length === 0) {
|
if (users.length === 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = users[0];
|
const user = users[0];
|
||||||
return {
|
return {
|
||||||
|
uid: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
nickname: user.nickname,
|
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 {
|
export class ApiRequestError extends Error {
|
||||||
public code: number | undefined;
|
public code: number | undefined;
|
||||||
@ -21,10 +21,20 @@ const httpMethods = {
|
|||||||
patch: axios.patch
|
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>(
|
export async function fetcher<JSON = unknown>(
|
||||||
url: string,
|
url: string,
|
||||||
init?: Omit<AxiosRequestConfig, "method"> & { method?: HttpMethod }
|
init?: Omit<AxiosRequestConfig, "method"> & { method?: HttpMethod }
|
||||||
): Promise<JSON> {
|
): Promise<JSON | AxiosResponse<any, any>> {
|
||||||
const { method = "get", data, ...config } = init || {};
|
const { method = "get", data, ...config } = init || {};
|
||||||
|
|
||||||
const fullConfig: AxiosRequestConfig = {
|
const fullConfig: AxiosRequestConfig = {
|
||||||
@ -38,6 +48,9 @@ export async function fetcher<JSON = unknown>(
|
|||||||
if (["post", "patch", "put"].includes(m)) {
|
if (["post", "patch", "put"].includes(m)) {
|
||||||
const response = await httpMethods[m](url, data, fullConfig);
|
const response = await httpMethods[m](url, data, fullConfig);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
} else if (m === "delete") {
|
||||||
|
const response = await axios.delete(url, fullConfig);
|
||||||
|
return response;
|
||||||
} else {
|
} else {
|
||||||
const response = await httpMethods[m](url, fullConfig);
|
const response = await httpMethods[m](url, fullConfig);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
@ -1,17 +1,43 @@
|
|||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
import { getUserBySession } from "@/lib/db/user";
|
import { getUserBySession, queryUserProfile } from "@/lib/db/user";
|
||||||
import type { UserResponse } from "@cvsa/backend";
|
|
||||||
|
|
||||||
export async function getCurrentUser(): Promise<UserResponse | null> {
|
export interface User {
|
||||||
|
uid: number;
|
||||||
|
username: string;
|
||||||
|
nickname: string | null;
|
||||||
|
role: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserProfile extends User {
|
||||||
|
isLoggedIn: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentUser(): Promise<User | null> {
|
||||||
const cookieStore = await cookies();
|
const cookieStore = await cookies();
|
||||||
const sessionID = cookieStore.get("session_id");
|
const sessionID = cookieStore.get("session_id");
|
||||||
|
|
||||||
if (!sessionID) return null;
|
if (!sessionID) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await getUserBySession(sessionID.value);
|
const user = await getUserBySession(sessionID.value);
|
||||||
|
|
||||||
return user ?? null;
|
return user ?? null;
|
||||||
} catch (error) {
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,21 @@ const nextConfig: NextConfig = {
|
|||||||
experimental: {
|
experimental: {
|
||||||
externalDir: true
|
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"]
|
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "next",
|
"name": "next",
|
||||||
"version": "0.1.0",
|
"version": "2.9.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack -p 7400",
|
"dev": "next dev --turbopack -p 7400",
|
||||||
@ -11,23 +11,28 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cvsa/backend": "^0.5.3",
|
"@cvsa/backend": "^0.5.3",
|
||||||
"@cvsa/core": "^0.0.5",
|
"@cvsa/core": "0.0.10",
|
||||||
"@mdx-js/loader": "^3.1.0",
|
"@mdx-js/loader": "^3.1.0",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
"@next/mdx": "^15.3.3",
|
"@next/mdx": "^15.3.3",
|
||||||
|
"@types/luxon": "^3.6.2",
|
||||||
"@types/mdx": "^2.0.13",
|
"@types/mdx": "^2.0.13",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^12.15.0",
|
"framer-motion": "^12.15.0",
|
||||||
"fumadocs-mdx": "^11.6.6",
|
"fumadocs-mdx": "^11.6.6",
|
||||||
"i18next": "^25.2.1",
|
"i18next": "^25.2.1",
|
||||||
|
"jotai": "^2.12.5",
|
||||||
|
"luxon": "^3.6.1",
|
||||||
"next": "^15.3.3",
|
"next": "^15.3.3",
|
||||||
"next-intl": "^4.1.0",
|
"next-intl": "^4.1.0",
|
||||||
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"swr": "^2.3.3",
|
"swr": "^2.3.3",
|
||||||
"ua-parser-js": "^2.0.3",
|
"ua-parser-js": "^2.0.3",
|
||||||
"yup": "^1.6.1"
|
"yup": "^1.6.1",
|
||||||
|
"yup-numeric": "^0.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
|
@ -1,121 +0,0 @@
|
|||||||
import { Database } from "jsr:@db/sqlite@0.12";
|
|
||||||
import { ensureDir } from "https://deno.land/std@0.113.0/fs/mod.ts";
|
|
||||||
|
|
||||||
// 常量定义
|
|
||||||
const MAX_RETRIES = 3;
|
|
||||||
const API_URL = "https://api.bilibili.com/x/web-interface/newlist?rid=30&ps=50&pn=";
|
|
||||||
const DATABASE_PATH = "./data/main.db";
|
|
||||||
const LOG_DIR = "./logs/bili-info-crawl";
|
|
||||||
const LOG_FILE = `${LOG_DIR}/run-${Date.now() / 1000}.log`;
|
|
||||||
|
|
||||||
// 打开数据库
|
|
||||||
const db = new Database(DATABASE_PATH, { int64: true });
|
|
||||||
|
|
||||||
// 设置日志
|
|
||||||
async function setupLogging() {
|
|
||||||
await ensureDir(LOG_DIR);
|
|
||||||
const logStream = await Deno.open(LOG_FILE, {
|
|
||||||
write: true,
|
|
||||||
create: true,
|
|
||||||
append: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const redirectConsole =
|
|
||||||
// deno-lint-ignore no-explicit-any
|
|
||||||
(originalConsole: (...args: any[]) => void) =>
|
|
||||||
// deno-lint-ignore no-explicit-any
|
|
||||||
(...args: any[]) => {
|
|
||||||
const message = args.map((
|
|
||||||
arg,
|
|
||||||
) => (typeof arg === "object" ? JSON.stringify(arg) : arg)).join(" ");
|
|
||||||
originalConsole(message);
|
|
||||||
logStream.write(new TextEncoder().encode(message + "\n"));
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log = redirectConsole(console.log);
|
|
||||||
console.error = redirectConsole(console.error);
|
|
||||||
console.warn = redirectConsole(console.warn);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Metadata {
|
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取最后一次更新的时间
|
|
||||||
function getLastUpdate(): Date {
|
|
||||||
const result = db.prepare(
|
|
||||||
"SELECT value FROM metadata WHERE key = 'fetchAid-lastUpdate'",
|
|
||||||
).get() as Metadata;
|
|
||||||
return result ? new Date(result.value as string) : new Date(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最后更新时间
|
|
||||||
function updateLastUpdate() {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
db.prepare("UPDATE metadata SET value = ? WHERE key = 'fetchAid-lastUpdate'")
|
|
||||||
.run(now);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数:获取数据
|
|
||||||
// deno-lint-ignore no-explicit-any
|
|
||||||
async function fetchData(pn: number, retries = MAX_RETRIES): Promise<any> {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_URL}${pn}`);
|
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
|
||||||
return await response.json();
|
|
||||||
} catch (error) {
|
|
||||||
if (retries > 0) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
return fetchData(pn, retries - 1);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 插入 aid 到数据库
|
|
||||||
function insertAid(aid: number) {
|
|
||||||
db.prepare(
|
|
||||||
"INSERT OR IGNORE INTO bili_info_crawl (aid, status) VALUES (?, 'pending')",
|
|
||||||
).run(aid);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主函数
|
|
||||||
async function main() {
|
|
||||||
await setupLogging();
|
|
||||||
|
|
||||||
let pn = 1;
|
|
||||||
let shouldContinue = true;
|
|
||||||
const lastUpdate = getLastUpdate();
|
|
||||||
|
|
||||||
while (shouldContinue) {
|
|
||||||
try {
|
|
||||||
const data = await fetchData(pn);
|
|
||||||
const archives = data.data.archives;
|
|
||||||
|
|
||||||
for (const archive of archives) {
|
|
||||||
const pubTime = new Date(archive.pubdate * 1000);
|
|
||||||
if (pubTime > lastUpdate) {
|
|
||||||
insertAid(archive.aid);
|
|
||||||
} else {
|
|
||||||
shouldContinue = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pn++;
|
|
||||||
console.log(`Fetched page ${pn}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching data for pn=${pn}: ${error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新最后更新时间
|
|
||||||
updateLastUpdate();
|
|
||||||
|
|
||||||
// 关闭数据库
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 运行主函数
|
|
||||||
main().catch(console.error);
|
|
@ -1,223 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
import { Database } from "jsr:@db/sqlite@0.12";
|
|
||||||
import { getBiliBiliVideoInfo } from "./videoInfo.ts";
|
|
||||||
import { ensureDir } from "https://deno.land/std@0.113.0/fs/mod.ts";
|
|
||||||
|
|
||||||
const aidPath = "./data/2025010104_c30_aids.txt";
|
|
||||||
const db = new Database("./data/main.db", { int64: true });
|
|
||||||
const regions = [
|
|
||||||
"shanghai",
|
|
||||||
"hangzhou",
|
|
||||||
"qingdao",
|
|
||||||
"beijing",
|
|
||||||
"zhangjiakou",
|
|
||||||
"chengdu",
|
|
||||||
"shenzhen",
|
|
||||||
"hohhot",
|
|
||||||
];
|
|
||||||
const logDir = "./logs/bili-info-crawl";
|
|
||||||
const logFile = path.join(logDir, `run-${Date.now() / 1000}.log`);
|
|
||||||
const shouldReadTextFile = false;
|
|
||||||
|
|
||||||
const SECOND = 1000;
|
|
||||||
const SECONDS = SECOND;
|
|
||||||
const MINUTE = 60 * SECONDS;
|
|
||||||
const MINUTES = MINUTE;
|
|
||||||
const IPs = regions.length;
|
|
||||||
|
|
||||||
const rateLimits = [
|
|
||||||
{ window: 5 * MINUTES, maxRequests: 160 * IPs },
|
|
||||||
{ window: 30 * SECONDS, maxRequests: 20 * IPs },
|
|
||||||
{ window: 1.2 * SECOND, maxRequests: 1 * IPs },
|
|
||||||
];
|
|
||||||
|
|
||||||
const requestQueue: number[] = [];
|
|
||||||
|
|
||||||
async function setupLogging() {
|
|
||||||
await ensureDir(logDir);
|
|
||||||
const logStream = await Deno.open(logFile, {
|
|
||||||
write: true,
|
|
||||||
create: true,
|
|
||||||
append: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const redirectConsole =
|
|
||||||
// deno-lint-ignore no-explicit-any
|
|
||||||
(originalConsole: (...args: any[]) => void) =>
|
|
||||||
// deno-lint-ignore no-explicit-any
|
|
||||||
(...args: any[]) => {
|
|
||||||
const message = args.map((
|
|
||||||
arg,
|
|
||||||
) => (typeof arg === "object" ? JSON.stringify(arg) : arg)).join(" ");
|
|
||||||
originalConsole(message);
|
|
||||||
logStream.write(new TextEncoder().encode(message + "\n"));
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log = redirectConsole(console.log);
|
|
||||||
console.error = redirectConsole(console.error);
|
|
||||||
console.warn = redirectConsole(console.warn);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRateLimited(): boolean {
|
|
||||||
const now = Date.now();
|
|
||||||
return rateLimits.some(({ window, maxRequests }) => {
|
|
||||||
const windowStart = now - window;
|
|
||||||
const requestsInWindow = requestQueue.filter((timestamp) => timestamp >= windowStart).length;
|
|
||||||
return requestsInWindow >= maxRequests;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readFromText() {
|
|
||||||
const aidRawcontent = await Deno.readTextFile(aidPath);
|
|
||||||
const aids = aidRawcontent
|
|
||||||
.split("\n")
|
|
||||||
.filter((line) => line.length > 0)
|
|
||||||
.map((line) => parseInt(line));
|
|
||||||
|
|
||||||
// if (!db.prepare("SELECT COUNT(*) FROM bili_info_crawl").get()) {
|
|
||||||
// const insertStmt = db.prepare("INSERT OR IGNORE INTO bili_info_crawl (aid, status) VALUES (?, 'pending')");
|
|
||||||
// aids.forEach((aid) => insertStmt.run(aid));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 查询数据库中已经存在的 aid
|
|
||||||
const existingAids = db
|
|
||||||
.prepare("SELECT aid FROM bili_info_crawl")
|
|
||||||
.all()
|
|
||||||
.map((row) => row.aid);
|
|
||||||
console.log(existingAids.length);
|
|
||||||
|
|
||||||
// 将 existingAids 转换为 Set 以提高查找效率
|
|
||||||
const existingAidsSet = new Set(existingAids);
|
|
||||||
|
|
||||||
// 找出 aids 数组中不存在于数据库的条目
|
|
||||||
const newAids = aids.filter((aid) => !existingAidsSet.has(aid));
|
|
||||||
|
|
||||||
// 插入这些新条目
|
|
||||||
const insertStmt = db.prepare(
|
|
||||||
"INSERT OR IGNORE INTO bili_info_crawl (aid, status) VALUES (?, 'pending')",
|
|
||||||
);
|
|
||||||
newAids.forEach((aid) => insertStmt.run(aid));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function insertAidsToDB() {
|
|
||||||
if (shouldReadTextFile) {
|
|
||||||
await readFromText();
|
|
||||||
}
|
|
||||||
|
|
||||||
const aidsInDB = db
|
|
||||||
.prepare(
|
|
||||||
"SELECT aid FROM bili_info_crawl WHERE status = 'pending' OR status = 'failed'",
|
|
||||||
)
|
|
||||||
.all()
|
|
||||||
.map((row) => row.aid) as number[];
|
|
||||||
|
|
||||||
const totalAids = aidsInDB.length;
|
|
||||||
let processedAids = 0;
|
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const processAid = async (aid: number) => {
|
|
||||||
try {
|
|
||||||
const res = await getBiliBiliVideoInfo(
|
|
||||||
aid,
|
|
||||||
regions[processedAids % regions.length],
|
|
||||||
);
|
|
||||||
if (res === null) {
|
|
||||||
updateAidStatus(aid, "failed");
|
|
||||||
} else {
|
|
||||||
const rawData = JSON.parse(res);
|
|
||||||
if (rawData.code === 0) {
|
|
||||||
updateAidStatus(
|
|
||||||
aid,
|
|
||||||
"success",
|
|
||||||
rawData.data.View.bvid,
|
|
||||||
JSON.stringify(rawData.data),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updateAidStatus(aid, "error", undefined, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error updating aid ${aid}: ${error}`);
|
|
||||||
updateAidStatus(aid, "failed");
|
|
||||||
} finally {
|
|
||||||
processedAids++;
|
|
||||||
logProgress(aid, processedAids, totalAids, startTime);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
|
||||||
if (aidsInDB.length === 0) {
|
|
||||||
clearInterval(interval);
|
|
||||||
console.log("All aids processed.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isRateLimited()) {
|
|
||||||
const aid = aidsInDB.shift();
|
|
||||||
if (aid !== undefined) {
|
|
||||||
requestQueue.push(Date.now());
|
|
||||||
await processAid(aid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
console.log("Starting to process aids...");
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAidStatus(
|
|
||||||
aid: number,
|
|
||||||
status: string,
|
|
||||||
bvid?: string,
|
|
||||||
data?: string,
|
|
||||||
) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
UPDATE bili_info_crawl
|
|
||||||
SET status = ?,
|
|
||||||
${bvid ? "bvid = ?," : ""}
|
|
||||||
${data ? "data = ?," : ""}
|
|
||||||
timestamp = ?
|
|
||||||
WHERE aid = ?
|
|
||||||
`);
|
|
||||||
const params = [
|
|
||||||
status,
|
|
||||||
...(bvid ? [bvid] : []),
|
|
||||||
...(data ? [data] : []),
|
|
||||||
Date.now() / 1000,
|
|
||||||
aid,
|
|
||||||
];
|
|
||||||
stmt.run(...params);
|
|
||||||
}
|
|
||||||
|
|
||||||
function logProgress(
|
|
||||||
aid: number,
|
|
||||||
processedAids: number,
|
|
||||||
totalAids: number,
|
|
||||||
startTime: number,
|
|
||||||
) {
|
|
||||||
const elapsedTime = Date.now() - startTime;
|
|
||||||
const elapsedSeconds = Math.floor(elapsedTime / 1000);
|
|
||||||
const elapsedMinutes = Math.floor(elapsedSeconds / 60);
|
|
||||||
const elapsedHours = Math.floor(elapsedMinutes / 60);
|
|
||||||
|
|
||||||
const remainingAids = totalAids - processedAids;
|
|
||||||
const averageTimePerAid = elapsedTime / processedAids;
|
|
||||||
const eta = remainingAids * averageTimePerAid;
|
|
||||||
const etaSeconds = Math.floor(eta / 1000);
|
|
||||||
const etaMinutes = Math.floor(etaSeconds / 60);
|
|
||||||
const etaHours = Math.floor(etaMinutes / 60);
|
|
||||||
|
|
||||||
const progress = `${processedAids}/${totalAids}, ${
|
|
||||||
((processedAids / totalAids) * 100).toFixed(
|
|
||||||
2,
|
|
||||||
)
|
|
||||||
}%, elapsed ${elapsedHours.toString().padStart(2, "0")}:${(elapsedMinutes % 60).toString().padStart(2, "0")}:${
|
|
||||||
(
|
|
||||||
elapsedSeconds % 60
|
|
||||||
)
|
|
||||||
.toString()
|
|
||||||
.padStart(2, "0")
|
|
||||||
}, ETA ${etaHours}h${(etaMinutes % 60).toString().padStart(2, "0")}m`;
|
|
||||||
console.log(`Updated aid ${aid}, ${progress}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await setupLogging();
|
|
||||||
insertAidsToDB();
|
|
@ -1,60 +0,0 @@
|
|||||||
export async function getBiliBiliVideoInfo(
|
|
||||||
bvidORaid?: string | number,
|
|
||||||
region: string = "hangzhou",
|
|
||||||
) {
|
|
||||||
const bvid = typeof bvidORaid === "string" ? bvidORaid : undefined;
|
|
||||||
const aid = typeof bvidORaid === "number" ? bvidORaid : undefined;
|
|
||||||
|
|
||||||
const baseURL = "https://api.bilibili.com/x/web-interface/view/detail";
|
|
||||||
const urlObject = new URL(baseURL);
|
|
||||||
|
|
||||||
if (aid) {
|
|
||||||
urlObject.searchParams.append("aid", aid.toString());
|
|
||||||
const finalURL = urlObject.toString();
|
|
||||||
return await proxyRequestWithRegion(finalURL, region);
|
|
||||||
} else if (bvid) {
|
|
||||||
urlObject.searchParams.append("bvid", bvid);
|
|
||||||
const finalURL = urlObject.toString();
|
|
||||||
return await proxyRequestWithRegion(finalURL, region);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function proxyRequestWithRegion(
|
|
||||||
url: string,
|
|
||||||
region: string,
|
|
||||||
): Promise<any | null> {
|
|
||||||
const td = new TextDecoder();
|
|
||||||
// aliyun configure set --access-key-id $ALIYUN_AK --access-key-secret $ALIYUN_SK --region cn-shenzhen --profile CVSA-shenzhen --mode AK
|
|
||||||
const p = await new Deno.Command("aliyun", {
|
|
||||||
args: [
|
|
||||||
"fc",
|
|
||||||
"POST",
|
|
||||||
`/2023-03-30/functions/proxy-${region}/invocations`,
|
|
||||||
"--qualifier",
|
|
||||||
"LATEST",
|
|
||||||
"--header",
|
|
||||||
"Content-Type=application/json;x-fc-invocation-type=Sync;x-fc-log-type=None;",
|
|
||||||
"--body",
|
|
||||||
JSON.stringify({ url: url }),
|
|
||||||
"--profile",
|
|
||||||
`CVSA-${region}`,
|
|
||||||
],
|
|
||||||
}).output();
|
|
||||||
try {
|
|
||||||
const out = td.decode(p.stdout);
|
|
||||||
const rawData = JSON.parse(out);
|
|
||||||
if (rawData.statusCode !== 200) {
|
|
||||||
console.error(
|
|
||||||
`Error proxying request ${url} to ${region} , statusCode: ${rawData.statusCode}`,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return JSON.parse(rawData.body);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Error proxying request ${url} to ${region}: ${e}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
32
src/metadataArchive.ts
Normal file
32
src/metadataArchive.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import arg from "arg";
|
||||||
|
//import { getVideoDetails } from "@crawler/net/getVideoDetails";
|
||||||
|
import logger from "@core/log/logger";
|
||||||
|
|
||||||
|
const quit = (reason: string) => {
|
||||||
|
logger.error(reason);
|
||||||
|
process.exit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const args = arg({
|
||||||
|
"--aids": String // --port <number> or --port=<number>
|
||||||
|
});
|
||||||
|
|
||||||
|
const aidsFileName = args["--aids"];
|
||||||
|
|
||||||
|
if (!aidsFileName) {
|
||||||
|
quit("Missing --aids <file_path>");
|
||||||
|
}
|
||||||
|
|
||||||
|
const aidsFile = Bun.file(aidsFileName!);
|
||||||
|
const fileExists = await aidsFile.exists();
|
||||||
|
if (!fileExists) {
|
||||||
|
quit(`${aidsFile} does not exist.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const aidsText = await aidsFile.text();
|
||||||
|
const aids = aidsText
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => parseInt(line))
|
||||||
|
.filter((num) => !Number.isNaN(num));
|
||||||
|
|
||||||
|
logger.log(`Read ${aids.length} aids.`);
|
35
tsconfig.json
Normal file
35
tsconfig.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"include": ["**/*.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@core/*": ["./packages/core/*"],
|
||||||
|
"@crawler/*": ["./packages/crawler/*"]
|
||||||
|
},
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user