ref: format with biome
This commit is contained in:
parent
43b3d82b3f
commit
c55cfb36fc
@ -2,52 +2,52 @@ import "dotenv/config";
|
||||
|
||||
export const apps = [
|
||||
{
|
||||
cwd: "./packages/crawler",
|
||||
interpreter: "bun",
|
||||
name: "crawler-jobadder",
|
||||
script: "src/jobAdder.wrapper.ts",
|
||||
cwd: "./packages/crawler",
|
||||
interpreter: "bun",
|
||||
},
|
||||
{
|
||||
cwd: "./packages/crawler",
|
||||
env: {
|
||||
LOG_ERR: "logs/error.log",
|
||||
LOG_VERBOSE: "logs/verbose.log",
|
||||
LOG_WARN: "logs/warn.log",
|
||||
},
|
||||
interpreter: "bun",
|
||||
name: "crawler-worker",
|
||||
script: "src/worker.ts",
|
||||
cwd: "./packages/crawler",
|
||||
interpreter: "bun",
|
||||
env: {
|
||||
LOG_VERBOSE: "logs/verbose.log",
|
||||
LOG_WARN: "logs/warn.log",
|
||||
LOG_ERR: "logs/error.log",
|
||||
},
|
||||
},
|
||||
{
|
||||
cwd: "./packages/crawler",
|
||||
env: {
|
||||
LOG_ERR: "logs/error.log",
|
||||
LOG_VERBOSE: "logs/verbose.log",
|
||||
LOG_WARN: "logs/warn.log",
|
||||
},
|
||||
interpreter: "bun",
|
||||
name: "crawler-filter",
|
||||
script: "src/filterWorker.wrapper.ts",
|
||||
cwd: "./packages/crawler",
|
||||
interpreter: "bun",
|
||||
env: {
|
||||
LOG_VERBOSE: "logs/verbose.log",
|
||||
LOG_WARN: "logs/warn.log",
|
||||
LOG_ERR: "logs/error.log",
|
||||
},
|
||||
},
|
||||
{
|
||||
cwd: "./ml/api",
|
||||
env: {
|
||||
LOG_ERR: "logs/error.log",
|
||||
LOG_VERBOSE: "logs/verbose.log",
|
||||
LOG_WARN: "logs/warn.log",
|
||||
PYTHONPATH: "./ml/api:./ml/filter",
|
||||
},
|
||||
interpreter: process.env.PYTHON_INTERPRETER || "python3",
|
||||
name: "ml-api",
|
||||
script: "start.py",
|
||||
cwd: "./ml/api",
|
||||
interpreter: process.env.PYTHON_INTERPRETER || "python3",
|
||||
env: {
|
||||
PYTHONPATH: "./ml/api:./ml/filter",
|
||||
LOG_VERBOSE: "logs/verbose.log",
|
||||
LOG_WARN: "logs/warn.log",
|
||||
LOG_ERR: "logs/error.log",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cvsa-be",
|
||||
script: "src/index.ts",
|
||||
cwd: "./packages/backend",
|
||||
interpreter: "bun",
|
||||
env: {
|
||||
NODE_ENV: "production",
|
||||
},
|
||||
interpreter: "bun",
|
||||
name: "cvsa-be",
|
||||
script: "src/index.ts",
|
||||
},
|
||||
];
|
||||
|
||||
@ -31,12 +31,12 @@ export async function verifyUser(
|
||||
}
|
||||
|
||||
return {
|
||||
createdAt: foundUser.createdAt,
|
||||
id: foundUser.id,
|
||||
username: foundUser.username,
|
||||
nickname: foundUser.nickname,
|
||||
role: foundUser.role,
|
||||
unqId: foundUser.unqId,
|
||||
createdAt: foundUser.createdAt,
|
||||
username: foundUser.username,
|
||||
};
|
||||
}
|
||||
|
||||
@ -52,12 +52,12 @@ export async function createSession(
|
||||
|
||||
try {
|
||||
await db.insert(loginSessionsInCredentials).values({
|
||||
id: sessionId,
|
||||
uid: userId,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
expireAt: expireAt.toISOString(),
|
||||
id: sessionId,
|
||||
ipAddress,
|
||||
lastUsedAt: new Date().toISOString(),
|
||||
uid: userId,
|
||||
userAgent,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error as Error);
|
||||
@ -107,8 +107,8 @@ export async function validateSession(
|
||||
.where(eq(loginSessionsInCredentials.id, sessionId));
|
||||
|
||||
return {
|
||||
user: users[0],
|
||||
session: session,
|
||||
user: users[0],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -38,13 +38,13 @@ const avSchema = z.string().regex(AV_REGEX);
|
||||
export function detectBiliID(id: string) {
|
||||
if (bvSchema.safeParse(id).success) {
|
||||
return {
|
||||
type: "bv" as const,
|
||||
id: id as `BV1${string}`,
|
||||
type: "bv" as const,
|
||||
};
|
||||
} else if (avSchema.safeParse(id).success) {
|
||||
return {
|
||||
type: "av" as const,
|
||||
id: id as `av${string}`,
|
||||
type: "av" as const,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -2,69 +2,69 @@ import { z } from "zod";
|
||||
|
||||
const videoStatsSchema = z.object({
|
||||
aid: z.number(),
|
||||
view: z.number(),
|
||||
danmaku: z.number(),
|
||||
reply: z.number(),
|
||||
favorite: z.number(),
|
||||
coin: z.number(),
|
||||
share: z.number(),
|
||||
now_rank: z.number(),
|
||||
danmaku: z.number(),
|
||||
favorite: z.number(),
|
||||
his_rank: z.number(),
|
||||
like: z.number(),
|
||||
now_rank: z.number(),
|
||||
reply: z.number(),
|
||||
share: z.number(),
|
||||
view: z.number(),
|
||||
});
|
||||
|
||||
export const BiliAPIVideoMetadataSchema = z.object({
|
||||
bvid: z.string(),
|
||||
aid: z.number(),
|
||||
bvid: z.string(),
|
||||
copyright: z.number(),
|
||||
pic: z.string(),
|
||||
title: z.string(),
|
||||
pubdate: z.number(),
|
||||
ctime: z.number(),
|
||||
desc: z.string(),
|
||||
desc_v2: z.string(),
|
||||
tname: z.string(),
|
||||
tid: z.number(),
|
||||
tid_v2: z.number(),
|
||||
tname_v2: z.string(),
|
||||
state: z.number(),
|
||||
duration: z.number(),
|
||||
owner: z.object({
|
||||
face: z.string(),
|
||||
mid: z.number(),
|
||||
name: z.string(),
|
||||
face: z.string(),
|
||||
}),
|
||||
pic: z.string(),
|
||||
pubdate: z.number(),
|
||||
stat: videoStatsSchema,
|
||||
state: z.number(),
|
||||
tid: z.number(),
|
||||
tid_v2: z.number(),
|
||||
title: z.string(),
|
||||
tname: z.string(),
|
||||
tname_v2: z.string(),
|
||||
});
|
||||
|
||||
export const BiliVideoSchema = z.object({
|
||||
duration: z.number().nullable(),
|
||||
id: z.number(),
|
||||
aid: z.number(),
|
||||
publishedAt: z.string().nullable(),
|
||||
bvid: z.string().nullable(),
|
||||
coverUrl: z.string().nullable(),
|
||||
createdAt: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
bvid: z.string().nullable(),
|
||||
uid: z.number().nullable(),
|
||||
duration: z.number().nullable(),
|
||||
id: z.number(),
|
||||
publishedAt: z.string().nullable(),
|
||||
status: z.number(),
|
||||
tags: z.string().nullable(),
|
||||
title: z.string().nullable(),
|
||||
status: z.number(),
|
||||
coverUrl: z.string().nullable(),
|
||||
uid: z.number().nullable(),
|
||||
});
|
||||
|
||||
export type BiliVideoType = z.infer<typeof BiliVideoSchema>;
|
||||
|
||||
export const SongSchema = z.object({
|
||||
duration: z.number().nullable(),
|
||||
name: z.string().nullable(),
|
||||
id: z.number(),
|
||||
aid: z.number().nullable(),
|
||||
createdAt: z.string(),
|
||||
deleted: z.boolean(),
|
||||
duration: z.number().nullable(),
|
||||
id: z.number(),
|
||||
image: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
neteaseId: z.number().nullable(),
|
||||
producer: z.string().nullable(),
|
||||
publishedAt: z.string().nullable(),
|
||||
type: z.number().nullable(),
|
||||
neteaseId: z.number().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
deleted: z.boolean(),
|
||||
image: z.string().nullable(),
|
||||
producer: z.string().nullable(),
|
||||
});
|
||||
|
||||
@ -7,100 +7,100 @@ export interface Singer {
|
||||
|
||||
export const singers: Singer[] = [
|
||||
{
|
||||
name: "洛天依",
|
||||
color: "#66CCFF",
|
||||
birthday: "0712",
|
||||
color: "#66CCFF",
|
||||
name: "洛天依",
|
||||
},
|
||||
{
|
||||
name: "言和",
|
||||
color: "#00FFCC",
|
||||
birthday: "0711",
|
||||
color: "#00FFCC",
|
||||
name: "言和",
|
||||
},
|
||||
{
|
||||
name: "乐正绫",
|
||||
color: "#EE0000",
|
||||
birthday: "0412",
|
||||
color: "#EE0000",
|
||||
name: "乐正绫",
|
||||
},
|
||||
{
|
||||
name: "乐正龙牙",
|
||||
color: "#006666",
|
||||
birthday: "1002",
|
||||
color: "#006666",
|
||||
name: "乐正龙牙",
|
||||
},
|
||||
{
|
||||
name: "徵羽摩柯",
|
||||
color: "#0080FF",
|
||||
birthday: "1210",
|
||||
color: "#0080FF",
|
||||
name: "徵羽摩柯",
|
||||
},
|
||||
{
|
||||
name: "墨清弦",
|
||||
birthday: "0520",
|
||||
color: "#FFFF00",
|
||||
birthday: "0520",
|
||||
name: "墨清弦",
|
||||
},
|
||||
{
|
||||
name: "星尘",
|
||||
color: "#9999FF",
|
||||
birthday: "0812",
|
||||
color: "#9999FF",
|
||||
name: "星尘",
|
||||
},
|
||||
{
|
||||
name: "永夜Minus",
|
||||
color: "#613c8a",
|
||||
birthday: "1208",
|
||||
color: "#613c8a",
|
||||
name: "永夜Minus",
|
||||
},
|
||||
{
|
||||
name: "心华",
|
||||
color: "#EE82EE",
|
||||
birthday: "0210",
|
||||
color: "#EE82EE",
|
||||
name: "心华",
|
||||
},
|
||||
{
|
||||
name: "海伊",
|
||||
color: "#3399FF",
|
||||
birthday: "0722",
|
||||
color: "#3399FF",
|
||||
name: "海伊",
|
||||
},
|
||||
{
|
||||
name: "苍穹",
|
||||
color: "#8BC0B5",
|
||||
birthday: "0520",
|
||||
color: "#8BC0B5",
|
||||
name: "苍穹",
|
||||
},
|
||||
{
|
||||
name: "赤羽",
|
||||
color: "#FF4004",
|
||||
birthday: "1126",
|
||||
color: "#FF4004",
|
||||
name: "赤羽",
|
||||
},
|
||||
{
|
||||
name: "诗岸",
|
||||
color: "#F6BE72",
|
||||
birthday: "0119",
|
||||
color: "#F6BE72",
|
||||
name: "诗岸",
|
||||
},
|
||||
{
|
||||
name: "牧心",
|
||||
color: "#2A2859",
|
||||
birthday: "0807",
|
||||
color: "#2A2859",
|
||||
name: "牧心",
|
||||
},
|
||||
{
|
||||
name: "起礼",
|
||||
birthday: "0713",
|
||||
color: "#FF0099",
|
||||
birthday: "0713",
|
||||
name: "起礼",
|
||||
},
|
||||
{
|
||||
name: "起复",
|
||||
birthday: "0713",
|
||||
color: "#99FF00",
|
||||
birthday: "0713",
|
||||
name: "起复",
|
||||
},
|
||||
{
|
||||
name: "夏语遥",
|
||||
color: "#34CCCC",
|
||||
birthday: "1110",
|
||||
color: "#34CCCC",
|
||||
name: "夏语遥",
|
||||
},
|
||||
];
|
||||
|
||||
export const specialSingers = [
|
||||
{
|
||||
name: "雅音宫羽",
|
||||
message: "你是我最真模样,从来不曾遗忘。",
|
||||
name: "雅音宫羽",
|
||||
},
|
||||
{
|
||||
name: "初音未来",
|
||||
message: "初始之音,响彻未来!",
|
||||
name: "初音未来",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -40,9 +40,9 @@ export const requireAuth = new Elysia({ name: "require-auth" })
|
||||
if (!sessionId) {
|
||||
set.status = 401;
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
isAuthenticated: false,
|
||||
session: null,
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -52,17 +52,17 @@ export const requireAuth = new Elysia({ name: "require-auth" })
|
||||
if (!validationResult) {
|
||||
set.status = 401;
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
isAuthenticated: false,
|
||||
session: null,
|
||||
user: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Session is valid, return user and session context
|
||||
return {
|
||||
user: validationResult.user,
|
||||
session: validationResult.session,
|
||||
isAuthenticated: true,
|
||||
session: validationResult.session,
|
||||
user: validationResult.user,
|
||||
};
|
||||
})
|
||||
.onBeforeHandle({ as: "scoped" }, ({ user, status }) => {
|
||||
|
||||
@ -14,8 +14,8 @@ export const openAPIMiddleware = openapi({
|
||||
},
|
||||
references: fromTypes(),
|
||||
scalar: {
|
||||
theme: "kepler",
|
||||
hideClientButton: true,
|
||||
hideDarkModeToggle: true,
|
||||
theme: "kepler",
|
||||
},
|
||||
});
|
||||
|
||||
@ -13,8 +13,8 @@ class TimeLogger {
|
||||
|
||||
getCompletedDurations() {
|
||||
return Array.from(this.durations.entries()).map(([name, duration]) => ({
|
||||
name,
|
||||
duration,
|
||||
name,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -25,41 +25,41 @@ export const loginHandler = new Elysia({ prefix: "/auth" }).use(ip()).post(
|
||||
|
||||
return {
|
||||
message: "You are logged in.",
|
||||
sessionID: sessionId,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
role: user.role,
|
||||
username: user.username,
|
||||
},
|
||||
sessionID: sessionId,
|
||||
};
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
password: t.String(),
|
||||
username: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
description:
|
||||
"This endpoint authenticates users by verifying their credentials and creates a new session. \
|
||||
Upon successful authentication, it returns user information and sets a secure HTTP-only cookie \
|
||||
for session management. The session includes IP address and user agent tracking for security purposes.",
|
||||
summary: "User login",
|
||||
},
|
||||
response: {
|
||||
200: t.Object({
|
||||
message: t.String(),
|
||||
sessionID: t.String(),
|
||||
user: t.Object({
|
||||
id: t.Integer(),
|
||||
username: t.String(),
|
||||
nickname: t.Optional(t.String()),
|
||||
role: t.String(),
|
||||
username: t.String(),
|
||||
}),
|
||||
sessionID: t.String(),
|
||||
}),
|
||||
401: t.Object({
|
||||
message: t.String(),
|
||||
}),
|
||||
},
|
||||
body: t.Object({
|
||||
username: t.String(),
|
||||
password: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "User login",
|
||||
description:
|
||||
"This endpoint authenticates users by verifying their credentials and creates a new session. \
|
||||
Upon successful authentication, it returns user information and sets a secure HTTP-only cookie \
|
||||
for session management. The session includes IP address and user agent tracking for security purposes.",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -18,6 +18,13 @@ export const logoutHandler = new Elysia({ prefix: "/auth" }).use(requireAuth).de
|
||||
return { message: "Successfully logged out." };
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
description:
|
||||
"This endpoint logs out the current user by deactivating their session and removing the session cookie. \
|
||||
It requires an active session cookie to be present in the request. After successful logout, the session \
|
||||
is invalidated and cannot be used again.",
|
||||
summary: "Logout current session",
|
||||
},
|
||||
response: {
|
||||
200: t.Object({
|
||||
message: t.String(),
|
||||
@ -26,12 +33,5 @@ export const logoutHandler = new Elysia({ prefix: "/auth" }).use(requireAuth).de
|
||||
message: t.String(),
|
||||
}),
|
||||
},
|
||||
detail: {
|
||||
summary: "Logout current session",
|
||||
description:
|
||||
"This endpoint logs out the current user by deactivating their session and removing the session cookie. \
|
||||
It requires an active session cookie to be present in the request. After successful logout, the session \
|
||||
is invalidated and cannot be used again.",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -9,18 +9,18 @@ export const getCurrentUserHandler = new Elysia().use(requireAuth).get(
|
||||
}
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
nickname: user.nickname,
|
||||
role: user.role,
|
||||
username: user.username,
|
||||
};
|
||||
},
|
||||
{
|
||||
response: {
|
||||
200: t.Object({
|
||||
id: t.Integer(),
|
||||
username: t.String(),
|
||||
nickname: t.Union([t.String(), t.Null()]),
|
||||
role: t.String(),
|
||||
username: t.String(),
|
||||
}),
|
||||
401: t.Object({
|
||||
message: t.String(),
|
||||
|
||||
@ -8,42 +8,42 @@ export const pingHandler = new Elysia({ prefix: "/ping" }).use(ip()).get(
|
||||
return {
|
||||
message: "pong",
|
||||
request: {
|
||||
body: body,
|
||||
headers: headers,
|
||||
ip: ip,
|
||||
method: request.method,
|
||||
body: body,
|
||||
url: request.url,
|
||||
},
|
||||
response: {
|
||||
time: Date.now(),
|
||||
status: 200,
|
||||
time: Date.now(),
|
||||
version: VERSION,
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
body: t.Optional(t.String()),
|
||||
detail: {
|
||||
description:
|
||||
"This endpoint returns a 'pong' message along with comprehensive information about the incoming request and the server's current status, including request headers, IP address, and server version. It's useful for monitoring API availability and debugging.",
|
||||
summary: "Send a ping",
|
||||
},
|
||||
response: {
|
||||
200: t.Object({
|
||||
message: t.String(),
|
||||
request: t.Object({
|
||||
body: t.Optional(t.Union([t.String(), t.Null()])),
|
||||
headers: t.Any(),
|
||||
ip: t.Optional(t.String()),
|
||||
method: t.String(),
|
||||
body: t.Optional(t.Union([t.String(), t.Null()])),
|
||||
url: t.String(),
|
||||
}),
|
||||
response: t.Object({
|
||||
time: t.Number(),
|
||||
status: t.Number(),
|
||||
time: t.Number(),
|
||||
version: t.String(),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
body: t.Optional(t.String()),
|
||||
detail: {
|
||||
summary: "Send a ping",
|
||||
description:
|
||||
"This endpoint returns a 'pong' message along with comprehensive information about the incoming request and the server's current status, including request headers, IP address, and server version. It's useful for monitoring API availability and debugging.",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -8,10 +8,10 @@ import { VERSION } from "@backend/src";
|
||||
import { Elysia, t } from "elysia";
|
||||
|
||||
const SingerObj = t.Object({
|
||||
name: t.String(),
|
||||
color: t.Optional(t.String()),
|
||||
birthday: t.Optional(t.String()),
|
||||
color: t.Optional(t.String()),
|
||||
message: t.Optional(t.String()),
|
||||
name: t.String(),
|
||||
});
|
||||
|
||||
export const rootHandler = new Elysia().get(
|
||||
@ -32,35 +32,35 @@ export const rootHandler = new Elysia().get(
|
||||
}
|
||||
return {
|
||||
project: {
|
||||
name: "中V档案馆",
|
||||
mascot: "知夏",
|
||||
name: "中V档案馆",
|
||||
quote: "星河知海夏生光",
|
||||
},
|
||||
status: 200,
|
||||
version: VERSION,
|
||||
time: Date.now(),
|
||||
singer: singer,
|
||||
status: 200,
|
||||
time: Date.now(),
|
||||
version: VERSION,
|
||||
};
|
||||
},
|
||||
{
|
||||
response: {
|
||||
200: t.Object({
|
||||
project: t.Object({
|
||||
name: t.String(),
|
||||
mascot: t.String(),
|
||||
quote: t.String(),
|
||||
}),
|
||||
status: t.Number(),
|
||||
version: t.String(),
|
||||
time: t.Number(),
|
||||
singer: t.Union([SingerObj, t.Array(SingerObj)]),
|
||||
}),
|
||||
},
|
||||
detail: {
|
||||
summary: "Root route",
|
||||
description:
|
||||
"The root path. It returns a JSON object containing a random virtual singer, \
|
||||
backend version, current server time and other miscellaneous information.",
|
||||
summary: "Root route",
|
||||
},
|
||||
response: {
|
||||
200: t.Object({
|
||||
project: t.Object({
|
||||
mascot: t.String(),
|
||||
name: t.String(),
|
||||
quote: t.String(),
|
||||
}),
|
||||
singer: t.Union([SingerObj, t.Array(SingerObj)]),
|
||||
status: t.Number(),
|
||||
time: t.Number(),
|
||||
version: t.String(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -23,11 +23,11 @@ const getSongSearchResult = async (searchQuery: string) => {
|
||||
const lengthRatio = searchQuery.length / song.songs.name.length;
|
||||
const viewsLog = Math.log10(song.latest_video_snapshot.views + 1);
|
||||
return {
|
||||
type: "song" as "song",
|
||||
data: song,
|
||||
occurrences,
|
||||
viewsLog,
|
||||
lengthRatio,
|
||||
occurrences,
|
||||
type: "song" as "song",
|
||||
viewsLog,
|
||||
};
|
||||
})
|
||||
.filter((d) => d !== null);
|
||||
@ -52,9 +52,9 @@ const getSongSearchResult = async (searchQuery: string) => {
|
||||
normalizedOccurrences * 0.3 + result.lengthRatio * 0.5 + normalizedViewsLog * 0.2;
|
||||
|
||||
return {
|
||||
type: result.type,
|
||||
data: result.data.songs,
|
||||
rank: Math.min(Math.max(rank, 0), 1), // Ensure rank is between 0 and 1
|
||||
type: result.type,
|
||||
};
|
||||
});
|
||||
|
||||
@ -70,9 +70,9 @@ const getDBVideoSearchResult = async (searchQuery: string) => {
|
||||
.innerJoin(latestVideoSnapshot, eq(bilibiliMetadata.aid, latestVideoSnapshot.aid))
|
||||
.where(eq(bilibiliMetadata.aid, aid));
|
||||
return results.map((video) => ({
|
||||
type: "bili-video-db" as "bili-video-db",
|
||||
data: { views: video.latest_video_snapshot.views, ...video.bilibili_metadata },
|
||||
rank: 1, // Exact match
|
||||
type: "bili-video-db" as "bili-video-db",
|
||||
}));
|
||||
};
|
||||
|
||||
@ -92,9 +92,9 @@ const getVideoSearchResult = async (searchQuery: string) => {
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: "bili-video" as "bili-video",
|
||||
data: data,
|
||||
rank: 0.99, // Exact match
|
||||
type: "bili-video" as "bili-video",
|
||||
},
|
||||
];
|
||||
};
|
||||
@ -123,43 +123,43 @@ export const searchHandler = new Elysia({ prefix: "/search" }).get(
|
||||
};
|
||||
},
|
||||
{
|
||||
response: {
|
||||
200: z.object({
|
||||
elapsedMs: z.number(),
|
||||
data: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal("song"),
|
||||
data: SongSchema,
|
||||
rank: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("bili-video-db"),
|
||||
data: BiliVideoDataSchema,
|
||||
rank: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal("bili-video"),
|
||||
data: BiliAPIVideoMetadataSchema,
|
||||
rank: z.number(),
|
||||
}),
|
||||
])
|
||||
),
|
||||
}),
|
||||
404: z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
},
|
||||
query: z.object({
|
||||
query: z.string(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Search songs and videos",
|
||||
description:
|
||||
"This endpoint performs a comprehensive search across songs and videos in the database. \
|
||||
It searches for songs by name and videos by bilibili ID (av/BV format). The results are ranked \
|
||||
by relevance using a weighted algorithm that considers search term frequency, title length, \
|
||||
and view count. Returns search results with performance timing information.",
|
||||
summary: "Search songs and videos",
|
||||
},
|
||||
query: z.object({
|
||||
query: z.string(),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
data: z.array(
|
||||
z.union([
|
||||
z.object({
|
||||
data: SongSchema,
|
||||
rank: z.number(),
|
||||
type: z.literal("song"),
|
||||
}),
|
||||
z.object({
|
||||
data: BiliVideoDataSchema,
|
||||
rank: z.number(),
|
||||
type: z.literal("bili-video-db"),
|
||||
}),
|
||||
z.object({
|
||||
data: BiliAPIVideoMetadataSchema,
|
||||
rank: z.number(),
|
||||
type: z.literal("bili-video"),
|
||||
}),
|
||||
])
|
||||
),
|
||||
elapsedMs: z.number(),
|
||||
}),
|
||||
404: z.object({
|
||||
message: z.string(),
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -40,15 +40,26 @@ export const addSongHandler = new Elysia()
|
||||
});
|
||||
}
|
||||
return status(201, {
|
||||
message: "Successfully created import session.",
|
||||
jobID: job.id,
|
||||
message: "Successfully created import session.",
|
||||
});
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
description:
|
||||
"This endpoint allows authenticated users to import a song from bilibili by providing a video ID. \
|
||||
The video ID can be in av or BV format. The system validates the ID format, checks if the video already \
|
||||
exists in the database, and if not, creates a background job to fetch video metadata and add it to the songs collection. \
|
||||
Returns the job ID for tracking the import progress.",
|
||||
summary: "Import song from bilibili",
|
||||
},
|
||||
response: {
|
||||
201: t.Object({
|
||||
message: t.String(),
|
||||
jobID: t.String(),
|
||||
message: t.String(),
|
||||
}),
|
||||
400: t.Object({
|
||||
message: t.String(),
|
||||
@ -60,17 +71,6 @@ export const addSongHandler = new Elysia()
|
||||
message: t.String(),
|
||||
}),
|
||||
},
|
||||
body: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Import song from bilibili",
|
||||
description:
|
||||
"This endpoint allows authenticated users to import a song from bilibili by providing a video ID. \
|
||||
The video ID can be in av or BV format. The system validates the ID format, checks if the video already \
|
||||
exists in the database, and if not, creates a background job to fetch video metadata and add it to the songs collection. \
|
||||
Returns the job ID for tracking the import progress.",
|
||||
},
|
||||
}
|
||||
)
|
||||
.get(
|
||||
@ -80,10 +80,10 @@ export const addSongHandler = new Elysia()
|
||||
if (parseInt(jobID) === -1) {
|
||||
return {
|
||||
id: jobID,
|
||||
state: "completed",
|
||||
result: {
|
||||
message: "Video already exists in the songs table.",
|
||||
},
|
||||
state: "completed",
|
||||
};
|
||||
}
|
||||
const job = await LatestVideosQueue.getJob(jobID);
|
||||
@ -94,33 +94,33 @@ export const addSongHandler = new Elysia()
|
||||
}
|
||||
const state = await job.getState();
|
||||
return {
|
||||
id: job.id!,
|
||||
state,
|
||||
result: job.returnvalue,
|
||||
failedReason: job.failedReason,
|
||||
id: job.id!,
|
||||
result: job.returnvalue,
|
||||
state,
|
||||
};
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
description:
|
||||
"This endpoint retrieves the current status of a song import job. It returns the job state \
|
||||
(completed, failed, active, etc.), the result if completed, and any failure reason if the job failed. \
|
||||
Use this endpoint to monitor the progress of song imports initiated through the import endpoint.",
|
||||
summary: "Check import job status",
|
||||
},
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
response: {
|
||||
200: t.Object({
|
||||
id: t.String(),
|
||||
state: t.String(),
|
||||
result: t.Optional(t.Any()),
|
||||
failedReason: t.Optional(t.String()),
|
||||
id: t.String(),
|
||||
result: t.Optional(t.Any()),
|
||||
state: t.String(),
|
||||
}),
|
||||
404: t.Object({
|
||||
message: t.String(),
|
||||
}),
|
||||
},
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Check import job status",
|
||||
description:
|
||||
"This endpoint retrieves the current status of a song import job. It returns the job state \
|
||||
(completed, failed, active, etc.), the result if completed, and any failure reason if the job failed. \
|
||||
Use this endpoint to monitor the progress of song imports initiated through the import endpoint.",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -9,16 +9,26 @@ export const deleteSongHandler = new Elysia({ prefix: "/song" }).use(requireAuth
|
||||
const id = Number(params.id);
|
||||
await db.update(songs).set({ deleted: true }).where(eq(songs.id, id));
|
||||
await db.insert(history).values({
|
||||
objectId: id,
|
||||
changeType: "del-song",
|
||||
changedBy: user!.unqId,
|
||||
changeType: "del-song",
|
||||
data: null,
|
||||
objectId: id,
|
||||
});
|
||||
return {
|
||||
message: `Successfully deleted song ${id}.`,
|
||||
};
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
description:
|
||||
"This endpoint allows authenticated users to soft-delete a song from the database. \
|
||||
The song is marked as deleted rather than being permanently removed, preserving data integrity. \
|
||||
The deletion is logged in the history table for audit purposes. Requires authentication and appropriate permissions.",
|
||||
summary: "Delete song",
|
||||
},
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
response: {
|
||||
200: t.Object({
|
||||
message: t.String(),
|
||||
@ -30,15 +40,5 @@ export const deleteSongHandler = new Elysia({ prefix: "/song" }).use(requireAuth
|
||||
message: t.String(),
|
||||
}),
|
||||
},
|
||||
params: t.Object({
|
||||
id: t.String(),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Delete song",
|
||||
description:
|
||||
"This endpoint allows authenticated users to soft-delete a song from the database. \
|
||||
The song is marked as deleted rather than being permanently removed, preserving data integrity. \
|
||||
The deletion is logged in the history table for audit purposes. Requires authentication and appropriate permissions.",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -31,37 +31,37 @@ export const closeMileStoneHandler = new Elysia({ prefix: "/songs" }).use(server
|
||||
return q.limit(limit || 20).offset(offset || 0);
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
description:
|
||||
"This endpoint retrieves songs that are approaching significant view count milestones. \
|
||||
It supports three milestone types: 'dendou' (0-100k views), 'densetsu' (100k-1M views), and 'shinwa' (1M-10M views). \
|
||||
For each type, it returns videos that are within the specified view range and have an estimated time to reach \
|
||||
the next milestone below the threshold. Results are ordered by estimated time to milestone.",
|
||||
summary: "Get songs close to milestones",
|
||||
},
|
||||
params: t.Object({
|
||||
type: t.String({ enum: ["dendou", "densetsu", "shinwa"] }),
|
||||
}),
|
||||
query: t.Object({
|
||||
limit: t.Optional(t.Number()),
|
||||
offset: t.Optional(t.Number()),
|
||||
}),
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
bilibili_metadata: BiliVideoSchema,
|
||||
eta: z.object({
|
||||
aid: z.number(),
|
||||
currentViews: z.number(),
|
||||
eta: z.number(),
|
||||
speed: z.number(),
|
||||
currentViews: z.number(),
|
||||
updatedAt: z.string(),
|
||||
}),
|
||||
bilibili_metadata: BiliVideoSchema,
|
||||
})
|
||||
),
|
||||
404: t.Object({
|
||||
message: t.String(),
|
||||
}),
|
||||
},
|
||||
params: t.Object({
|
||||
type: t.String({ enum: ["dendou", "densetsu", "shinwa"] }),
|
||||
}),
|
||||
query: t.Object({
|
||||
offset: t.Optional(t.Number()),
|
||||
limit: t.Optional(t.Number()),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Get songs close to milestones",
|
||||
description:
|
||||
"This endpoint retrieves songs that are approaching significant view count milestones. \
|
||||
It supports three milestone types: 'dendou' (0-100k views), 'densetsu' (100k-1M views), and 'shinwa' (1M-10M views). \
|
||||
For each type, it returns videos that are within the specified view range and have an estimated time to reach \
|
||||
the next milestone below the threshold. Results are ordered by estimated time to milestone.",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -25,19 +25,30 @@ export const songEtaHandler = new Elysia({ prefix: "/video" }).get(
|
||||
return {
|
||||
aid: data[0].aid,
|
||||
eta: data[0].eta,
|
||||
views: data[0].currentViews,
|
||||
speed: data[0].speed,
|
||||
updatedAt: data[0].updatedAt,
|
||||
views: data[0].currentViews,
|
||||
};
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
description:
|
||||
"This endpoint retrieves the estimated time to reach the next milestone for a given video. \
|
||||
It accepts video IDs in av or BV format and returns the current view count, estimated time to \
|
||||
reach the next milestone (in hours), view growth speed, and last update timestamp. Useful for \
|
||||
tracking video growth and milestone predictions.",
|
||||
summary: "Get video milestone ETA",
|
||||
},
|
||||
headers: t.Object({
|
||||
Authorization: t.Optional(t.String()),
|
||||
}),
|
||||
response: {
|
||||
200: t.Object({
|
||||
aid: t.Number(),
|
||||
eta: t.Number(),
|
||||
views: t.Number(),
|
||||
speed: t.Number(),
|
||||
updatedAt: t.String(),
|
||||
views: t.Number(),
|
||||
}),
|
||||
400: t.Object({
|
||||
code: t.String(),
|
||||
@ -48,16 +59,5 @@ export const songEtaHandler = new Elysia({ prefix: "/video" }).get(
|
||||
message: t.String(),
|
||||
}),
|
||||
},
|
||||
headers: t.Object({
|
||||
Authorization: t.Optional(t.String()),
|
||||
}),
|
||||
detail: {
|
||||
summary: "Get video milestone ETA",
|
||||
description:
|
||||
"This endpoint retrieves the estimated time to reach the next milestone for a given video. \
|
||||
It accepts video IDs in av or BV format and returns the current view count, estimated time to \
|
||||
reach the next milestone (in hours), view growth speed, and last update timestamp. Useful for \
|
||||
tracking video growth and milestone predictions.",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -11,12 +11,12 @@ const videoSchema = BiliVideoSchema.omit({ publishedAt: true })
|
||||
.omit({ createdAt: true })
|
||||
.omit({ coverUrl: true })
|
||||
.extend({
|
||||
views: z.number(),
|
||||
username: z.string(),
|
||||
uid: z.number(),
|
||||
published_at: z.string(),
|
||||
createdAt: z.string(),
|
||||
cover_url: z.string(),
|
||||
createdAt: z.string(),
|
||||
published_at: z.string(),
|
||||
uid: z.number(),
|
||||
username: z.string(),
|
||||
views: z.number(),
|
||||
});
|
||||
|
||||
export const getUnlabelledVideos = new Elysia({ prefix: "/videos" }).use(requireAuth).get(
|
||||
@ -75,9 +75,9 @@ export const postVideoLabel = new Elysia({ prefix: "/video" }).use(requireAuth).
|
||||
if (!aid) {
|
||||
return status(400, {
|
||||
code: "MALFORMED_SLOT",
|
||||
errors: [],
|
||||
message:
|
||||
"We cannot parse the video ID, or we currently do not support this format.",
|
||||
errors: [],
|
||||
});
|
||||
}
|
||||
|
||||
@ -90,8 +90,8 @@ export const postVideoLabel = new Elysia({ prefix: "/video" }).use(requireAuth).
|
||||
if (video.length === 0) {
|
||||
return status(400, {
|
||||
code: "VIDEO_NOT_FOUND",
|
||||
message: "Video not found",
|
||||
errors: [],
|
||||
message: "Video not found",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -34,13 +34,13 @@ async function insertVideoSnapshot(data: VideoInfoData) {
|
||||
|
||||
await db.insert(videoSnapshot).values({
|
||||
aid,
|
||||
views,
|
||||
danmakus,
|
||||
replies,
|
||||
likes,
|
||||
coins,
|
||||
shares,
|
||||
danmakus,
|
||||
favorites,
|
||||
likes,
|
||||
replies,
|
||||
shares,
|
||||
views,
|
||||
});
|
||||
snapshotCounter.add(1);
|
||||
}
|
||||
@ -54,9 +54,9 @@ export const getVideoMetadataHandler = new Elysia({ prefix: "/video" }).get(
|
||||
if (!aid) {
|
||||
return c.status(400, {
|
||||
code: "MALFORMED_SLOT",
|
||||
errors: [],
|
||||
message:
|
||||
"We cannot parse the video ID, or we currently do not support this format.",
|
||||
errors: [],
|
||||
});
|
||||
}
|
||||
|
||||
@ -70,8 +70,8 @@ export const getVideoMetadataHandler = new Elysia({ prefix: "/video" }).get(
|
||||
if (typeof r === "number") {
|
||||
return c.status(500, {
|
||||
code: "THIRD_PARTY_ERROR",
|
||||
message: `Got status code ${r} from bilibili API.`,
|
||||
errors: [],
|
||||
message: `Got status code ${r} from bilibili API.`,
|
||||
});
|
||||
}
|
||||
|
||||
@ -83,18 +83,18 @@ export const getVideoMetadataHandler = new Elysia({ prefix: "/video" }).get(
|
||||
return data;
|
||||
},
|
||||
{
|
||||
response: {
|
||||
200: BiliAPIVideoMetadataSchema,
|
||||
400: ErrorResponseSchema,
|
||||
500: ErrorResponseSchema,
|
||||
},
|
||||
detail: {
|
||||
summary: "Get video metadata",
|
||||
description:
|
||||
"This endpoint retrieves comprehensive metadata for a bilibili video. It accepts video IDs in av or BV format \
|
||||
and returns detailed information including title, description, uploader, statistics (views, likes, coins, etc.), \
|
||||
and publication date. The data is cached for 60 seconds to reduce API calls. If the video is not in cache, \
|
||||
it fetches fresh data from bilibili API and stores a snapshot in the database.",
|
||||
summary: "Get video metadata",
|
||||
},
|
||||
response: {
|
||||
200: BiliAPIVideoMetadataSchema,
|
||||
400: ErrorResponseSchema,
|
||||
500: ErrorResponseSchema,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -15,9 +15,9 @@ export const getVideoSnapshotsHandler = new Elysia({ prefix: "/video" }).get(
|
||||
if (!aid) {
|
||||
return c.status(400, {
|
||||
code: "MALFORMED_SLOT",
|
||||
errors: [],
|
||||
message:
|
||||
"We cannot parse the video ID, or we currently do not support this format.",
|
||||
errors: [],
|
||||
});
|
||||
}
|
||||
|
||||
@ -36,31 +36,31 @@ export const getVideoSnapshotsHandler = new Elysia({ prefix: "/video" }).get(
|
||||
return data;
|
||||
},
|
||||
{
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
createdAt: z.string(),
|
||||
views: z.number(),
|
||||
coins: z.number().nullable(),
|
||||
likes: z.number().nullable(),
|
||||
favorites: z.number().nullable(),
|
||||
shares: z.number().nullable(),
|
||||
danmakus: z.number().nullable(),
|
||||
aid: z.number(),
|
||||
replies: z.number().nullable(),
|
||||
})
|
||||
),
|
||||
400: ErrorResponseSchema,
|
||||
500: ErrorResponseSchema,
|
||||
},
|
||||
detail: {
|
||||
summary: "Get video snapshots",
|
||||
description:
|
||||
"This endpoint retrieves historical view count snapshots for a bilibili video. It accepts video IDs in av or BV format \
|
||||
and returns a chronological list of snapshots showing how the video's statistics (views, likes, coins, favorites, etc.) \
|
||||
have changed over time. If no snapshots exist for the video, it automatically queues a snapshot job to collect initial data. \
|
||||
Results are ordered by creation date in descending order.",
|
||||
summary: "Get video snapshots",
|
||||
},
|
||||
response: {
|
||||
200: z.array(
|
||||
z.object({
|
||||
aid: z.number(),
|
||||
coins: z.number().nullable(),
|
||||
createdAt: z.string(),
|
||||
danmakus: z.number().nullable(),
|
||||
favorites: z.number().nullable(),
|
||||
id: z.number(),
|
||||
likes: z.number().nullable(),
|
||||
replies: z.number().nullable(),
|
||||
shares: z.number().nullable(),
|
||||
views: z.number(),
|
||||
})
|
||||
),
|
||||
400: ErrorResponseSchema,
|
||||
500: ErrorResponseSchema,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@ -12,10 +12,10 @@ queueEvents.on<CustomListener>(
|
||||
"addSong",
|
||||
async ({ uid, songID }: { uid: string; songID: number }) => {
|
||||
await db.insert(history).values({
|
||||
objectId: songID,
|
||||
changeType: "add-song",
|
||||
changedBy: uid,
|
||||
changeType: "add-song",
|
||||
data: null,
|
||||
objectId: songID,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@ -24,10 +24,10 @@ export const onAfterHandler = new Elysia().onAfterHandle(
|
||||
? JSON.stringify(realResponse.response, null, 2)
|
||||
: JSON.stringify(realResponse.response);
|
||||
return new Response(encoder.encode(text), {
|
||||
status: realResponse.code as any,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
status: realResponse.code as any,
|
||||
});
|
||||
}
|
||||
const text = isBrowser
|
||||
|
||||
@ -28,7 +28,6 @@ function generateErrorCodeRegex(strings: string[]): string {
|
||||
|
||||
export const ErrorResponseSchema = t.Object({
|
||||
code: t.String({ pattern: generateErrorCodeRegex(errorCodes) }),
|
||||
message: t.String(),
|
||||
errors: t.Array(t.String()),
|
||||
i18n: t.Optional(
|
||||
t.Object({
|
||||
@ -36,4 +35,5 @@ export const ErrorResponseSchema = t.Object({
|
||||
values: t.Optional(t.Record(t.String(), t.Union([t.String(), t.Number(), t.Date()]))),
|
||||
})
|
||||
),
|
||||
message: t.String(),
|
||||
});
|
||||
|
||||
@ -166,15 +166,18 @@ async function handleFetch(
|
||||
}
|
||||
|
||||
function createJsonResponse(data: ProxyResponseData, requestId: string): Response {
|
||||
return new Response(JSON.stringify({
|
||||
...data,
|
||||
requestId,
|
||||
}), {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
...data,
|
||||
requestId,
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createErrorResponse(message: string, status: number, requestId: string): Response {
|
||||
@ -182,8 +185,8 @@ function createErrorResponse(message: string, status: number, requestId: string)
|
||||
JSON.stringify({
|
||||
data: "",
|
||||
error: message,
|
||||
time: Date.now(),
|
||||
requestId,
|
||||
time: Date.now(),
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
|
||||
14
packages/cf-worker/worker-configuration.d.ts
vendored
14
packages/cf-worker/worker-configuration.d.ts
vendored
@ -5,7 +5,7 @@ declare namespace Cloudflare {
|
||||
interface GlobalProps {
|
||||
mainModule: typeof import("./src/index");
|
||||
}
|
||||
interface Env {}
|
||||
type Env = {};
|
||||
}
|
||||
interface Env extends Cloudflare.Env {}
|
||||
|
||||
@ -464,7 +464,7 @@ declare const performance: Performance;
|
||||
declare const Cloudflare: Cloudflare;
|
||||
declare const origin: string;
|
||||
declare const navigator: Navigator;
|
||||
interface TestController {}
|
||||
type TestController = {};
|
||||
interface ExecutionContext<Props = unknown> {
|
||||
waitUntil(promise: Promise<any>): void;
|
||||
passThroughOnException(): void;
|
||||
@ -590,7 +590,7 @@ type DurableObjectLocationHint =
|
||||
interface DurableObjectNamespaceGetDurableObjectOptions {
|
||||
locationHint?: DurableObjectLocationHint;
|
||||
}
|
||||
interface DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> {}
|
||||
type DurableObjectClass<_T extends Rpc.DurableObjectBranded | undefined = undefined> = {};
|
||||
interface DurableObjectState<Props = unknown> {
|
||||
waitUntil(promise: Promise<any>): void;
|
||||
readonly exports: Cloudflare.Exports;
|
||||
@ -2933,7 +2933,7 @@ interface TraceItem {
|
||||
interface TraceItemAlarmEventInfo {
|
||||
readonly scheduledTime: Date;
|
||||
}
|
||||
interface TraceItemCustomEventInfo {}
|
||||
type TraceItemCustomEventInfo = {};
|
||||
interface TraceItemScheduledEventInfo {
|
||||
readonly scheduledTime: number;
|
||||
readonly cron: string;
|
||||
@ -10460,7 +10460,7 @@ declare abstract class D1PreparedStatement {
|
||||
// but this will ensure type checking on older versions still passes.
|
||||
// TypeScript's interface merging will ensure our empty interface is effectively
|
||||
// ignored when `Disposable` is included in the standard lib.
|
||||
interface Disposable {}
|
||||
type Disposable = {};
|
||||
/**
|
||||
* An email message that can be sent from a Worker.
|
||||
*/
|
||||
@ -11166,7 +11166,7 @@ declare namespace Cloudflare {
|
||||
// will merge all declarations.
|
||||
//
|
||||
// You can use `wrangler types` to generate the `Env` type automatically.
|
||||
interface Env {}
|
||||
type Env = {};
|
||||
// Project-specific parameters used to inform types.
|
||||
//
|
||||
// This interface is, again, intended to be declared in project-specific files, and then that
|
||||
@ -11185,7 +11185,7 @@ declare namespace Cloudflare {
|
||||
// }
|
||||
//
|
||||
// You can use `wrangler types` to generate `GlobalProps` automatically.
|
||||
interface GlobalProps {}
|
||||
type GlobalProps = {};
|
||||
// Evaluates to the type of a property in GlobalProps, defaulting to `Default` if it is not
|
||||
// present.
|
||||
type GlobalProp<K extends string, Default> = K extends keyof GlobalProps
|
||||
|
||||
@ -18,17 +18,17 @@ const databasePassword = getEnvVar("DB_PASSWORD")!;
|
||||
const databasePort = getEnvVar("DB_PORT")!;
|
||||
|
||||
export const postgresConfig = {
|
||||
host: databaseHost,
|
||||
port: parseInt(databasePort),
|
||||
database: databaseName,
|
||||
username: databaseUser,
|
||||
host: databaseHost,
|
||||
password: databasePassword,
|
||||
port: parseInt(databasePort),
|
||||
username: databaseUser,
|
||||
};
|
||||
|
||||
export const postgresConfigCred = {
|
||||
hostname: databaseHost,
|
||||
port: parseInt(databasePort),
|
||||
database: databaseNameCred,
|
||||
user: databaseUser,
|
||||
hostname: databaseHost,
|
||||
password: databasePassword,
|
||||
port: parseInt(databasePort),
|
||||
user: databaseUser,
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ const host = process.env.REDIS_HOST || "localhost";
|
||||
const port = parseInt(process.env.REDIS_PORT) || 6379;
|
||||
|
||||
export const redis = new Redis({
|
||||
port: port,
|
||||
host: host,
|
||||
maxRetriesPerRequest: null,
|
||||
port: port,
|
||||
});
|
||||
|
||||
@ -2,10 +2,10 @@ import "dotenv/config";
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./drizzle/main",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL_MAIN!,
|
||||
},
|
||||
dialect: "postgresql",
|
||||
out: "./drizzle/main",
|
||||
schemaFilter: ["public", "credentials", "internal"],
|
||||
});
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./cred",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL_CRED!,
|
||||
},
|
||||
dialect: "postgresql",
|
||||
out: "./cred",
|
||||
});
|
||||
|
||||
@ -6,9 +6,9 @@ if (!process.env.DATABASE_URL_MAIN) {
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
out: "./drizzle/main",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL_MAIN,
|
||||
},
|
||||
dialect: "postgresql",
|
||||
out: "./drizzle/main",
|
||||
});
|
||||
|
||||
@ -43,12 +43,12 @@ const createTransport = (level: string, filename: string) => {
|
||||
return value;
|
||||
}
|
||||
return new transports.File({
|
||||
level,
|
||||
filename,
|
||||
format: format.combine(timestampFormat, format.json({ replacer })),
|
||||
level,
|
||||
maxFiles,
|
||||
maxsize,
|
||||
tailable,
|
||||
maxFiles,
|
||||
format: format.combine(timestampFormat, format.json({ replacer })),
|
||||
});
|
||||
};
|
||||
|
||||
@ -60,13 +60,13 @@ const winstonLogger = winston.createLogger({
|
||||
levels: winston.config.npm.levels,
|
||||
transports: [
|
||||
new transports.Console({
|
||||
level: "debug",
|
||||
format: format.combine(
|
||||
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss.SSSZZ" }),
|
||||
format.colorize(),
|
||||
format.errors({ stack: true }),
|
||||
customFormat
|
||||
),
|
||||
level: "debug",
|
||||
}),
|
||||
createTransport("silly", sillyLogPath),
|
||||
createTransport("warn", warnLogPath),
|
||||
@ -75,28 +75,28 @@ const winstonLogger = winston.createLogger({
|
||||
});
|
||||
|
||||
const logger = {
|
||||
silly: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.silly(message, { service, codePath });
|
||||
},
|
||||
verbose: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.verbose(message, { service, codePath });
|
||||
},
|
||||
log: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.info(message, { service, codePath });
|
||||
},
|
||||
debug: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.debug(message, { service, codePath });
|
||||
},
|
||||
warn: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.warn(message, { service, codePath });
|
||||
winstonLogger.debug(message, { codePath, service });
|
||||
},
|
||||
error: (error: string | Error, service?: string, codePath?: string) => {
|
||||
if (error instanceof Error) {
|
||||
winstonLogger.error(error.message, { service, error: error, codePath });
|
||||
winstonLogger.error(error.message, { codePath, error: error, service });
|
||||
} else {
|
||||
winstonLogger.error(error, { service, codePath });
|
||||
winstonLogger.error(error, { codePath, service });
|
||||
}
|
||||
},
|
||||
log: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.info(message, { codePath, service });
|
||||
},
|
||||
silly: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.silly(message, { codePath, service });
|
||||
},
|
||||
verbose: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.verbose(message, { codePath, service });
|
||||
},
|
||||
warn: (message: string, service?: string, codePath?: string) => {
|
||||
winstonLogger.warn(message, { codePath, service });
|
||||
},
|
||||
};
|
||||
|
||||
export default logger;
|
||||
|
||||
@ -40,9 +40,9 @@ export class MultipleRateLimiter {
|
||||
const { duration, max } = this.configs[i];
|
||||
const { allowed } = await this.limiter.allow(`cvsa:${this.name}_${i}`, {
|
||||
burst: max,
|
||||
ratePerPeriod: max,
|
||||
period: duration,
|
||||
cost: 1,
|
||||
period: duration,
|
||||
ratePerPeriod: max,
|
||||
});
|
||||
if (!allowed && shouldThrow) {
|
||||
throw new RateLimiterError("Rate limit exceeded");
|
||||
|
||||
@ -17,14 +17,14 @@ export class BilibiliService {
|
||||
const stats = metadata.data.data.stat;
|
||||
return {
|
||||
aid,
|
||||
createdAt: new Date(metadata.time).toISOString(),
|
||||
views: stats.view,
|
||||
likes: stats.like,
|
||||
coins: stats.coin,
|
||||
createdAt: new Date(metadata.time).toISOString(),
|
||||
danmakus: stats.danmaku,
|
||||
favorites: stats.favorite,
|
||||
likes: stats.like,
|
||||
replies: stats.reply,
|
||||
shares: stats.share,
|
||||
danmakus: stats.danmaku,
|
||||
views: stats.view,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,9 +73,9 @@ export async function getVideoInfoFromAllData(aid: number) {
|
||||
|
||||
const row = rows[0];
|
||||
return {
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
tags: row.tags,
|
||||
title: row.title,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -31,10 +31,10 @@ export class APIManager {
|
||||
public async healthCheck(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/health`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
});
|
||||
|
||||
@ -57,19 +57,19 @@ export class APIManager {
|
||||
aid?: number
|
||||
): Promise<number> {
|
||||
const request: ClassificationRequest = {
|
||||
title: title.trim() || "untitled",
|
||||
aid: aid,
|
||||
description: description.trim() || "N/A",
|
||||
tags: tags.trim() || "empty",
|
||||
aid: aid,
|
||||
title: title.trim() || "untitled",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/classify`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(request),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
method: "POST",
|
||||
signal: AbortSignal.timeout(this.timeout),
|
||||
});
|
||||
|
||||
@ -98,11 +98,11 @@ export class APIManager {
|
||||
): Promise<Array<{ aid?: number; label: number; probabilities: number[] }>> {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/classify_batch`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requests),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requests),
|
||||
method: "POST",
|
||||
signal: AbortSignal.timeout(this.timeout * 2), // Longer timeout for batch
|
||||
});
|
||||
|
||||
|
||||
@ -61,8 +61,8 @@ export const archiveSnapshotsWorker = async (_job: Job) => {
|
||||
const now = Date.now();
|
||||
const targetTime = getRandomTimeInNextWeek();
|
||||
const interval = intervalToDuration({
|
||||
start: new Date(),
|
||||
end: new Date(targetTime),
|
||||
start: new Date(),
|
||||
});
|
||||
const formatted = formatDuration(interval, { format: ["days", "hours"] });
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { sql } from "@core/db/dbNew";
|
||||
import type { Job } from "bullmq";
|
||||
import { queueLatestVideos } from "mq/task/queueLatestVideo";
|
||||
|
||||
export const getLatestVideosWorker = async (_job: Job): Promise<void> => {
|
||||
await queueLatestVideos(sql);
|
||||
await queueLatestVideos();
|
||||
};
|
||||
|
||||
@ -22,8 +22,8 @@ interface AddSongEventPayload {
|
||||
const publishAddsongEvent = async (songID: number, uid: string) =>
|
||||
latestVideosEventsProducer.publishEvent<AddSongEventPayload>({
|
||||
eventName: "addSong",
|
||||
uid: uid,
|
||||
songID: songID,
|
||||
uid: uid,
|
||||
});
|
||||
|
||||
export const getVideoInfoWorker = async (job: Job<GetVideoInfoJobData>): Promise<void> => {
|
||||
@ -56,34 +56,34 @@ export const getVideoInfoWorker = async (job: Job<GetVideoInfoJobData>): Promise
|
||||
await insertIntoMetadata({
|
||||
aid,
|
||||
bvid: data.View.bvid,
|
||||
coverUrl: data.View.pic,
|
||||
description: data.View.desc,
|
||||
uid: uid,
|
||||
duration: data.View.duration,
|
||||
publishedAt: new Date(data.View.pubdate * 1000).toISOString(),
|
||||
tags: data.Tags.filter((tag) => !["old_channel", "topic"].indexOf(tag.tag_type))
|
||||
.map((tag) => tag.tag_name)
|
||||
.join(","),
|
||||
title: data.View.title,
|
||||
publishedAt: new Date(data.View.pubdate * 1000).toISOString(),
|
||||
duration: data.View.duration,
|
||||
coverUrl: data.View.pic,
|
||||
uid: uid,
|
||||
});
|
||||
|
||||
const userExists = await userExistsInBiliUsers(aid);
|
||||
if (!userExists) {
|
||||
await db.insert(bilibiliUser).values({
|
||||
uid,
|
||||
username: data.View.owner.name,
|
||||
avatar: data.View.owner.face,
|
||||
desc: data.Card.card.sign,
|
||||
fans: data.Card.follower,
|
||||
avatar: data.View.owner.face,
|
||||
uid,
|
||||
username: data.View.owner.name,
|
||||
});
|
||||
} else {
|
||||
await db
|
||||
.update(bilibiliUser)
|
||||
.set({
|
||||
username: data.View.owner.name,
|
||||
avatar: data.View.owner.face,
|
||||
desc: data.Card.card.sign,
|
||||
fans: data.Card.follower,
|
||||
avatar: data.View.owner.face,
|
||||
username: data.View.owner.name,
|
||||
})
|
||||
.where(eq(bilibiliUser.uid, uid));
|
||||
}
|
||||
@ -92,13 +92,13 @@ export const getVideoInfoWorker = async (job: Job<GetVideoInfoJobData>): Promise
|
||||
|
||||
await db.insert(videoSnapshot).values({
|
||||
aid,
|
||||
views: stat.view,
|
||||
danmakus: stat.danmaku,
|
||||
replies: stat.reply,
|
||||
likes: stat.like,
|
||||
coins: stat.coin,
|
||||
shares: stat.share,
|
||||
danmakus: stat.danmaku,
|
||||
favorites: stat.favorite,
|
||||
likes: stat.like,
|
||||
replies: stat.reply,
|
||||
shares: stat.share,
|
||||
views: stat.view,
|
||||
});
|
||||
|
||||
snapshotCounter.add(1);
|
||||
|
||||
@ -32,12 +32,12 @@ export const bulkSnapshotTickWorker = async (_job: Job) => {
|
||||
const schedulesData = group.map((schedule) => {
|
||||
return {
|
||||
aid: Number(schedule.aid),
|
||||
id: Number(schedule.id),
|
||||
type: schedule.type,
|
||||
created_at: schedule.created_at,
|
||||
started_at: schedule.started_at,
|
||||
finished_at: schedule.finished_at,
|
||||
id: Number(schedule.id),
|
||||
started_at: schedule.started_at,
|
||||
status: schedule.status,
|
||||
type: schedule.type,
|
||||
};
|
||||
});
|
||||
await SnapshotQueue.add(
|
||||
|
||||
@ -17,8 +17,8 @@ import { closetMilestone } from "./snapshotTick";
|
||||
|
||||
const snapshotTypeToTaskMap = {
|
||||
milestone: "snapshotMilestoneVideo",
|
||||
normal: "snapshotVideo",
|
||||
new: "snapshotMilestoneVideo",
|
||||
normal: "snapshotVideo",
|
||||
} as const;
|
||||
|
||||
export const snapshotVideoWorker = async (job: Job): Promise<void> => {
|
||||
|
||||
@ -44,28 +44,28 @@ export async function takeVideoSnapshot(
|
||||
const favorites = data.stat.favorite;
|
||||
|
||||
await insertVideoSnapshot({
|
||||
createdAt: new Date(time).toISOString(),
|
||||
views,
|
||||
coins,
|
||||
likes,
|
||||
favorites,
|
||||
shares,
|
||||
danmakus,
|
||||
replies,
|
||||
aid,
|
||||
coins,
|
||||
createdAt: new Date(time).toISOString(),
|
||||
danmakus,
|
||||
favorites,
|
||||
likes,
|
||||
replies,
|
||||
shares,
|
||||
views,
|
||||
});
|
||||
|
||||
logger.log(`Taken snapshot for video ${aid}.`, "net", "fn:insertVideoSnapshot");
|
||||
|
||||
return {
|
||||
aid,
|
||||
views,
|
||||
danmakus,
|
||||
replies,
|
||||
likes,
|
||||
coins,
|
||||
shares,
|
||||
danmakus,
|
||||
favorites,
|
||||
likes,
|
||||
replies,
|
||||
shares,
|
||||
time,
|
||||
views,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { Psql } from "@core/db/psql.d";
|
||||
import { SECOND } from "@core/lib";
|
||||
import logger from "@core/log";
|
||||
import { videoExistsInAllData } from "db/bilibili_metadata";
|
||||
@ -6,14 +5,14 @@ import { LatestVideosQueue } from "mq/index";
|
||||
import { getLatestVideoAids } from "net/getLatestVideoAids";
|
||||
import { sleep } from "utils/sleep";
|
||||
|
||||
export async function queueLatestVideos(sql: Psql): Promise<number | null> {
|
||||
export async function queueLatestVideos(): Promise<number | null> {
|
||||
let page = 1;
|
||||
let i = 0;
|
||||
const videosFound = new Set();
|
||||
while (true) {
|
||||
const pageSize = page == 1 ? 10 : 30;
|
||||
const pageSize = page === 1 ? 10 : 30;
|
||||
const aids = await getLatestVideoAids(page, pageSize);
|
||||
if (aids.length == 0) {
|
||||
if (aids.length === 0) {
|
||||
logger.verbose("No more videos found", "net", "fn:insertLatestVideos()");
|
||||
break;
|
||||
}
|
||||
@ -28,12 +27,12 @@ export async function queueLatestVideos(sql: Psql): Promise<number | null> {
|
||||
"getVideoInfo",
|
||||
{ aid },
|
||||
{
|
||||
delay,
|
||||
attempts: 100,
|
||||
backoff: {
|
||||
type: "fixed",
|
||||
delay: SECOND * 5,
|
||||
type: "fixed",
|
||||
},
|
||||
delay,
|
||||
}
|
||||
);
|
||||
videosFound.add(aid);
|
||||
|
||||
@ -26,7 +26,7 @@ const filterWorker = new Worker(
|
||||
break;
|
||||
}
|
||||
},
|
||||
{ connection: redis as ConnectionOptions, concurrency: 2, removeOnComplete: { count: 1000 } }
|
||||
{ concurrency: 2, connection: redis as ConnectionOptions, removeOnComplete: { count: 1000 } }
|
||||
);
|
||||
|
||||
process.on("SIGINT", () => shutdown("SIGINT", filterWorker));
|
||||
|
||||
@ -58,8 +58,8 @@ const latestVideoWorker = new Worker(
|
||||
}
|
||||
},
|
||||
{
|
||||
connection: redis as ConnectionOptions,
|
||||
concurrency: 6,
|
||||
connection: redis as ConnectionOptions,
|
||||
removeOnComplete: { count: 1440 },
|
||||
removeOnFail: { count: 0 },
|
||||
}
|
||||
@ -100,7 +100,7 @@ const snapshotWorker = new Worker(
|
||||
break;
|
||||
}
|
||||
},
|
||||
{ connection: redis as ConnectionOptions, concurrency: 50, removeOnComplete: { count: 2000 } }
|
||||
{ concurrency: 50, connection: redis as ConnectionOptions, removeOnComplete: { count: 2000 } }
|
||||
);
|
||||
|
||||
snapshotWorker.on("error", (err) => {
|
||||
@ -118,7 +118,7 @@ const miscWorker = new Worker(
|
||||
break;
|
||||
}
|
||||
},
|
||||
{ connection: redis as ConnectionOptions, concurrency: 5, removeOnComplete: { count: 1000 } }
|
||||
{ concurrency: 5, connection: redis as ConnectionOptions, removeOnComplete: { count: 1000 } }
|
||||
);
|
||||
|
||||
miscWorker.on("error", (err) => {
|
||||
|
||||
@ -11,7 +11,7 @@ import { Switch } from "./Switch";
|
||||
import { useTheme } from "./ThemeContext";
|
||||
import { i18nProvider } from "./utils";
|
||||
|
||||
const defaultColor: Oklch = { mode: "oklch", h: 29.2339, c: 0.244572, l: 0.596005 };
|
||||
const defaultColor: Oklch = { c: 0.244572, h: 29.2339, l: 0.596005, mode: "oklch" };
|
||||
|
||||
const colorAtom = atomWithStorage<Oklch>("selectedColor", defaultColor);
|
||||
const p3Atom = atomWithStorage<boolean>("showP3", false);
|
||||
|
||||
@ -5,19 +5,6 @@ export type ThemeMode = "light" | "dark";
|
||||
|
||||
export const buildColorTokens = (base: Oklch) => {
|
||||
return {
|
||||
light: {
|
||||
background: getAdjustedColor(base, 0.98, 0.01),
|
||||
"bg-elevated-1": getAdjustedColor(base, 1, 0.008),
|
||||
"body-text": getAdjustedColor(base, 0.1, 0.01),
|
||||
"border-var-1": getAdjustedColor(base, 0.845, 0.004),
|
||||
"border-var-2": getAdjustedColor(base, 0.8, 0.007),
|
||||
"border-var-3": getAdjustedColor(base, 0.755, 0.01),
|
||||
primary: getAdjustedColor(base, 0.48, 0.08),
|
||||
"on-primary": getAdjustedColor(base, 0.999, 0.001),
|
||||
"on-bg-var-2": getAdjustedColor(base, 0.398, 0.0234),
|
||||
error: { mode: "oklch", l: 0.506, c: 0.192, h: 27.7 } as Oklch,
|
||||
"on-error": getAdjustedColor(base, 0.99, 0.01),
|
||||
},
|
||||
dark: {
|
||||
background: getAdjustedColor(base, 0.15, 0.002),
|
||||
"bg-elevated-1": getAdjustedColor(base, 0.2, 0.004),
|
||||
@ -25,11 +12,24 @@ export const buildColorTokens = (base: Oklch) => {
|
||||
"border-var-1": getAdjustedColor(base, 0.3, 0.004),
|
||||
"border-var-2": getAdjustedColor(base, 0.4, 0.007),
|
||||
"border-var-3": getAdjustedColor(base, 0.5, 0.01),
|
||||
primary: getAdjustedColor(base, 0.84, 0.1),
|
||||
"on-primary": getAdjustedColor(base, 0.3, 0.08),
|
||||
error: { c: 0.223, h: 27.8, l: 0.65, mode: "oklch" } as Oklch,
|
||||
"on-bg-var-2": getAdjustedColor(base, 0.83, 0.028),
|
||||
error: { mode: "oklch", l: 0.65, c: 0.223, h: 27.8 } as Oklch,
|
||||
"on-error": getAdjustedColor(base, 0.9, 0.01),
|
||||
"on-primary": getAdjustedColor(base, 0.3, 0.08),
|
||||
primary: getAdjustedColor(base, 0.84, 0.1),
|
||||
},
|
||||
light: {
|
||||
background: getAdjustedColor(base, 0.98, 0.01),
|
||||
"bg-elevated-1": getAdjustedColor(base, 1, 0.008),
|
||||
"body-text": getAdjustedColor(base, 0.1, 0.01),
|
||||
"border-var-1": getAdjustedColor(base, 0.845, 0.004),
|
||||
"border-var-2": getAdjustedColor(base, 0.8, 0.007),
|
||||
"border-var-3": getAdjustedColor(base, 0.755, 0.01),
|
||||
error: { c: 0.192, h: 27.7, l: 0.506, mode: "oklch" } as Oklch,
|
||||
"on-bg-var-2": getAdjustedColor(base, 0.398, 0.0234),
|
||||
"on-error": getAdjustedColor(base, 0.99, 0.01),
|
||||
"on-primary": getAdjustedColor(base, 0.999, 0.001),
|
||||
primary: getAdjustedColor(base, 0.48, 0.08),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -81,7 +81,7 @@ export const ColorBlock = ({ baseColor, text, l, c, h }: ColorBlockProps) => {
|
||||
animate={{ opacity: 1, width: 22 }}
|
||||
transition={{
|
||||
opacity: { duration: 0.2, ease: "backOut" },
|
||||
width: { type: "spring", bounce: 0.2, duration: 0.5 },
|
||||
width: { bounce: 0.2, duration: 0.5, type: "spring" },
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
|
||||
@ -84,8 +84,8 @@ export const Handle = ({
|
||||
shadow-[0px_0px_7px_2px_rgba(0,0,0,0.35)] cursor-grab active:cursor-grabbing
|
||||
touch-none select-none"
|
||||
style={{
|
||||
left: `${pos}%`,
|
||||
backgroundColor: `oklch(${color.l} ${color.c} ${color.h})`,
|
||||
left: `${pos}%`,
|
||||
transform: "translateY(-50%) translateX(-50%) rotate(45deg)",
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
|
||||
@ -24,10 +24,10 @@ export const Slider = ({ useP3, channel, color, onChange, i18nProvider }: Slider
|
||||
|
||||
const canvasRef = useRef<null | HTMLCanvasElement>(null);
|
||||
useOklchCanvas({
|
||||
channel: channel,
|
||||
max: maxValue[channel],
|
||||
canvasRef: canvasRef,
|
||||
channel: channel,
|
||||
color,
|
||||
max: maxValue[channel],
|
||||
useP3,
|
||||
});
|
||||
|
||||
|
||||
@ -44,10 +44,10 @@ export function useOklchCanvas({ useP3, channel, max, canvasRef, color }: UseOkl
|
||||
|
||||
try {
|
||||
const testColor = oklch({
|
||||
mode: "oklch",
|
||||
l: channel === "l" ? value : color.l,
|
||||
c: channel === "c" ? value : color.c,
|
||||
h: channel === "h" ? value : color.h,
|
||||
l: channel === "l" ? value : color.l,
|
||||
mode: "oklch",
|
||||
});
|
||||
|
||||
if (testColor && inGamut(colorGamut)(testColor)) {
|
||||
|
||||
@ -7,22 +7,22 @@ export const round = (value: number, precision: number) => {
|
||||
export const roundOklch = (oklch: Oklch) => {
|
||||
return {
|
||||
...oklch,
|
||||
l: round(oklch.l, precision.l),
|
||||
c: round(oklch.c, precision.c),
|
||||
h: round(oklch.h!, precision.h),
|
||||
l: round(oklch.l, precision.l),
|
||||
};
|
||||
};
|
||||
|
||||
export const precision = {
|
||||
l: 4,
|
||||
c: 4,
|
||||
h: 2,
|
||||
l: 4,
|
||||
};
|
||||
|
||||
export const maxValue = {
|
||||
l: 1,
|
||||
c: 0.37,
|
||||
h: 360,
|
||||
l: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -7,27 +7,27 @@ import { cn } from "@/lib//utils";
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
icon: "size-9",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
},
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -47,7 +47,7 @@ function Button({
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
className={cn(buttonVariants({ className, size, variant }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -4,7 +4,7 @@ import * as RechartsPrimitive from "recharts";
|
||||
import { cn } from "@/lib//utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
const THEMES = { dark: ".dark", light: "" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
@ -192,11 +192,11 @@ function ChartTooltipContent({
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5":
|
||||
nestLabel && indicator === "dashed",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"w-1": indicator === "line",
|
||||
}
|
||||
)}
|
||||
style={
|
||||
|
||||
@ -12,8 +12,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
|
||||
@ -54,8 +54,8 @@ export const MilestoneVideos: React.FC = () => {
|
||||
try {
|
||||
const { data, error } = await app.songs["close-milestone"]({ type }).get({
|
||||
query: {
|
||||
offset: currentOffset,
|
||||
limit: 20,
|
||||
offset: currentOffset,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -12,8 +12,8 @@ const app = treaty<App>(import.meta.env.VITE_API_URL!);
|
||||
export default function Login() {
|
||||
const navigate = useNavigate();
|
||||
const [formData, setFormData] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
username: "",
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
@ -18,6 +18,10 @@ export type Snapshot = {
|
||||
export const columns: ColumnDef<Snapshot>[] = [
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue("createdAt") as string;
|
||||
return <div>{formatDateTime(new Date(createdAt))}</div>;
|
||||
},
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
@ -29,57 +33,53 @@ export const columns: ColumnDef<Snapshot>[] = [
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const createdAt = row.getValue("createdAt") as string;
|
||||
return <div>{formatDateTime(new Date(createdAt))}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "views",
|
||||
header: "播放",
|
||||
cell: ({ row }) => {
|
||||
const views = row.getValue("views") as number;
|
||||
return <div>{views.toLocaleString()}</div>;
|
||||
},
|
||||
header: "播放",
|
||||
},
|
||||
{
|
||||
accessorKey: "likes",
|
||||
header: "点赞",
|
||||
cell: ({ row }) => {
|
||||
const likes = row.getValue("likes") as number;
|
||||
return <div>{likes.toLocaleString()}</div>;
|
||||
},
|
||||
header: "点赞",
|
||||
},
|
||||
{
|
||||
accessorKey: "favorites",
|
||||
header: "收藏",
|
||||
cell: ({ row }) => {
|
||||
const favorites = row.getValue("favorites") as number;
|
||||
return <div>{favorites.toLocaleString()}</div>;
|
||||
},
|
||||
header: "收藏",
|
||||
},
|
||||
{
|
||||
accessorKey: "coins",
|
||||
header: "硬币",
|
||||
cell: ({ row }) => {
|
||||
const coins = row.getValue("coins") as number;
|
||||
return <div>{coins.toLocaleString()}</div>;
|
||||
},
|
||||
header: "硬币",
|
||||
},
|
||||
{
|
||||
accessorKey: "danmakus",
|
||||
header: "弹幕",
|
||||
cell: ({ row }) => {
|
||||
const danmakus = row.getValue("danmakus") as number;
|
||||
return <div>{danmakus.toLocaleString()}</div>;
|
||||
},
|
||||
header: "弹幕",
|
||||
},
|
||||
{
|
||||
accessorKey: "shares",
|
||||
header: "转发",
|
||||
cell: ({ row }) => {
|
||||
const shares = row.getValue("shares") as number;
|
||||
return <div>{shares.toLocaleString()}</div>;
|
||||
},
|
||||
header: "转发",
|
||||
},
|
||||
];
|
||||
|
||||
@ -30,12 +30,12 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
data,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
state: {
|
||||
sorting,
|
||||
},
|
||||
|
||||
@ -55,7 +55,7 @@ export function formatHours(hours: number): string {
|
||||
export function addHoursToNow(hours: number): string {
|
||||
const d = new Date();
|
||||
d.setSeconds(d.getSeconds() + hours * 3600);
|
||||
return formatDateTime(d, true)
|
||||
return formatDateTime(d, true);
|
||||
}
|
||||
|
||||
export default function SongInfo({ loaderData }: Route.ComponentProps) {
|
||||
|
||||
@ -140,25 +140,25 @@ export const processSnapshots = (
|
||||
};
|
||||
|
||||
const createSnapshotData = (timestamp: number, snapshot: any) => ({
|
||||
createdAt: new Date(timestamp).toISOString(),
|
||||
views: snapshot.views,
|
||||
likes: snapshot.likes || 0,
|
||||
favorites: snapshot.favorites || 0,
|
||||
coins: snapshot.coins || 0,
|
||||
createdAt: new Date(timestamp).toISOString(),
|
||||
danmakus: snapshot.danmakus || 0,
|
||||
favorites: snapshot.favorites || 0,
|
||||
likes: snapshot.likes || 0,
|
||||
views: snapshot.views,
|
||||
});
|
||||
|
||||
const createInterpolatedSnapshot = (timestamp: number, prev: any, next: any, ratio: number) => ({
|
||||
createdAt: new Date(timestamp).toISOString(),
|
||||
views: Math.round(prev.views + (next.views - prev.views) * ratio),
|
||||
likes: Math.round((prev.likes || 0) + ((next.likes || 0) - (prev.likes || 0)) * ratio),
|
||||
favorites: Math.round(
|
||||
(prev.favorites || 0) + ((next.favorites || 0) - (prev.favorites || 0)) * ratio
|
||||
),
|
||||
coins: Math.round((prev.coins || 0) + ((next.coins || 0) - (prev.coins || 0)) * ratio),
|
||||
createdAt: new Date(timestamp).toISOString(),
|
||||
danmakus: Math.round(
|
||||
(prev.danmakus || 0) + ((next.danmakus || 0) - (prev.danmakus || 0)) * ratio
|
||||
),
|
||||
favorites: Math.round(
|
||||
(prev.favorites || 0) + ((next.favorites || 0) - (prev.favorites || 0)) * ratio
|
||||
),
|
||||
likes: Math.round((prev.likes || 0) + ((next.likes || 0) - (prev.likes || 0)) * ratio),
|
||||
views: Math.round(prev.views + (next.views - prev.views) * ratio),
|
||||
});
|
||||
|
||||
export const detectMilestoneAchievements = (
|
||||
@ -198,9 +198,9 @@ export const detectMilestoneAchievements = (
|
||||
const milestoneTime = new Date(prevTime + ratio * timeDiff);
|
||||
|
||||
const achievement: MilestoneAchievement = {
|
||||
achievedAt: milestoneTime.toISOString(),
|
||||
milestone,
|
||||
milestoneName,
|
||||
achievedAt: milestoneTime.toISOString(),
|
||||
views: milestone,
|
||||
};
|
||||
|
||||
@ -217,9 +217,9 @@ export const detectMilestoneAchievements = (
|
||||
const exactSnapshot =
|
||||
prevSnapshot.views === milestone ? prevSnapshot : currentSnapshot;
|
||||
const achievement: MilestoneAchievement = {
|
||||
achievedAt: exactSnapshot.createdAt,
|
||||
milestone,
|
||||
milestoneName,
|
||||
achievedAt: exactSnapshot.createdAt,
|
||||
views: milestone,
|
||||
};
|
||||
|
||||
|
||||
@ -29,13 +29,13 @@ const StatsTable = ({ snapshots }: { snapshots: Snapshots | null }) => {
|
||||
}
|
||||
|
||||
const tableData: Snapshot[] = snapshots.map((snapshot) => ({
|
||||
createdAt: snapshot.createdAt,
|
||||
views: snapshot.views,
|
||||
likes: snapshot.likes || 0,
|
||||
favorites: snapshot.favorites || 0,
|
||||
coins: snapshot.coins || 0,
|
||||
createdAt: snapshot.createdAt,
|
||||
danmakus: snapshot.danmakus || 0,
|
||||
favorites: snapshot.favorites || 0,
|
||||
likes: snapshot.likes || 0,
|
||||
shares: snapshot.shares || 0,
|
||||
views: snapshot.views,
|
||||
}));
|
||||
|
||||
return <DataTable columns={columns} data={tableData} />;
|
||||
@ -85,16 +85,16 @@ export const SnapshotsView = ({
|
||||
if (!snapshots) return null;
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
createdAt: publishedAt,
|
||||
views: 0,
|
||||
coins: 0,
|
||||
likes: 0,
|
||||
favorites: 0,
|
||||
shares: 0,
|
||||
danmakus: 0,
|
||||
aid: 0,
|
||||
coins: 0,
|
||||
createdAt: publishedAt,
|
||||
danmakus: 0,
|
||||
favorites: 0,
|
||||
id: 0,
|
||||
likes: 0,
|
||||
replies: 0,
|
||||
shares: 0,
|
||||
views: 0,
|
||||
},
|
||||
...snapshots,
|
||||
]
|
||||
|
||||
@ -11,23 +11,23 @@ import {
|
||||
} from "@/components/ui/chart";
|
||||
|
||||
const chartConfigLight = {
|
||||
views: {
|
||||
label: "播放",
|
||||
color: "#111417",
|
||||
},
|
||||
likes: {
|
||||
label: "点赞",
|
||||
},
|
||||
views: {
|
||||
color: "#111417",
|
||||
label: "播放",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
const chartConfigDark = {
|
||||
views: {
|
||||
label: "播放",
|
||||
color: "#EEEEF0",
|
||||
},
|
||||
likes: {
|
||||
label: "点赞",
|
||||
},
|
||||
views: {
|
||||
color: "#EEEEF0",
|
||||
label: "播放",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
interface ChartData {
|
||||
|
||||
@ -35,7 +35,7 @@ import type { Route } from "./+types/users";
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "User Management - Admin" },
|
||||
{ name: "description", content: "Manage users and permissions" },
|
||||
{ content: "Manage users and permissions", name: "description" },
|
||||
];
|
||||
}
|
||||
|
||||
@ -48,7 +48,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
// Fetch all users
|
||||
const allUsers = await db.select().from(users).orderBy(users.createdAt);
|
||||
|
||||
return { users: allUsers, currentUser: user };
|
||||
return { currentUser: user, users: allUsers };
|
||||
}
|
||||
|
||||
export async function action({ request }: Route.ActionArgs) {
|
||||
@ -119,12 +119,12 @@ export async function action({ request }: Route.ActionArgs) {
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
await db.insert(users).values({
|
||||
id: await generateId(6),
|
||||
username,
|
||||
password: hashedPassword,
|
||||
isAdmin,
|
||||
createdAt: new Date(),
|
||||
id: await generateId(6),
|
||||
isAdmin,
|
||||
password: hashedPassword,
|
||||
updatedAt: new Date(),
|
||||
username,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
@ -155,8 +155,8 @@ export async function action({ request }: Route.ActionArgs) {
|
||||
}
|
||||
|
||||
const updateData: any = {
|
||||
username,
|
||||
isAdmin,
|
||||
username,
|
||||
};
|
||||
|
||||
// Only update password if provided
|
||||
|
||||
@ -41,9 +41,9 @@ export function ProjectDialog({
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit({
|
||||
name,
|
||||
description,
|
||||
isPublic,
|
||||
name,
|
||||
});
|
||||
onOpenChange(false);
|
||||
// Reset form
|
||||
|
||||
@ -40,8 +40,8 @@ export function UserSearchModal({
|
||||
|
||||
try {
|
||||
const response = await fetch(`/project/${projectId}/settings`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
|
||||
@ -54,11 +54,11 @@ export function TaskForm({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const currentColumn = columns.find((col) => col.id === columnId);
|
||||
const priorityLabel = { low: "Low", medium: "Medium", high: "High" }[priority];
|
||||
const priorityLabel = { high: "High", low: "Low", medium: "Medium" }[priority];
|
||||
const priorityColor = {
|
||||
high: "bg-red-100 text-red-800",
|
||||
low: "bg-green-100 text-green-800",
|
||||
medium: "bg-yellow-100 text-yellow-800",
|
||||
high: "bg-red-100 text-red-800",
|
||||
}[priority];
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
@ -71,11 +71,11 @@ export function TaskForm({
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
await onSubmit({
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
columnId,
|
||||
priority,
|
||||
description: description.trim(),
|
||||
dueDate,
|
||||
priority,
|
||||
title: title.trim(),
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
|
||||
@ -7,20 +7,20 @@ import { cn } from "@/lib/utils";
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -7,29 +7,29 @@ import { cn } from "@/lib/utils";
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
icon: "size-9",
|
||||
"icon-lg": "size-10",
|
||||
"icon-sm": "size-8",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
},
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -49,7 +49,7 @@ function Button({
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
className={cn(buttonVariants({ className, size, variant }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -33,36 +33,16 @@ function Calendar({
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn("flex gap-4 flex-col md:flex-row relative", defaultClassNames.months),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn("absolute bg-popover inset-0 opacity-0", defaultClassNames.dropdown),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
@ -70,51 +50,61 @@ function Calendar({
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
|
||||
dropdown: cn("absolute bg-popover inset-0 opacity-0", defaultClassNames.dropdown),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
months: cn("flex gap-4 flex-col md:flex-row relative", defaultClassNames.months),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
table: "w-full border-collapse",
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
|
||||
@ -127,6 +117,16 @@ function Calendar({
|
||||
return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
|
||||
@ -104,12 +104,12 @@ function SidebarProvider({
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
open,
|
||||
openMobile,
|
||||
setOpen,
|
||||
setOpenMobile,
|
||||
state,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
@ -452,21 +452,21 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
sm: "h-7 text-xs",
|
||||
},
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -493,7 +493,7 @@ function SidebarMenuButton({
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
className={cn(sidebarMenuButtonVariants({ size, variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -18,18 +18,18 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
|
||||
@ -14,7 +14,7 @@ import type { Route } from "./+types/newProject";
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Create New Project" },
|
||||
{ name: "description", content: "Create a new project for task management" },
|
||||
{ content: "Create a new project for task management", name: "description" },
|
||||
];
|
||||
}
|
||||
|
||||
@ -38,11 +38,11 @@ export async function action({ request }: Route.ActionArgs) {
|
||||
|
||||
// Create the project
|
||||
await db.insert(projects).values({
|
||||
id: projectId,
|
||||
ownerId: user.id,
|
||||
name,
|
||||
description,
|
||||
createdAt: now,
|
||||
description,
|
||||
id: projectId,
|
||||
name,
|
||||
ownerId: user.id,
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
@ -55,11 +55,11 @@ export async function action({ request }: Route.ActionArgs) {
|
||||
|
||||
for (const column of defaultColumns) {
|
||||
await db.insert(columns).values({
|
||||
createdAt: now,
|
||||
id: await generateId(6),
|
||||
projectId,
|
||||
name: column.name,
|
||||
position: column.position,
|
||||
createdAt: now,
|
||||
projectId,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ import { projectPageAction } from "./projectPageAction";
|
||||
export function meta({ loaderData }: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: `${loaderData.project.name} - FramSpor` },
|
||||
{ name: "description", content: `Manage tasks for ${loaderData.project.name}` },
|
||||
{ content: `Manage tasks for ${loaderData.project.name}`, name: "description" },
|
||||
];
|
||||
}
|
||||
|
||||
@ -77,10 +77,10 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
);
|
||||
|
||||
return {
|
||||
project,
|
||||
columns: columnsWithTasks,
|
||||
user,
|
||||
canEdit,
|
||||
columns: columnsWithTasks,
|
||||
project,
|
||||
user,
|
||||
};
|
||||
}
|
||||
|
||||
@ -144,8 +144,8 @@ export default function ProjectBoard({ loaderData }: Route.ComponentProps) {
|
||||
formData.append("name", data.name);
|
||||
|
||||
const response = await fetch(`/project/${project.id}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
revalidator.revalidate();
|
||||
@ -157,8 +157,8 @@ export default function ProjectBoard({ loaderData }: Route.ComponentProps) {
|
||||
formData.append("columnId", columnId);
|
||||
|
||||
const response = await fetch(`/project/${project.id}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@ -179,8 +179,8 @@ export default function ProjectBoard({ loaderData }: Route.ComponentProps) {
|
||||
formData.append("isPublic", data.isPublic ? "true" : "false");
|
||||
|
||||
const response = await fetch(`/project/${project.id}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@ -194,8 +194,8 @@ export default function ProjectBoard({ loaderData }: Route.ComponentProps) {
|
||||
formData.append("intent", "deleteProject");
|
||||
|
||||
const response = await fetch(`/project/${project.id}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@ -229,8 +229,8 @@ export default function ProjectBoard({ loaderData }: Route.ComponentProps) {
|
||||
}
|
||||
|
||||
const response = await fetch(`/project/${project.id}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
revalidator.revalidate();
|
||||
@ -242,8 +242,8 @@ export default function ProjectBoard({ loaderData }: Route.ComponentProps) {
|
||||
formData.append("taskId", editingTask.id);
|
||||
|
||||
const response = await fetch(`/project/${project.id}`, {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
revalidator.revalidate();
|
||||
@ -279,8 +279,8 @@ export default function ProjectBoard({ loaderData }: Route.ComponentProps) {
|
||||
onSubmit={handleProjectSubmit}
|
||||
onDelete={handleDeleteProject}
|
||||
initialData={{
|
||||
name: project.name,
|
||||
description: project.description || "",
|
||||
name: project.name,
|
||||
}}
|
||||
isEditing={true}
|
||||
/>
|
||||
|
||||
@ -67,14 +67,14 @@ export const projectPageAction = async ({ request, params }: Route.ActionArgs) =
|
||||
const taskId = await generateId(7);
|
||||
|
||||
await db.insert(tasks).values({
|
||||
id: taskId,
|
||||
projectId: projectId,
|
||||
columnId: columnId,
|
||||
title: title,
|
||||
description: description,
|
||||
priority: priority,
|
||||
dueDate: dueDate ? new Date(dueDate) : null,
|
||||
createdAt: new Date(),
|
||||
description: description,
|
||||
dueDate: dueDate ? new Date(dueDate) : null,
|
||||
id: taskId,
|
||||
priority: priority,
|
||||
projectId: projectId,
|
||||
title: title,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
@ -96,11 +96,11 @@ export const projectPageAction = async ({ request, params }: Route.ActionArgs) =
|
||||
await db
|
||||
.update(tasks)
|
||||
.set({
|
||||
title: title,
|
||||
description: description,
|
||||
columnId: columnId,
|
||||
priority: priority,
|
||||
description: description,
|
||||
dueDate: dueDate ? new Date(dueDate) : null,
|
||||
priority: priority,
|
||||
title: title,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(tasks.id, taskId));
|
||||
@ -142,15 +142,15 @@ export const projectPageAction = async ({ request, params }: Route.ActionArgs) =
|
||||
: 0;
|
||||
|
||||
await db.insert(columns).values({
|
||||
createdAt: new Date(),
|
||||
id: columnId,
|
||||
projectId: projectId,
|
||||
name: name,
|
||||
position: newPosition,
|
||||
createdAt: new Date(),
|
||||
projectId: projectId,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
return { success: true, columnId };
|
||||
return { columnId, success: true };
|
||||
}
|
||||
|
||||
if (intent === "updateColumn") {
|
||||
@ -171,7 +171,7 @@ export const projectPageAction = async ({ request, params }: Route.ActionArgs) =
|
||||
})
|
||||
.where(eq(columns.id, columnId));
|
||||
|
||||
return { success: true, columnId };
|
||||
return { columnId, success: true };
|
||||
}
|
||||
|
||||
if (intent === "deleteColumn") {
|
||||
@ -190,7 +190,7 @@ export const projectPageAction = async ({ request, params }: Route.ActionArgs) =
|
||||
|
||||
await db.delete(columns).where(eq(columns.id, columnId));
|
||||
|
||||
return { success: true, columnId };
|
||||
return { columnId, success: true };
|
||||
}
|
||||
|
||||
if (intent === "reorderColumns") {
|
||||
@ -223,10 +223,10 @@ export const projectPageAction = async ({ request, params }: Route.ActionArgs) =
|
||||
await db
|
||||
.update(projects)
|
||||
.set({
|
||||
name: name,
|
||||
description: description,
|
||||
updatedAt: new Date(),
|
||||
isPublic: isPublic,
|
||||
name: name,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(projects.id, projectId));
|
||||
|
||||
@ -248,7 +248,7 @@ export const projectPageAction = async ({ request, params }: Route.ActionArgs) =
|
||||
|
||||
await db.delete(projects).where(eq(projects.id, projectId));
|
||||
|
||||
return { success: true, redirect: "/" };
|
||||
return { redirect: "/", success: true };
|
||||
}
|
||||
|
||||
return { error: "Unknown action" };
|
||||
|
||||
@ -43,7 +43,7 @@ import type { Route } from "./+types/settings";
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Project Settings" },
|
||||
{ name: "description", content: "Manage project settings and permissions" },
|
||||
{ content: "Manage project settings and permissions", name: "description" },
|
||||
];
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ export async function loader({ request, params }: Route.LoaderArgs) {
|
||||
.where(and(eq(projects.id, projectId), eq(projects.ownerId, user.id)))
|
||||
.get();
|
||||
|
||||
return { project, allUsers, currentPermissions, currentUser: user, isOwner };
|
||||
return { allUsers, currentPermissions, currentUser: user, isOwner, project };
|
||||
}
|
||||
|
||||
export async function action({ request, params }: Route.ActionArgs) {
|
||||
@ -127,9 +127,9 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||
await db
|
||||
.update(projects)
|
||||
.set({
|
||||
name,
|
||||
description,
|
||||
isPublic,
|
||||
name,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(projects.id, projectId));
|
||||
@ -172,11 +172,11 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||
}
|
||||
|
||||
await db.insert(projectPermissions).values({
|
||||
canEdit: canEditPermission,
|
||||
createdAt: new Date(),
|
||||
id: crypto.randomUUID(),
|
||||
projectId,
|
||||
userId,
|
||||
canEdit: canEditPermission,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
return redirect(`/project/${projectId}`);
|
||||
@ -333,8 +333,8 @@ export function UsersManagement({
|
||||
fetch(
|
||||
`/project/${project.id}/settings`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
}}
|
||||
|
||||
@ -12,15 +12,15 @@ import "./app.css";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
|
||||
export const links: Route.LinksFunction = () => [
|
||||
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
|
||||
{ href: "https://fonts.googleapis.com", rel: "preconnect" },
|
||||
{
|
||||
rel: "preconnect",
|
||||
href: "https://fonts.gstatic.com",
|
||||
crossOrigin: "anonymous",
|
||||
href: "https://fonts.gstatic.com",
|
||||
rel: "preconnect",
|
||||
},
|
||||
{
|
||||
rel: "stylesheet",
|
||||
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
|
||||
rel: "stylesheet",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import type { Route } from "./+types/setup";
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "Initial Setup" },
|
||||
{ name: "description", content: "Create initial admin user" },
|
||||
{ content: "Create initial admin user", name: "description" },
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ import type { Route } from "./+types/profile";
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "User Profile" },
|
||||
{ name: "description", content: "Manage your account settings" },
|
||||
{ content: "Manage your account settings", name: "description" },
|
||||
];
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ export async function action({ request }: Route.ActionArgs) {
|
||||
.set({ password: hashedNewPassword, updatedAt: new Date() })
|
||||
.where(eq(users.id, user.id));
|
||||
|
||||
return { success: true, message: "Password updated successfully" };
|
||||
return { message: "Password updated successfully", success: true };
|
||||
}
|
||||
|
||||
if (intent === "changeUsername") {
|
||||
@ -88,14 +88,14 @@ export async function action({ request }: Route.ActionArgs) {
|
||||
|
||||
await db
|
||||
.update(users)
|
||||
.set({ username: newUsername, updatedAt: new Date() })
|
||||
.set({ updatedAt: new Date(), username: newUsername })
|
||||
.where(eq(users.id, user.id));
|
||||
|
||||
const updatedUser = await db.select().from(users).where(eq(users.id, user.id)).get();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Username updated successfully",
|
||||
success: true,
|
||||
updatedUser,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
out: "./drizzle",
|
||||
schema: "./lib/db/schema.ts",
|
||||
dialect: "sqlite",
|
||||
dbCredentials: {
|
||||
url: process.env.DB_FILE_NAME!,
|
||||
},
|
||||
dialect: "sqlite",
|
||||
out: "./drizzle",
|
||||
schema: "./lib/db/schema.ts",
|
||||
});
|
||||
|
||||
@ -24,11 +24,11 @@ export async function createUser(username: string, password: string) {
|
||||
const hashedPassword = await Argon2id.hashEncoded(password);
|
||||
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
createdAt: now,
|
||||
id: userId,
|
||||
password: hashedPassword,
|
||||
updatedAt: now,
|
||||
username,
|
||||
});
|
||||
|
||||
return userId;
|
||||
@ -52,10 +52,10 @@ export async function createSession(userId: string) {
|
||||
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
||||
|
||||
await db.insert(sessions).values({
|
||||
createdAt: new Date(),
|
||||
expiresAt,
|
||||
id: sessionId,
|
||||
userId,
|
||||
expiresAt,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
return sessionId;
|
||||
|
||||
@ -3,26 +3,28 @@ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
// Users table
|
||||
export const users = sqliteTable("users", {
|
||||
id: text("id").primaryKey(),
|
||||
username: text("username").notNull(),
|
||||
password: text("password_hash").notNull(),
|
||||
isAdmin: integer("is_admin", { mode: "boolean" }).default(false),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
id: text("id").primaryKey(),
|
||||
isAdmin: integer("is_admin", { mode: "boolean" }).default(false),
|
||||
password: text("password_hash").notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
||||
username: text("username").notNull(),
|
||||
});
|
||||
|
||||
// Sessions table for authentication
|
||||
export const sessions = sqliteTable("sessions", {
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||
id: text("id").primaryKey(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
});
|
||||
|
||||
// Project permissions table
|
||||
export const projectPermissions = sqliteTable("project_permissions", {
|
||||
canEdit: integer("can_edit", { mode: "boolean" }).default(false),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
@ -30,49 +32,47 @@ export const projectPermissions = sqliteTable("project_permissions", {
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
canEdit: integer("can_edit", { mode: "boolean" }).default(false),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
});
|
||||
|
||||
// Projects table
|
||||
export const projects = sqliteTable("projects", {
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
description: text("description"),
|
||||
id: text("id").primaryKey(),
|
||||
isPublic: integer("is_public", { mode: "boolean" }).default(false),
|
||||
name: text("name").notNull(),
|
||||
ownerId: text("owner_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
description: text("description"),
|
||||
isPublic: integer("is_public", { mode: "boolean" }).default(false),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
||||
});
|
||||
|
||||
// Columns table for Kanban board
|
||||
export const columns = sqliteTable("columns", {
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
position: integer("position").notNull(),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
name: text("name").notNull(),
|
||||
position: integer("position").notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
||||
});
|
||||
|
||||
// Tasks table
|
||||
export const tasks = sqliteTable("tasks", {
|
||||
id: text("id").primaryKey(),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
columnId: text("column_id")
|
||||
.notNull()
|
||||
.references(() => columns.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
description: text("description"),
|
||||
priority: text("priority", { enum: ["low", "medium", "high"] }).default("medium"),
|
||||
dueDate: integer("due_date", { mode: "timestamp" }),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
||||
description: text("description"),
|
||||
dueDate: integer("due_date", { mode: "timestamp" }),
|
||||
id: text("id").primaryKey(),
|
||||
priority: text("priority", { enum: ["low", "medium", "high"] }).default("medium"),
|
||||
projectId: text("project_id")
|
||||
.notNull()
|
||||
.references(() => projects.id, { onDelete: "cascade" }),
|
||||
title: text("title").notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
||||
});
|
||||
|
||||
@ -91,13 +91,13 @@ export const sessionsRelations = relations(sessions, ({ one }) => ({
|
||||
}));
|
||||
|
||||
export const projectsRelations = relations(projects, ({ one, many }) => ({
|
||||
columns: many(columns),
|
||||
owner: one(users, {
|
||||
fields: [projects.ownerId],
|
||||
references: [users.id],
|
||||
relationName: "owner",
|
||||
}),
|
||||
permissions: many(projectPermissions),
|
||||
columns: many(columns),
|
||||
tasks: many(tasks),
|
||||
}));
|
||||
|
||||
@ -121,14 +121,14 @@ export const columnsRelations = relations(columns, ({ one, many }) => ({
|
||||
}));
|
||||
|
||||
export const tasksRelations = relations(tasks, ({ one }) => ({
|
||||
project: one(projects, {
|
||||
fields: [tasks.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
column: one(columns, {
|
||||
fields: [tasks.columnId],
|
||||
references: [columns.id],
|
||||
}),
|
||||
project: one(projects, {
|
||||
fields: [tasks.projectId],
|
||||
references: [projects.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// Types
|
||||
|
||||
@ -35,8 +35,8 @@ export const handler = async (event, _context) => {
|
||||
const randomUserAgent = userAgents[Math.floor(Math.random() * userAgents.length)];
|
||||
const response = await fetch(eventObj.url, {
|
||||
headers: {
|
||||
"User-Agent": randomUserAgent,
|
||||
Referer: refererUrl,
|
||||
"User-Agent": randomUserAgent,
|
||||
},
|
||||
});
|
||||
statusCode = response.status;
|
||||
@ -51,19 +51,19 @@ export const handler = async (event, _context) => {
|
||||
const randomUserAgent = userAgents[Math.floor(Math.random() * userAgents.length)];
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": randomUserAgent,
|
||||
Referer: refererUrl,
|
||||
"User-Agent": randomUserAgent,
|
||||
},
|
||||
});
|
||||
const responseBody = await response.text();
|
||||
return {
|
||||
statusCode: response.status,
|
||||
body: responseBody,
|
||||
statusCode: response.status,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
body: `Error fetching URL: ${error.message}`,
|
||||
statusCode: 500,
|
||||
};
|
||||
}
|
||||
});
|
||||
@ -73,7 +73,7 @@ export const handler = async (event, _context) => {
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: statusCode,
|
||||
body: JSON.stringify(body),
|
||||
statusCode: statusCode,
|
||||
};
|
||||
};
|
||||
|
||||
@ -10,8 +10,8 @@ const quit = (reason?: string) => {
|
||||
};
|
||||
|
||||
const args = arg({
|
||||
"--db": String,
|
||||
"--aids": String,
|
||||
"--db": String,
|
||||
});
|
||||
|
||||
const dbPath = args["--db"];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user