refactor: replace all explicit any types with proper TypeScript types
Backend resolvers: typed enrichArticle/enrichVideo/enrichModel with DB and $inferSelect types, SQL<unknown>[] for conditions arrays, proper enum casts for status/role fields, $inferInsert for .set() updates, typed raw SQL result rows in gamification, ReplyLike interface for ctx.reply in auth. Frontend: typed catch blocks with Error/interface casts, isActiveLink param, adminGetUser response, tags filter callback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { builder } from "../builder";
|
import { builder } from "../builder";
|
||||||
import { ArticleType, ArticleListType, AdminArticleListType } from "../types/index";
|
import { ArticleType, ArticleListType, AdminArticleListType } from "../types/index";
|
||||||
import { articles, users } from "../../db/schema/index";
|
import { articles, users } from "../../db/schema/index";
|
||||||
import { eq, and, lte, desc, asc, ilike, or, count, arrayContains } from "drizzle-orm";
|
import { eq, and, lte, desc, asc, ilike, or, count, arrayContains, type SQL } from "drizzle-orm";
|
||||||
import { requireAdmin } from "../../lib/acl";
|
import { requireAdmin } from "../../lib/acl";
|
||||||
|
import type { DB } from "../../db/connection";
|
||||||
|
|
||||||
async function enrichArticle(db: any, article: any) {
|
async function enrichArticle(db: DB, article: typeof articles.$inferSelect) {
|
||||||
let author = null;
|
let author = null;
|
||||||
if (article.author) {
|
if (article.author) {
|
||||||
const authorUser = await db
|
const authorUser = await db
|
||||||
@@ -39,7 +40,7 @@ builder.queryField("articles", (t) =>
|
|||||||
const pageSize = args.limit ?? 24;
|
const pageSize = args.limit ?? 24;
|
||||||
const offset = args.offset ?? 0;
|
const offset = args.offset ?? 0;
|
||||||
|
|
||||||
const conditions: any[] = [lte(articles.publish_date, new Date())];
|
const conditions: SQL<unknown>[] = [lte(articles.publish_date, new Date())];
|
||||||
if (args.featured !== null && args.featured !== undefined) {
|
if (args.featured !== null && args.featured !== undefined) {
|
||||||
conditions.push(eq(articles.featured, args.featured));
|
conditions.push(eq(articles.featured, args.featured));
|
||||||
}
|
}
|
||||||
@@ -50,28 +51,24 @@ builder.queryField("articles", (t) =>
|
|||||||
or(
|
or(
|
||||||
ilike(articles.title, `%${args.search}%`),
|
ilike(articles.title, `%${args.search}%`),
|
||||||
ilike(articles.excerpt, `%${args.search}%`),
|
ilike(articles.excerpt, `%${args.search}%`),
|
||||||
),
|
) as SQL<unknown>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const orderArgs =
|
|
||||||
args.sortBy === "name"
|
|
||||||
? [asc(articles.title)]
|
|
||||||
: args.sortBy === "featured"
|
|
||||||
? [desc(articles.featured), desc(articles.publish_date)]
|
|
||||||
: [desc(articles.publish_date)];
|
|
||||||
|
|
||||||
const where = and(...conditions);
|
const where = and(...conditions);
|
||||||
|
const baseQuery = ctx.db.select().from(articles).where(where);
|
||||||
|
const ordered =
|
||||||
|
args.sortBy === "name"
|
||||||
|
? baseQuery.orderBy(asc(articles.title))
|
||||||
|
: args.sortBy === "featured"
|
||||||
|
? baseQuery.orderBy(desc(articles.featured), desc(articles.publish_date))
|
||||||
|
: baseQuery.orderBy(desc(articles.publish_date));
|
||||||
|
|
||||||
const [articleList, totalRows] = await Promise.all([
|
const [articleList, totalRows] = await Promise.all([
|
||||||
(ctx.db.select().from(articles).where(where) as any)
|
ordered.limit(pageSize).offset(offset),
|
||||||
.orderBy(...orderArgs)
|
|
||||||
.limit(pageSize)
|
|
||||||
.offset(offset),
|
|
||||||
ctx.db.select({ total: count() }).from(articles).where(where),
|
ctx.db.select({ total: count() }).from(articles).where(where),
|
||||||
]);
|
]);
|
||||||
const items = await Promise.all(
|
const items = await Promise.all(articleList.map((article) => enrichArticle(ctx.db, article)));
|
||||||
articleList.map((article: any) => enrichArticle(ctx.db, article)),
|
|
||||||
);
|
|
||||||
return { items, total: totalRows[0]?.total ?? 0 };
|
return { items, total: totalRows[0]?.total ?? 0 };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -130,13 +127,13 @@ builder.queryField("adminListArticles", (t) =>
|
|||||||
const limit = args.limit ?? 50;
|
const limit = args.limit ?? 50;
|
||||||
const offset = args.offset ?? 0;
|
const offset = args.offset ?? 0;
|
||||||
|
|
||||||
const conditions: any[] = [];
|
const conditions: SQL<unknown>[] = [];
|
||||||
if (args.search) {
|
if (args.search) {
|
||||||
conditions.push(
|
conditions.push(
|
||||||
or(
|
or(
|
||||||
ilike(articles.title, `%${args.search}%`),
|
ilike(articles.title, `%${args.search}%`),
|
||||||
ilike(articles.excerpt, `%${args.search}%`),
|
ilike(articles.excerpt, `%${args.search}%`),
|
||||||
),
|
) as SQL<unknown>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (args.category) conditions.push(eq(articles.category, args.category));
|
if (args.category) conditions.push(eq(articles.category, args.category));
|
||||||
@@ -154,9 +151,7 @@ builder.queryField("adminListArticles", (t) =>
|
|||||||
.offset(offset),
|
.offset(offset),
|
||||||
ctx.db.select({ total: count() }).from(articles).where(where),
|
ctx.db.select({ total: count() }).from(articles).where(where),
|
||||||
]);
|
]);
|
||||||
const items = await Promise.all(
|
const items = await Promise.all(articleList.map((article) => enrichArticle(ctx.db, article)));
|
||||||
articleList.map((article: any) => enrichArticle(ctx.db, article)),
|
|
||||||
);
|
|
||||||
return { items, total: totalRows[0]?.total ?? 0 };
|
return { items, total: totalRows[0]?.total ?? 0 };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -232,7 +227,7 @@ builder.mutationField("updateArticle", (t) =>
|
|||||||
|
|
||||||
const updated = await ctx.db
|
const updated = await ctx.db
|
||||||
.update(articles)
|
.update(articles)
|
||||||
.set(updates as any)
|
.set(updates as Partial<typeof articles.$inferInsert>)
|
||||||
.where(eq(articles.id, args.id))
|
.where(eq(articles.id, args.id))
|
||||||
.returning();
|
.returning();
|
||||||
if (!updated[0]) return null;
|
if (!updated[0]) return null;
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { builder } from "../builder";
|
|||||||
import { CurrentUserType } from "../types/index";
|
import { CurrentUserType } from "../types/index";
|
||||||
import { users } from "../../db/schema/index";
|
import { users } from "../../db/schema/index";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
interface ReplyLike {
|
||||||
|
header?: (name: string, value: string) => void;
|
||||||
|
}
|
||||||
import { hash, verify as verifyArgon } from "../../lib/argon";
|
import { hash, verify as verifyArgon } from "../../lib/argon";
|
||||||
import { setSession, deleteSession } from "../../lib/auth";
|
import { setSession, deleteSession } from "../../lib/auth";
|
||||||
import { sendVerification, sendPasswordReset } from "../../lib/email";
|
import { sendVerification, sendPasswordReset } from "../../lib/email";
|
||||||
@@ -46,12 +50,7 @@ builder.mutationField("login", (t) =>
|
|||||||
// Set session cookie
|
// Set session cookie
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
const cookieValue = `session_token=${token}; HttpOnly; Path=/; SameSite=Strict; Max-Age=86400${isProduction ? "; Secure" : ""}`;
|
const cookieValue = `session_token=${token}; HttpOnly; Path=/; SameSite=Strict; Max-Age=86400${isProduction ? "; Secure" : ""}`;
|
||||||
(ctx.reply as any).header?.("Set-Cookie", cookieValue);
|
(ctx.reply as ReplyLike).header?.("Set-Cookie", cookieValue);
|
||||||
|
|
||||||
// For graphql-yoga response
|
|
||||||
if ((ctx as any).serverResponse) {
|
|
||||||
(ctx as any).serverResponse.setHeader("Set-Cookie", cookieValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return user[0];
|
return user[0];
|
||||||
},
|
},
|
||||||
@@ -76,7 +75,7 @@ builder.mutationField("logout", (t) =>
|
|||||||
// Clear cookie
|
// Clear cookie
|
||||||
const isProduction = process.env.NODE_ENV === "production";
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
const cookieValue = `session_token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0${isProduction ? "; Secure" : ""}`;
|
const cookieValue = `session_token=; HttpOnly; Path=/; SameSite=Strict; Max-Age=0${isProduction ? "; Secure" : ""}`;
|
||||||
(ctx.reply as any).header?.("Set-Cookie", cookieValue);
|
(ctx.reply as ReplyLike).header?.("Set-Cookie", cookieValue);
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ builder.queryField("commentsForVideo", (t) =>
|
|||||||
.orderBy(desc(comments.date_created));
|
.orderBy(desc(comments.date_created));
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
commentList.map(async (c: any) => {
|
commentList.map(async (c) => {
|
||||||
const user = await ctx.db
|
const user = await ctx.db
|
||||||
.select({
|
.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
@@ -125,7 +125,7 @@ builder.queryField("adminListComments", (t) =>
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const items = await Promise.all(
|
const items = await Promise.all(
|
||||||
commentList.map(async (c: any) => {
|
commentList.map(async (c) => {
|
||||||
const user = await ctx.db
|
const user = await ctx.db
|
||||||
.select({
|
.select({
|
||||||
id: users.id,
|
id: users.id,
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ builder.queryField("leaderboard", (t) =>
|
|||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset);
|
.offset(offset);
|
||||||
|
|
||||||
return entries.map((e: any, i: number) => ({ ...e, rank: offset + i + 1 }));
|
return entries.map((e, i) => ({ ...e, rank: offset + i + 1 }));
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -101,7 +101,7 @@ builder.queryField("userGamification", (t) =>
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
stats: stats[0] ? { ...stats[0], rank } : null,
|
stats: stats[0] ? { ...stats[0], rank } : null,
|
||||||
achievements: userAchievements.map((a: any) => ({
|
achievements: userAchievements.map((a) => ({
|
||||||
...a,
|
...a,
|
||||||
date_unlocked: a.date_unlocked!,
|
date_unlocked: a.date_unlocked!,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { builder } from "../builder";
|
import { builder } from "../builder";
|
||||||
import { ModelType, ModelListType } from "../types/index";
|
import { ModelType, ModelListType } from "../types/index";
|
||||||
import { users, user_photos, files } from "../../db/schema/index";
|
import { users, user_photos, files } from "../../db/schema/index";
|
||||||
import { eq, and, desc, asc, ilike, count, arrayContains } from "drizzle-orm";
|
import { eq, and, desc, asc, ilike, count, arrayContains, type SQL } from "drizzle-orm";
|
||||||
|
import type { DB } from "../../db/connection";
|
||||||
|
|
||||||
async function enrichModel(db: any, user: any) {
|
async function enrichModel(db: DB, user: typeof users.$inferSelect) {
|
||||||
// Fetch photos
|
// Fetch photos
|
||||||
const photoRows = await db
|
const photoRows = await db
|
||||||
.select({ id: files.id, filename: files.filename })
|
.select({ id: files.id, filename: files.filename })
|
||||||
@@ -14,8 +15,8 @@ async function enrichModel(db: any, user: any) {
|
|||||||
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const photos = photoRows
|
const photos = photoRows
|
||||||
.filter((p: any) => p.id && !seen.has(p.id) && seen.add(p.id))
|
.filter((p) => p.id && !seen.has(p.id) && seen.add(p.id))
|
||||||
.map((p: any) => ({ id: p.id, filename: p.filename }));
|
.map((p) => ({ id: p.id, filename: p.filename }));
|
||||||
|
|
||||||
return { ...user, photos };
|
return { ...user, photos };
|
||||||
}
|
}
|
||||||
@@ -35,7 +36,7 @@ builder.queryField("models", (t) =>
|
|||||||
const pageSize = args.limit ?? 24;
|
const pageSize = args.limit ?? 24;
|
||||||
const offset = args.offset ?? 0;
|
const offset = args.offset ?? 0;
|
||||||
|
|
||||||
const conditions: any[] = [eq(users.role, "model")];
|
const conditions: SQL<unknown>[] = [eq(users.role, "model")];
|
||||||
if (args.search) conditions.push(ilike(users.artist_name, `%${args.search}%`));
|
if (args.search) conditions.push(ilike(users.artist_name, `%${args.search}%`));
|
||||||
if (args.tag) conditions.push(arrayContains(users.tags, [args.tag]));
|
if (args.tag) conditions.push(arrayContains(users.tags, [args.tag]));
|
||||||
|
|
||||||
@@ -46,7 +47,7 @@ builder.queryField("models", (t) =>
|
|||||||
ctx.db.select().from(users).where(where).orderBy(order).limit(pageSize).offset(offset),
|
ctx.db.select().from(users).where(where).orderBy(order).limit(pageSize).offset(offset),
|
||||||
ctx.db.select({ total: count() }).from(users).where(where),
|
ctx.db.select({ total: count() }).from(users).where(where),
|
||||||
]);
|
]);
|
||||||
const items = await Promise.all(modelList.map((m: any) => enrichModel(ctx.db, m)));
|
const items = await Promise.all(modelList.map((m) => enrichModel(ctx.db, m)));
|
||||||
return { items, total: totalRows[0]?.total ?? 0 };
|
return { items, total: totalRows[0]?.total ?? 0 };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { GraphQLError } from "graphql";
|
|||||||
import { builder } from "../builder";
|
import { builder } from "../builder";
|
||||||
import { RecordingType, AdminRecordingListType } from "../types/index";
|
import { RecordingType, AdminRecordingListType } from "../types/index";
|
||||||
import { recordings, recording_plays } from "../../db/schema/index";
|
import { recordings, recording_plays } from "../../db/schema/index";
|
||||||
import { eq, and, desc, ilike, count } from "drizzle-orm";
|
import { eq, and, desc, ilike, count, type SQL } from "drizzle-orm";
|
||||||
import { slugify } from "../../lib/slugify";
|
import { slugify } from "../../lib/slugify";
|
||||||
import { awardPoints, checkAchievements } from "../../lib/gamification";
|
import { awardPoints, checkAchievements } from "../../lib/gamification";
|
||||||
import { requireAdmin } from "../../lib/acl";
|
import { requireAdmin } from "../../lib/acl";
|
||||||
@@ -21,7 +21,7 @@ builder.queryField("recordings", (t) =>
|
|||||||
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
|
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
|
||||||
|
|
||||||
const conditions = [eq(recordings.user_id, ctx.currentUser.id)];
|
const conditions = [eq(recordings.user_id, ctx.currentUser.id)];
|
||||||
if (args.status) conditions.push(eq(recordings.status, args.status as any));
|
if (args.status) conditions.push(eq(recordings.status, args.status as "draft" | "published"));
|
||||||
if (args.linkedVideoId) conditions.push(eq(recordings.linked_video, args.linkedVideoId));
|
if (args.linkedVideoId) conditions.push(eq(recordings.linked_video, args.linkedVideoId));
|
||||||
|
|
||||||
const limit = args.limit || 50;
|
const limit = args.limit || 50;
|
||||||
@@ -115,7 +115,7 @@ builder.mutationField("createRecording", (t) =>
|
|||||||
user_id: ctx.currentUser.id,
|
user_id: ctx.currentUser.id,
|
||||||
tags: args.tags || [],
|
tags: args.tags || [],
|
||||||
linked_video: args.linkedVideoId || null,
|
linked_video: args.linkedVideoId || null,
|
||||||
status: (args.status as any) || "draft",
|
status: (args.status as "draft" | "published") || "draft",
|
||||||
public: false,
|
public: false,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
@@ -174,7 +174,7 @@ builder.mutationField("updateRecording", (t) =>
|
|||||||
|
|
||||||
const updated = await ctx.db
|
const updated = await ctx.db
|
||||||
.update(recordings)
|
.update(recordings)
|
||||||
.set(updates as any)
|
.set(updates as Partial<typeof recordings.$inferInsert>)
|
||||||
.where(eq(recordings.id, args.id))
|
.where(eq(recordings.id, args.id))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -355,9 +355,9 @@ builder.queryField("adminListRecordings", (t) =>
|
|||||||
const limit = args.limit ?? 50;
|
const limit = args.limit ?? 50;
|
||||||
const offset = args.offset ?? 0;
|
const offset = args.offset ?? 0;
|
||||||
|
|
||||||
const conditions: any[] = [];
|
const conditions: SQL<unknown>[] = [];
|
||||||
if (args.search) conditions.push(ilike(recordings.title, `%${args.search}%`));
|
if (args.search) conditions.push(ilike(recordings.title, `%${args.search}%`));
|
||||||
if (args.status) conditions.push(eq(recordings.status, args.status as any));
|
if (args.status) conditions.push(eq(recordings.status, args.status as "draft" | "published"));
|
||||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
|
|
||||||
const [rows, totalRows] = await Promise.all([
|
const [rows, totalRows] = await Promise.all([
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { GraphQLError } from "graphql";
|
|||||||
import { builder } from "../builder";
|
import { builder } from "../builder";
|
||||||
import { CurrentUserType, UserType, AdminUserListType, AdminUserDetailType } from "../types/index";
|
import { CurrentUserType, UserType, AdminUserListType, AdminUserDetailType } from "../types/index";
|
||||||
import { users, user_photos, files } from "../../db/schema/index";
|
import { users, user_photos, files } from "../../db/schema/index";
|
||||||
import { eq, ilike, or, count, and } from "drizzle-orm";
|
import { eq, ilike, or, count, and, asc, type SQL } from "drizzle-orm";
|
||||||
import { requireAdmin } from "../../lib/acl";
|
import { requireAdmin } from "../../lib/acl";
|
||||||
|
|
||||||
builder.queryField("me", (t) =>
|
builder.queryField("me", (t) =>
|
||||||
@@ -63,7 +63,7 @@ builder.mutationField("updateProfile", (t) =>
|
|||||||
|
|
||||||
await ctx.db
|
await ctx.db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set(updates as any)
|
.set(updates as Partial<typeof users.$inferInsert>)
|
||||||
.where(eq(users.id, ctx.currentUser.id));
|
.where(eq(users.id, ctx.currentUser.id));
|
||||||
|
|
||||||
const updated = await ctx.db
|
const updated = await ctx.db
|
||||||
@@ -93,27 +93,27 @@ builder.queryField("adminListUsers", (t) =>
|
|||||||
const limit = args.limit ?? 50;
|
const limit = args.limit ?? 50;
|
||||||
const offset = args.offset ?? 0;
|
const offset = args.offset ?? 0;
|
||||||
|
|
||||||
let query = ctx.db.select().from(users);
|
const conditions: SQL<unknown>[] = [];
|
||||||
let countQuery = ctx.db.select({ total: count() }).from(users);
|
|
||||||
|
|
||||||
const conditions: any[] = [];
|
|
||||||
if (args.role) {
|
if (args.role) {
|
||||||
conditions.push(eq(users.role, args.role as any));
|
conditions.push(eq(users.role, args.role as "model" | "viewer" | "admin"));
|
||||||
}
|
}
|
||||||
if (args.search) {
|
if (args.search) {
|
||||||
const pattern = `%${args.search}%`;
|
const pattern = `%${args.search}%`;
|
||||||
conditions.push(or(ilike(users.email, pattern), ilike(users.artist_name, pattern)));
|
conditions.push(
|
||||||
}
|
or(ilike(users.email, pattern), ilike(users.artist_name, pattern)) as SQL<unknown>,
|
||||||
|
);
|
||||||
if (conditions.length > 0) {
|
|
||||||
const where = conditions.length === 1 ? conditions[0] : and(...conditions);
|
|
||||||
query = (query as any).where(where);
|
|
||||||
countQuery = (countQuery as any).where(where);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
const [items, totalRows] = await Promise.all([
|
const [items, totalRows] = await Promise.all([
|
||||||
(query as any).limit(limit).offset(offset),
|
ctx.db
|
||||||
countQuery,
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(where)
|
||||||
|
.orderBy(asc(users.artist_name))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset),
|
||||||
|
ctx.db.select({ total: count() }).from(users).where(where),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { items, total: totalRows[0]?.total ?? 0 };
|
return { items, total: totalRows[0]?.total ?? 0 };
|
||||||
@@ -139,7 +139,8 @@ builder.mutationField("adminUpdateUser", (t) =>
|
|||||||
requireAdmin(ctx);
|
requireAdmin(ctx);
|
||||||
|
|
||||||
const updates: Record<string, unknown> = { date_updated: new Date() };
|
const updates: Record<string, unknown> = { date_updated: new Date() };
|
||||||
if (args.role !== undefined && args.role !== null) updates.role = args.role as any;
|
if (args.role !== undefined && args.role !== null)
|
||||||
|
updates.role = args.role as "model" | "viewer" | "admin";
|
||||||
if (args.isAdmin !== undefined && args.isAdmin !== null) updates.is_admin = args.isAdmin;
|
if (args.isAdmin !== undefined && args.isAdmin !== null) updates.is_admin = args.isAdmin;
|
||||||
if (args.firstName !== undefined && args.firstName !== null)
|
if (args.firstName !== undefined && args.firstName !== null)
|
||||||
updates.first_name = args.firstName;
|
updates.first_name = args.firstName;
|
||||||
@@ -151,7 +152,7 @@ builder.mutationField("adminUpdateUser", (t) =>
|
|||||||
|
|
||||||
const updated = await ctx.db
|
const updated = await ctx.db
|
||||||
.update(users)
|
.update(users)
|
||||||
.set(updates as any)
|
.set(updates as Partial<typeof users.$inferInsert>)
|
||||||
.where(eq(users.id, args.userId))
|
.where(eq(users.id, args.userId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -194,8 +195,8 @@ builder.queryField("adminGetUser", (t) =>
|
|||||||
.orderBy(user_photos.sort);
|
.orderBy(user_photos.sort);
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const photos = photoRows
|
const photos = photoRows
|
||||||
.filter((p: any) => p.id && !seen.has(p.id) && seen.add(p.id))
|
.filter((p) => p.id && !seen.has(p.id) && seen.add(p.id))
|
||||||
.map((p: any) => ({ id: p.id, filename: p.filename }));
|
.map((p) => ({ id: p.id, filename: p.filename }));
|
||||||
return { ...user[0], photos };
|
return { ...user[0], photos };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -28,10 +28,12 @@ import {
|
|||||||
lt,
|
lt,
|
||||||
gte,
|
gte,
|
||||||
arrayContains,
|
arrayContains,
|
||||||
|
type SQL,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { requireAdmin } from "../../lib/acl";
|
import { requireAdmin } from "../../lib/acl";
|
||||||
|
import type { DB } from "../../db/connection";
|
||||||
|
|
||||||
async function enrichVideo(db: any, video: any) {
|
async function enrichVideo(db: DB, video: typeof videos.$inferSelect) {
|
||||||
// Fetch models
|
// Fetch models
|
||||||
const modelRows = await db
|
const modelRows = await db
|
||||||
.select({
|
.select({
|
||||||
@@ -87,7 +89,7 @@ builder.queryField("videos", (t) =>
|
|||||||
const pageSize = args.limit ?? 24;
|
const pageSize = args.limit ?? 24;
|
||||||
const offset = args.offset ?? 0;
|
const offset = args.offset ?? 0;
|
||||||
|
|
||||||
const conditions: any[] = [lte(videos.upload_date, new Date())];
|
const conditions: SQL<unknown>[] = [lte(videos.upload_date, new Date())];
|
||||||
if (!ctx.currentUser) conditions.push(eq(videos.premium, false));
|
if (!ctx.currentUser) conditions.push(eq(videos.premium, false));
|
||||||
if (args.featured !== null && args.featured !== undefined) {
|
if (args.featured !== null && args.featured !== undefined) {
|
||||||
conditions.push(eq(videos.featured, args.featured));
|
conditions.push(eq(videos.featured, args.featured));
|
||||||
@@ -107,7 +109,7 @@ builder.queryField("videos", (t) =>
|
|||||||
conditions.push(
|
conditions.push(
|
||||||
inArray(
|
inArray(
|
||||||
videos.id,
|
videos.id,
|
||||||
videoIds.map((v: any) => v.video_id),
|
videoIds.map((v) => v.video_id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -148,8 +150,8 @@ builder.queryField("videos", (t) =>
|
|||||||
.leftJoin(files, eq(videos.movie, files.id))
|
.leftJoin(files, eq(videos.movie, files.id))
|
||||||
.where(fullWhere),
|
.where(fullWhere),
|
||||||
]);
|
]);
|
||||||
const videoList = rows.map((r: any) => r.v || r);
|
const videoList = rows.map((r) => r.v);
|
||||||
const items = await Promise.all(videoList.map((v: any) => enrichVideo(ctx.db, v)));
|
const items = await Promise.all(videoList.map((v) => enrichVideo(ctx.db, v)));
|
||||||
return { items, total: totalRows[0]?.total ?? 0 };
|
return { items, total: totalRows[0]?.total ?? 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +159,7 @@ builder.queryField("videos", (t) =>
|
|||||||
ctx.db.select().from(videos).where(where).orderBy(order).limit(pageSize).offset(offset),
|
ctx.db.select().from(videos).where(where).orderBy(order).limit(pageSize).offset(offset),
|
||||||
ctx.db.select({ total: count() }).from(videos).where(where),
|
ctx.db.select({ total: count() }).from(videos).where(where),
|
||||||
]);
|
]);
|
||||||
const items = await Promise.all(rows.map((v: any) => enrichVideo(ctx.db, v)));
|
const items = await Promise.all(rows.map((v) => enrichVideo(ctx.db, v)));
|
||||||
return { items, total: totalRows[0]?.total ?? 0 };
|
return { items, total: totalRows[0]?.total ?? 0 };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -421,7 +423,7 @@ builder.queryField("analytics", (t) =>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const videoIds = modelVideoIds.map((v: any) => v.video_id);
|
const videoIds = modelVideoIds.map((v) => v.video_id);
|
||||||
const videoList = await ctx.db.select().from(videos).where(inArray(videos.id, videoIds));
|
const videoList = await ctx.db.select().from(videos).where(inArray(videos.id, videoIds));
|
||||||
const plays = await ctx.db
|
const plays = await ctx.db
|
||||||
.select()
|
.select()
|
||||||
@@ -435,14 +437,14 @@ builder.queryField("analytics", (t) =>
|
|||||||
const totalLikes = videoList.reduce((sum, v) => sum + (v.likes_count || 0), 0);
|
const totalLikes = videoList.reduce((sum, v) => sum + (v.likes_count || 0), 0);
|
||||||
const totalPlays = videoList.reduce((sum, v) => sum + (v.plays_count || 0), 0);
|
const totalPlays = videoList.reduce((sum, v) => sum + (v.plays_count || 0), 0);
|
||||||
|
|
||||||
const playsByDate = plays.reduce((acc: any, play) => {
|
const playsByDate = plays.reduce((acc: Record<string, number>, play) => {
|
||||||
const date = new Date(play.date_created).toISOString().split("T")[0];
|
const date = new Date(play.date_created).toISOString().split("T")[0];
|
||||||
if (!acc[date]) acc[date] = 0;
|
if (!acc[date]) acc[date] = 0;
|
||||||
acc[date]++;
|
acc[date]++;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const likesByDate = likes.reduce((acc: any, like) => {
|
const likesByDate = likes.reduce((acc: Record<string, number>, like) => {
|
||||||
const date = new Date(like.date_created).toISOString().split("T")[0];
|
const date = new Date(like.date_created).toISOString().split("T")[0];
|
||||||
if (!acc[date]) acc[date] = 0;
|
if (!acc[date]) acc[date] = 0;
|
||||||
acc[date]++;
|
acc[date]++;
|
||||||
@@ -499,7 +501,7 @@ builder.queryField("adminListVideos", (t) =>
|
|||||||
const limit = args.limit ?? 50;
|
const limit = args.limit ?? 50;
|
||||||
const offset = args.offset ?? 0;
|
const offset = args.offset ?? 0;
|
||||||
|
|
||||||
const conditions: any[] = [];
|
const conditions: SQL<unknown>[] = [];
|
||||||
if (args.search) conditions.push(ilike(videos.title, `%${args.search}%`));
|
if (args.search) conditions.push(ilike(videos.title, `%${args.search}%`));
|
||||||
if (args.premium !== null && args.premium !== undefined)
|
if (args.premium !== null && args.premium !== undefined)
|
||||||
conditions.push(eq(videos.premium, args.premium));
|
conditions.push(eq(videos.premium, args.premium));
|
||||||
@@ -517,7 +519,7 @@ builder.queryField("adminListVideos", (t) =>
|
|||||||
.offset(offset),
|
.offset(offset),
|
||||||
ctx.db.select({ total: count() }).from(videos).where(where),
|
ctx.db.select({ total: count() }).from(videos).where(where),
|
||||||
]);
|
]);
|
||||||
const items = await Promise.all(rows.map((v: any) => enrichVideo(ctx.db, v)));
|
const items = await Promise.all(rows.map((v) => enrichVideo(ctx.db, v)));
|
||||||
return { items, total: totalRows[0]?.total ?? 0 };
|
return { items, total: totalRows[0]?.total ?? 0 };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -590,7 +592,7 @@ builder.mutationField("updateVideo", (t) =>
|
|||||||
|
|
||||||
const updated = await ctx.db
|
const updated = await ctx.db
|
||||||
.update(videos)
|
.update(videos)
|
||||||
.set(updates as any)
|
.set(updates as Partial<typeof videos.$inferInsert>)
|
||||||
.where(eq(videos.id, args.id))
|
.where(eq(videos.id, args.id))
|
||||||
.returning();
|
.returning();
|
||||||
if (!updated[0]) return null;
|
if (!updated[0]) return null;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export async function calculateWeightedScore(db: DB, userId: string): Promise<nu
|
|||||||
FROM user_points
|
FROM user_points
|
||||||
WHERE user_id = ${userId}
|
WHERE user_id = ${userId}
|
||||||
`);
|
`);
|
||||||
return parseFloat((result.rows[0] as any)?.weighted_score || "0");
|
return parseFloat((result.rows[0] as { weighted_score?: string })?.weighted_score || "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUserStats(db: DB, userId: string): Promise<void> {
|
export async function updateUserStats(db: DB, userId: string): Promise<void> {
|
||||||
@@ -84,7 +84,7 @@ export async function updateUserStats(db: DB, userId: string): Promise<void> {
|
|||||||
sql`, `,
|
sql`, `,
|
||||||
)})
|
)})
|
||||||
`);
|
`);
|
||||||
playbacksCount = parseInt((playbacksResult.rows[0] as any)?.count || "0");
|
playbacksCount = parseInt((playbacksResult.rows[0] as { count?: string })?.count || "0");
|
||||||
} else {
|
} else {
|
||||||
const playbacksResult = await db
|
const playbacksResult = await db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
@@ -242,7 +242,7 @@ async function getAchievementProgress(
|
|||||||
WHERE rp.user_id = ${userId}
|
WHERE rp.user_id = ${userId}
|
||||||
AND r.user_id != ${userId}
|
AND r.user_id != ${userId}
|
||||||
`);
|
`);
|
||||||
return parseInt((result.rows[0] as any)?.count || "0");
|
return parseInt((result.rows[0] as { count?: string })?.count || "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["completionist_10", "completionist_100"].includes(code)) {
|
if (["completionist_10", "completionist_100"].includes(code)) {
|
||||||
@@ -293,7 +293,7 @@ async function getAchievementProgress(
|
|||||||
WHERE rp.user_id = ${userId} AND r.user_id != ${userId}
|
WHERE rp.user_id = ${userId} AND r.user_id != ${userId}
|
||||||
`);
|
`);
|
||||||
const rc = recordingsResult[0]?.count || 0;
|
const rc = recordingsResult[0]?.count || 0;
|
||||||
const pc = parseInt((playsResult.rows[0] as any)?.count || "0");
|
const pc = parseInt((playsResult.rows[0] as { count?: string })?.count || "0");
|
||||||
return rc >= 50 && pc >= 100 ? 1 : 0;
|
return rc >= 50 && pc >= 100 ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
isMobileMenuOpen = false;
|
isMobileMenuOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActiveLink(link: any) {
|
function isActiveLink(link: { name: string; href: string }) {
|
||||||
return (
|
return (
|
||||||
(page.url.pathname === "/" && link === navLinks[0]) ||
|
(page.url.pathname === "/" && link === navLinks[0]) ||
|
||||||
(page.url.pathname.startsWith(link.href) && link !== navLinks[0])
|
(page.url.pathname.startsWith(link.href) && link !== navLinks[0])
|
||||||
|
|||||||
@@ -20,7 +20,9 @@
|
|||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
{#if description}
|
{#if description}
|
||||||
<p class="text-xl md:text-2xl text-muted-foreground mb-10 leading-relaxed max-w-4xl mx-auto">
|
<p
|
||||||
|
class="text-xl md:text-2xl text-muted-foreground mb-10 leading-relaxed max-w-4xl mx-auto"
|
||||||
|
>
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
return "text-green-400 bg-green-400/20";
|
return "text-green-400 bg-green-400/20";
|
||||||
case "draft":
|
case "draft":
|
||||||
return "text-yellow-400 bg-yellow-400/20";
|
return "text-yellow-400 bg-yellow-400/20";
|
||||||
default:
|
default:
|
||||||
return "text-gray-400 bg-gray-400/20";
|
return "text-gray-400 bg-gray-400/20";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1245,7 +1245,9 @@ export async function adminGetUser(userId: string, token?: string) {
|
|||||||
"adminGetUser",
|
"adminGetUser",
|
||||||
async () => {
|
async () => {
|
||||||
const client = token ? getAuthClient(token) : getGraphQLClient();
|
const client = token ? getAuthClient(token) : getGraphQLClient();
|
||||||
const data = await client.request<{ adminGetUser: any }>(ADMIN_GET_USER_QUERY, { userId });
|
const data = await client.request<{
|
||||||
|
adminGetUser: User & { photos: Array<{ id: string; filename: string }> };
|
||||||
|
}>(ADMIN_GET_USER_QUERY, { userId });
|
||||||
return data.adminGetUser;
|
return data.adminGetUser;
|
||||||
},
|
},
|
||||||
{ userId },
|
{ userId },
|
||||||
|
|||||||
@@ -75,7 +75,9 @@
|
|||||||
class="w-24 h-24 rounded-full mx-auto object-cover ring-4 ring-primary/20 group-hover:ring-primary/40 transition-all bg-muted"
|
class="w-24 h-24 rounded-full mx-auto object-cover ring-4 ring-primary/20 group-hover:ring-primary/40 transition-all bg-muted"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="font-semibold text-lg group-hover:text-primary transition-colors">{model.artist_name}</h3>
|
<h3 class="font-semibold text-lg group-hover:text-primary transition-colors">
|
||||||
|
{model.artist_name}
|
||||||
|
</h3>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</a>
|
</a>
|
||||||
@@ -110,7 +112,9 @@
|
|||||||
class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent group-hover:scale-105 transition-transform duration-300"
|
class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent group-hover:scale-105 transition-transform duration-300"
|
||||||
></div>
|
></div>
|
||||||
<div class="absolute bottom-2 left-2 text-white text-sm font-medium">
|
<div class="absolute bottom-2 left-2 text-white text-sm font-medium">
|
||||||
{#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if}
|
{#if video.movie_file?.duration}{formatVideoDuration(
|
||||||
|
video.movie_file.duration,
|
||||||
|
)}{/if}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
|||||||
@@ -66,8 +66,8 @@
|
|||||||
});
|
});
|
||||||
toast.success($_("admin.article_form.update_success"));
|
toast.success($_("admin.article_form.update_success"));
|
||||||
goto("/admin/articles");
|
goto("/admin/articles");
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
toast.error(e?.message ?? $_("admin.article_form.update_error"));
|
toast.error((e instanceof Error ? e.message : null) ?? $_("admin.article_form.update_error"));
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,8 +67,8 @@
|
|||||||
});
|
});
|
||||||
toast.success($_("admin.article_form.create_success"));
|
toast.success($_("admin.article_form.create_success"));
|
||||||
goto("/admin/articles");
|
goto("/admin/articles");
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
toast.error(e?.message ?? $_("admin.article_form.create_error"));
|
toast.error((e instanceof Error ? e.message : null) ?? $_("admin.article_form.create_error"));
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,8 +91,8 @@
|
|||||||
isAdmin,
|
isAdmin,
|
||||||
});
|
});
|
||||||
toast.success($_("admin.user_edit.save_success"));
|
toast.success($_("admin.user_edit.save_success"));
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
toast.error(e?.message ?? $_("admin.user_edit.save_error"));
|
toast.error((e instanceof Error ? e.message : null) ?? $_("admin.user_edit.save_error"));
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,8 +77,8 @@
|
|||||||
await setVideoModels(data.video.id, selectedModelIds);
|
await setVideoModels(data.video.id, selectedModelIds);
|
||||||
toast.success($_("admin.video_form.update_success"));
|
toast.success($_("admin.video_form.update_success"));
|
||||||
goto("/admin/videos");
|
goto("/admin/videos");
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
toast.error(e?.message ?? $_("admin.video_form.update_error"));
|
toast.error((e instanceof Error ? e.message : null) ?? $_("admin.video_form.update_error"));
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,8 +89,8 @@
|
|||||||
}
|
}
|
||||||
toast.success($_("admin.video_form.create_success"));
|
toast.success($_("admin.video_form.create_success"));
|
||||||
goto("/admin/videos");
|
goto("/admin/videos");
|
||||||
} catch (e: any) {
|
} catch (e) {
|
||||||
toast.error(e?.message ?? $_("admin.video_form.create_error"));
|
toast.error((e instanceof Error ? e.message : null) ?? $_("admin.video_form.create_error"));
|
||||||
} finally {
|
} finally {
|
||||||
saving = false;
|
saving = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,9 @@
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
await login(email, password);
|
await login(email, password);
|
||||||
goto("/videos", { invalidateAll: true });
|
goto("/videos", { invalidateAll: true });
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
const raw = err.response?.errors?.[0]?.message ?? err.message;
|
const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
|
||||||
|
const raw = e.response?.errors?.[0]?.message ?? e.message;
|
||||||
error = raw === "Invalid credentials" ? $_("auth.login.error_invalid_credentials") : raw;
|
error = raw === "Invalid credentials" ? $_("auth.login.error_invalid_credentials") : raw;
|
||||||
isError = true;
|
isError = true;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -315,8 +315,8 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
disabled={data.page <= 1}
|
disabled={data.page <= 1}
|
||||||
onclick={() => goToPage(data.page - 1)}
|
onclick={() => goToPage(data.page - 1)}
|
||||||
class="border-primary/20 hover:bg-primary/10"
|
class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
|
||||||
>{$_("common.previous")}</Button>
|
>
|
||||||
{#each pageNumbers() as p, i (i)}
|
{#each pageNumbers() as p, i (i)}
|
||||||
{#if p === -1}
|
{#if p === -1}
|
||||||
<span class="px-2 text-muted-foreground select-none">…</span>
|
<span class="px-2 text-muted-foreground select-none">…</span>
|
||||||
@@ -327,8 +327,8 @@
|
|||||||
onclick={() => goToPage(p)}
|
onclick={() => goToPage(p)}
|
||||||
class={p === data.page
|
class={p === data.page
|
||||||
? "bg-gradient-to-r from-primary to-accent min-w-9"
|
? "bg-gradient-to-r from-primary to-accent min-w-9"
|
||||||
: "border-primary/20 hover:bg-primary/10 min-w-9"}
|
: "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
|
||||||
>{p}</Button>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<Button
|
<Button
|
||||||
@@ -336,8 +336,8 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
disabled={data.page >= totalPages}
|
disabled={data.page >= totalPages}
|
||||||
onclick={() => goToPage(data.page + 1)}
|
onclick={() => goToPage(data.page + 1)}
|
||||||
class="border-primary/20 hover:bg-primary/10"
|
class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
|
||||||
>{$_("common.next")}</Button>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{$_("common.total_results", { values: { total: data.total } })}
|
{$_("common.total_results", { values: { total: data.total } })}
|
||||||
|
|||||||
@@ -153,10 +153,15 @@
|
|||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<h3 class="font-semibold text-lg mb-1">About {author.artist_name}</h3>
|
<h3 class="font-semibold text-lg mb-1">About {author.artist_name}</h3>
|
||||||
{#if author.description}
|
{#if author.description}
|
||||||
<p class="text-sm text-muted-foreground mb-3 leading-relaxed">{author.description}</p>
|
<p class="text-sm text-muted-foreground mb-3 leading-relaxed">
|
||||||
|
{author.description}
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if author.slug}
|
{#if author.slug}
|
||||||
<a href="/models/{author.slug}" class="inline-flex items-center gap-1 text-sm text-primary hover:underline">
|
<a
|
||||||
|
href="/models/{author.slug}"
|
||||||
|
class="inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
||||||
|
>
|
||||||
View profile
|
View profile
|
||||||
<span class="icon-[ri--arrow-right-line] w-3.5 h-3.5"></span>
|
<span class="icon-[ri--arrow-right-line] w-3.5 h-3.5"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -90,8 +90,9 @@
|
|||||||
});
|
});
|
||||||
toast.success($_("me.settings.toast_update"));
|
toast.success($_("me.settings.toast_update"));
|
||||||
invalidateAll();
|
invalidateAll();
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
profileError = err.response?.errors?.[0]?.message ?? err.message;
|
const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
|
||||||
|
profileError = e.response?.errors?.[0]?.message ?? e.message ?? "Unknown error";
|
||||||
isProfileError = true;
|
isProfileError = true;
|
||||||
} finally {
|
} finally {
|
||||||
isProfileLoading = false;
|
isProfileLoading = false;
|
||||||
@@ -114,8 +115,9 @@
|
|||||||
toast.success($_("me.settings.toast_update"));
|
toast.success($_("me.settings.toast_update"));
|
||||||
invalidateAll();
|
invalidateAll();
|
||||||
password = confirmPassword = "";
|
password = confirmPassword = "";
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
securityError = err.response?.errors?.[0]?.message ?? err.message;
|
const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
|
||||||
|
securityError = e.response?.errors?.[0]?.message ?? e.message ?? "Unknown error";
|
||||||
isSecurityError = true;
|
isSecurityError = true;
|
||||||
} finally {
|
} finally {
|
||||||
isSecurityLoading = false;
|
isSecurityLoading = false;
|
||||||
@@ -290,9 +292,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</FileDropZone>
|
</FileDropZone>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">JPG, PNG · max 2 MB</p>
|
||||||
JPG, PNG · max 2 MB
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-muted-foreground/70">
|
<p class="text-xs text-muted-foreground/70">
|
||||||
Click or drop to {avatar ? "change" : "upload"}
|
Click or drop to {avatar ? "change" : "upload"}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -180,7 +180,6 @@
|
|||||||
<!-- <span>{model.videos} videos</span> -->
|
<!-- <span>{model.videos} videos</span> -->
|
||||||
<!-- category not available -->
|
<!-- category not available -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</a>
|
</a>
|
||||||
@@ -205,8 +204,8 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
disabled={data.page <= 1}
|
disabled={data.page <= 1}
|
||||||
onclick={() => goToPage(data.page - 1)}
|
onclick={() => goToPage(data.page - 1)}
|
||||||
class="border-primary/20 hover:bg-primary/10"
|
class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
|
||||||
>{$_("common.previous")}</Button>
|
>
|
||||||
{#each pageNumbers() as p, i (i)}
|
{#each pageNumbers() as p, i (i)}
|
||||||
{#if p === -1}
|
{#if p === -1}
|
||||||
<span class="px-2 text-muted-foreground select-none">…</span>
|
<span class="px-2 text-muted-foreground select-none">…</span>
|
||||||
@@ -217,8 +216,8 @@
|
|||||||
onclick={() => goToPage(p)}
|
onclick={() => goToPage(p)}
|
||||||
class={p === data.page
|
class={p === data.page
|
||||||
? "bg-gradient-to-r from-primary to-accent min-w-9"
|
? "bg-gradient-to-r from-primary to-accent min-w-9"
|
||||||
: "border-primary/20 hover:bg-primary/10 min-w-9"}
|
: "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
|
||||||
>{p}</Button>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<Button
|
<Button
|
||||||
@@ -226,8 +225,8 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
disabled={data.page >= totalPages}
|
disabled={data.page >= totalPages}
|
||||||
onclick={() => goToPage(data.page + 1)}
|
onclick={() => goToPage(data.page + 1)}
|
||||||
class="border-primary/20 hover:bg-primary/10"
|
class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
|
||||||
>{$_("common.next")}</Button>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{$_("common.total_results", { values: { total: data.total } })}
|
{$_("common.total_results", { values: { total: data.total } })}
|
||||||
|
|||||||
@@ -30,8 +30,9 @@
|
|||||||
await requestPassword(email);
|
await requestPassword(email);
|
||||||
toast.success($_("auth.password_request.toast_request", { values: { email } }));
|
toast.success($_("auth.password_request.toast_request", { values: { email } }));
|
||||||
goto("/login");
|
goto("/login");
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
error = err.response?.errors?.[0]?.message ?? err.message;
|
const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
|
||||||
|
error = e.response?.errors?.[0]?.message ?? e.message ?? "Unknown error";
|
||||||
isError = true;
|
isError = true;
|
||||||
} finally {
|
} finally {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|||||||
@@ -39,8 +39,9 @@
|
|||||||
await resetPassword(data.token, password);
|
await resetPassword(data.token, password);
|
||||||
toast.success($_("auth.password_reset.toast_reset"));
|
toast.success($_("auth.password_reset.toast_reset"));
|
||||||
goto("/login");
|
goto("/login");
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
const raw = err.response?.errors?.[0]?.message ?? err.message;
|
const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
|
||||||
|
const raw = e.response?.errors?.[0]?.message ?? e.message;
|
||||||
const tokenErrors = ["Invalid or expired reset token", "Reset token expired"];
|
const tokenErrors = ["Invalid or expired reset token", "Reset token expired"];
|
||||||
error = tokenErrors.includes(raw) ? $_("auth.password_reset.error_invalid_token") : raw;
|
error = tokenErrors.includes(raw) ? $_("auth.password_reset.error_invalid_token") : raw;
|
||||||
isError = true;
|
isError = true;
|
||||||
|
|||||||
@@ -47,8 +47,9 @@
|
|||||||
await register(email, password, firstName, lastName);
|
await register(email, password, firstName, lastName);
|
||||||
toast.success($_("auth.signup.toast_register", { values: { email } }));
|
toast.success($_("auth.signup.toast_register", { values: { email } }));
|
||||||
goto("/login");
|
goto("/login");
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
const raw = err.response?.errors?.[0]?.message ?? err.message;
|
const e = err as { response?: { errors?: Array<{ message: string }> }; message?: string };
|
||||||
|
const raw = e.response?.errors?.[0]?.message ?? e.message;
|
||||||
error = raw === "Email already registered" ? $_("auth.signup.error_email_taken") : raw;
|
error = raw === "Email already registered" ? $_("auth.signup.error_email_taken") : raw;
|
||||||
isError = true;
|
isError = true;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
const filteredItems = $derived(() => {
|
const filteredItems = $derived(() => {
|
||||||
return data.items
|
return data.items
|
||||||
.filter((item: any) => {
|
.filter((item) => {
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
searchQuery === "" || item.title.toLowerCase().includes(searchQuery.toLowerCase());
|
searchQuery === "" || item.title.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
const matchesCategory = categoryFilter === "all" || item.category === categoryFilter;
|
const matchesCategory = categoryFilter === "all" || item.category === categoryFilter;
|
||||||
@@ -72,7 +72,9 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Select type="single" bind:value={categoryFilter}>
|
<Select type="single" bind:value={categoryFilter}>
|
||||||
<SelectTrigger class="w-full md:w-48 bg-background/50 border-primary/20 focus:border-primary">
|
<SelectTrigger
|
||||||
|
class="w-full md:w-48 bg-background/50 border-primary/20 focus:border-primary"
|
||||||
|
>
|
||||||
<span class="icon-[ri--filter-line] w-4 h-4 mr-2"></span>
|
<span class="icon-[ri--filter-line] w-4 h-4 mr-2"></span>
|
||||||
{categoryFilter === "all"
|
{categoryFilter === "all"
|
||||||
? $_("tags.categories.all")
|
? $_("tags.categories.all")
|
||||||
|
|||||||
@@ -161,7 +161,9 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute bottom-3 left-3 bg-black/70 text-white text-sm px-2 py-1 rounded font-medium"
|
class="absolute bottom-3 left-3 bg-black/70 text-white text-sm px-2 py-1 rounded font-medium"
|
||||||
>
|
>
|
||||||
{#if video.movie_file?.duration}{formatVideoDuration(video.movie_file.duration)}{/if}
|
{#if video.movie_file?.duration}{formatVideoDuration(
|
||||||
|
video.movie_file.duration,
|
||||||
|
)}{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Premium Badge -->
|
<!-- Premium Badge -->
|
||||||
@@ -236,7 +238,6 @@
|
|||||||
{video.category}
|
{video.category}
|
||||||
</span> -->
|
</span> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</a>
|
</a>
|
||||||
@@ -263,8 +264,8 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
disabled={data.page <= 1}
|
disabled={data.page <= 1}
|
||||||
onclick={() => goToPage(data.page - 1)}
|
onclick={() => goToPage(data.page - 1)}
|
||||||
class="border-primary/20 hover:bg-primary/10"
|
class="border-primary/20 hover:bg-primary/10">{$_("common.previous")}</Button
|
||||||
>{$_("common.previous")}</Button>
|
>
|
||||||
{#each pageNumbers() as p, i (i)}
|
{#each pageNumbers() as p, i (i)}
|
||||||
{#if p === -1}
|
{#if p === -1}
|
||||||
<span class="px-2 text-muted-foreground select-none">…</span>
|
<span class="px-2 text-muted-foreground select-none">…</span>
|
||||||
@@ -275,8 +276,8 @@
|
|||||||
onclick={() => goToPage(p)}
|
onclick={() => goToPage(p)}
|
||||||
class={p === data.page
|
class={p === data.page
|
||||||
? "bg-gradient-to-r from-primary to-accent min-w-9"
|
? "bg-gradient-to-r from-primary to-accent min-w-9"
|
||||||
: "border-primary/20 hover:bg-primary/10 min-w-9"}
|
: "border-primary/20 hover:bg-primary/10 min-w-9"}>{p}</Button
|
||||||
>{p}</Button>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
<Button
|
<Button
|
||||||
@@ -284,8 +285,8 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
disabled={data.page >= totalPages}
|
disabled={data.page >= totalPages}
|
||||||
onclick={() => goToPage(data.page + 1)}
|
onclick={() => goToPage(data.page + 1)}
|
||||||
class="border-primary/20 hover:bg-primary/10"
|
class="border-primary/20 hover:bg-primary/10">{$_("common.next")}</Button
|
||||||
>{$_("common.next")}</Button>
|
>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-muted-foreground">
|
<p class="text-sm text-muted-foreground">
|
||||||
{$_("common.total_results", { values: { total: data.total } })}
|
{$_("common.total_results", { values: { total: data.total } })}
|
||||||
|
|||||||
@@ -58,8 +58,8 @@
|
|||||||
isLiked = true;
|
isLiked = true;
|
||||||
toast.success("Added to liked videos");
|
toast.success("Added to liked videos");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
toast.error(error.message || "Failed to update like");
|
toast.error((error instanceof Error ? error.message : null) || "Failed to update like");
|
||||||
} finally {
|
} finally {
|
||||||
isLikeLoading = false;
|
isLikeLoading = false;
|
||||||
}
|
}
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
invalidateAll();
|
invalidateAll();
|
||||||
newComment = "";
|
newComment = "";
|
||||||
showComments = true;
|
showComments = true;
|
||||||
} catch (err: any) {
|
} catch (err) {
|
||||||
commentError = err.message;
|
commentError = err instanceof Error ? err.message : "Unknown error";
|
||||||
isCommentError = true;
|
isCommentError = true;
|
||||||
} finally {
|
} finally {
|
||||||
isCommentLoading = false;
|
isCommentLoading = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user