diff --git a/packages/backend/src/db/schema/users.ts b/packages/backend/src/db/schema/users.ts index bd09042..36b2630 100644 --- a/packages/backend/src/db/schema/users.ts +++ b/packages/backend/src/db/schema/users.ts @@ -29,6 +29,7 @@ export const users = pgTable( role: roleEnum("role").notNull().default("viewer"), avatar: text("avatar").references(() => files.id, { onDelete: "set null" }), banner: text("banner").references(() => files.id, { onDelete: "set null" }), + photo: text("photo").references(() => files.id, { onDelete: "set null" }), is_admin: boolean("is_admin").notNull().default(false), email_verified: boolean("email_verified").notNull().default(false), email_verify_token: text("email_verify_token"), diff --git a/packages/backend/src/graphql/resolvers/users.ts b/packages/backend/src/graphql/resolvers/users.ts index 90318f8..6d3cd94 100644 --- a/packages/backend/src/graphql/resolvers/users.ts +++ b/packages/backend/src/graphql/resolvers/users.ts @@ -134,6 +134,7 @@ builder.mutationField("adminUpdateUser", (t) => artistName: t.arg.string(), avatarId: t.arg.string(), bannerId: t.arg.string(), + photoId: t.arg.string(), }, resolve: async (_root, args, ctx) => { requireAdmin(ctx); @@ -149,6 +150,7 @@ builder.mutationField("adminUpdateUser", (t) => updates.artist_name = args.artistName; if (args.avatarId !== undefined && args.avatarId !== null) updates.avatar = args.avatarId; if (args.bannerId !== undefined && args.bannerId !== null) updates.banner = args.bannerId; + if (args.photoId !== undefined && args.photoId !== null) updates.photo = args.photoId; const updated = await ctx.db .update(users) diff --git a/packages/backend/src/graphql/types/index.ts b/packages/backend/src/graphql/types/index.ts index 5d32214..28d5d88 100644 --- a/packages/backend/src/graphql/types/index.ts +++ b/packages/backend/src/graphql/types/index.ts @@ -55,6 +55,7 @@ export const UserType = builder.objectRef("User").implement({ is_admin: t.exposeBoolean("is_admin"), avatar: t.exposeString("avatar", { nullable: true }), banner: t.exposeString("banner", { nullable: true }), + photo: t.exposeString("photo", { nullable: true }), email_verified: t.exposeBoolean("email_verified"), date_created: t.expose("date_created", { type: "DateTime" }), }), @@ -75,6 +76,7 @@ export const CurrentUserType = builder.objectRef("CurrentUser").implement( is_admin: t.exposeBoolean("is_admin"), avatar: t.exposeString("avatar", { nullable: true }), banner: t.exposeString("banner", { nullable: true }), + photo: t.exposeString("photo", { nullable: true }), email_verified: t.exposeBoolean("email_verified"), date_created: t.expose("date_created", { type: "DateTime" }), }), @@ -133,6 +135,7 @@ export const ModelType = builder.objectRef("Model").implement({ description: t.exposeString("description", { nullable: true }), avatar: t.exposeString("avatar", { nullable: true }), banner: t.exposeString("banner", { nullable: true }), + photo: t.exposeString("photo", { nullable: true }), tags: t.exposeStringList("tags", { nullable: true }), date_created: t.expose("date_created", { type: "DateTime" }), photos: t.expose("photos", { type: [ModelPhotoType], nullable: true }), @@ -416,6 +419,7 @@ export const AdminUserDetailType = builder.objectRef("AdminUser is_admin: t.exposeBoolean("is_admin"), avatar: t.exposeString("avatar", { nullable: true }), banner: t.exposeString("banner", { nullable: true }), + photo: t.exposeString("photo", { nullable: true }), email_verified: t.exposeBoolean("email_verified"), date_created: t.expose("date_created", { type: "DateTime" }), photos: t.expose("photos", { type: [ModelPhotoType] }), diff --git a/packages/backend/src/migrations/0003_model_photo.sql b/packages/backend/src/migrations/0003_model_photo.sql new file mode 100644 index 0000000..b53c283 --- /dev/null +++ b/packages/backend/src/migrations/0003_model_photo.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "photo" text REFERENCES "files"("id") ON DELETE set null; diff --git a/packages/frontend/src/lib/i18n/locales/en.ts b/packages/frontend/src/lib/i18n/locales/en.ts index 044c674..8d3c05c 100644 --- a/packages/frontend/src/lib/i18n/locales/en.ts +++ b/packages/frontend/src/lib/i18n/locales/en.ts @@ -962,6 +962,10 @@ export default { artist_name: "Artist name", avatar: "Avatar", banner: "Banner", + model_photo: "Model photo", + model_photo_hint: "Used in model cards and on the model profile page. Avatar is used for comments and article authors.", + model_photo_uploaded: "Model photo uploaded", + model_photo_failed: "Model photo upload failed", is_admin: "Administrator", is_admin_hint: "Grants full admin access to the dashboard", photos: "Photo gallery", diff --git a/packages/frontend/src/lib/services.ts b/packages/frontend/src/lib/services.ts index dbbcb24..d5c2cfe 100644 --- a/packages/frontend/src/lib/services.ts +++ b/packages/frontend/src/lib/services.ts @@ -491,6 +491,7 @@ const MODELS_QUERY = gql` description avatar banner + photo tags date_created photos { @@ -540,6 +541,7 @@ const MODEL_BY_SLUG_QUERY = gql` description avatar banner + photo tags date_created photos { @@ -1151,6 +1153,7 @@ const ADMIN_UPDATE_USER_MUTATION = gql` $artistName: String $avatarId: String $bannerId: String + $photoId: String ) { adminUpdateUser( userId: $userId @@ -1161,6 +1164,7 @@ const ADMIN_UPDATE_USER_MUTATION = gql` artistName: $artistName avatarId: $avatarId bannerId: $bannerId + photoId: $photoId ) { id email @@ -1171,6 +1175,7 @@ const ADMIN_UPDATE_USER_MUTATION = gql` is_admin avatar banner + photo date_created } } @@ -1185,6 +1190,7 @@ export async function adminUpdateUser(input: { artistName?: string; avatarId?: string; bannerId?: string; + photoId?: string; }) { return loggedApiCall( "adminUpdateUser", @@ -1228,6 +1234,7 @@ const ADMIN_GET_USER_QUERY = gql` is_admin avatar banner + photo description tags email_verified diff --git a/packages/frontend/src/routes/admin/users/[id]/+page.svelte b/packages/frontend/src/routes/admin/users/[id]/+page.svelte index d1031d4..f3368f1 100644 --- a/packages/frontend/src/routes/admin/users/[id]/+page.svelte +++ b/packages/frontend/src/routes/admin/users/[id]/+page.svelte @@ -21,6 +21,7 @@ let artistName = $state(data.user.artist_name ?? ""); let avatarId = $state(data.user.avatar ?? null); let bannerId = $state(data.user.banner ?? null); + let photoId = $state(data.user.photo ?? null); let isAdmin = $state(data.user.is_admin ?? false); let saving = $state(false); @@ -52,6 +53,20 @@ } } + async function handlePhotoUpload2(files: File[]) { + const file = files[0]; + if (!file) return; + const fd = new FormData(); + fd.append("file", file); + try { + const res = await uploadFile(fd); + photoId = res.id; + toast.success($_("admin.user_edit.model_photo_uploaded")); + } catch { + toast.error($_("admin.user_edit.model_photo_failed")); + } + } + async function handlePhotoUpload(files: File[]) { for (const file of files) { const fd = new FormData(); @@ -88,6 +103,7 @@ artistName: artistName || undefined, avatarId: avatarId || undefined, bannerId: bannerId || undefined, + photoId: photoId || undefined, isAdmin, }); toast.success($_("admin.user_edit.save_success")); @@ -158,6 +174,20 @@ + +
+ +

{$_("admin.user_edit.model_photo_hint")}

+ {#if photoId} + + {/if} + +
+