From 462a51e376f3c4a7e7c4283e9fe368d18d241b3c Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:20:24 +0100 Subject: [PATCH] feat(bot): upload profile picture --- .../settings/bot/[slug]/gensettings.tsx | 109 ++++++++++++++++-- .../channel/[channelName]/page.client.tsx | 7 +- .../app/UniversalForm/UniversalForm.tsx | 3 +- .../src/components/app/UniversalForm/types.ts | 19 +-- apps/web/src/lib/form/actions.ts | 9 +- apps/web/src/lib/form/zod.ts | 1 + .../lib/services/uploadthing/fileRouter.ts | 2 +- 7 files changed, 129 insertions(+), 21 deletions(-) diff --git a/apps/web/src/app/(ui)/(protected)/settings/bot/[slug]/gensettings.tsx b/apps/web/src/app/(ui)/(protected)/settings/bot/[slug]/gensettings.tsx index fb0d1bf..c1af27b 100644 --- a/apps/web/src/app/(ui)/(protected)/settings/bot/[slug]/gensettings.tsx +++ b/apps/web/src/app/(ui)/(protected)/settings/bot/[slug]/gensettings.tsx @@ -1,18 +1,21 @@ 'use client'; import { UniversalForm } from '@/components/app/UniversalForm/UniversalForm'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { editBot } from '@/lib/form/actions'; import { BotAccount } from '@hctv/db'; import { useRouter } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { UploadButton } from '@/lib/uploadthing'; +import { toast } from 'sonner'; +import React from 'react'; export function GeneralSettings(props: BotAccount) { const router = useRouter(); + const [isUploading, setIsUploading] = React.useState(false); + const [uploadError, setUploadError] = React.useState(null); + const formRef = React.useRef(null); + return ( @@ -21,6 +24,7 @@ export function GeneralSettings(props: BotAccount) { { + return ( +
+ + + {field.value && ( +
+ + + {props.displayName[0]?.toUpperCase()} + +
+

Current profile picture

+

+ Click "Upload new image" to replace +

+
+ +
+ )} + +
+ { + setIsUploading(true); + setUploadError(null); + }} + onClientUploadComplete={(res) => { + setIsUploading(false); + if (res && res[0]) { + field.onChange(res[0].ufsUrl); + toast.success('Profile picture uploaded successfully!'); + setTimeout(() => { + formRef.current?.requestSubmit(); + }, 0); + } + }} + onUploadError={(error) => { + setIsUploading(false); + setUploadError(error.message); + toast.error(`Upload failed: ${error.message}`); + }} + disabled={isUploading} + /> + + {isUploading &&

Uploading...

} + + {uploadError &&

{uploadError}

} + + {!field.value && !isUploading && !uploadError && ( +

+ Upload a profile picture for your channel. +

+ )} +
+
+ ); + }, + }, ]} schemaName={'editBot'} action={editBot} + onActionComplete={(result) => { + if (result?.success) { + router.refresh(); + } + }} />
- ) -} \ No newline at end of file + ); +} diff --git a/apps/web/src/app/(ui)/(protected)/settings/channel/[channelName]/page.client.tsx b/apps/web/src/app/(ui)/(protected)/settings/channel/[channelName]/page.client.tsx index 5ca05ad..f04ead4 100644 --- a/apps/web/src/app/(ui)/(protected)/settings/channel/[channelName]/page.client.tsx +++ b/apps/web/src/app/(ui)/(protected)/settings/channel/[channelName]/page.client.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useRef } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; @@ -123,6 +123,7 @@ export default function ChannelSettingsClient({ const [region, setRegion] = useState('hq'); const channelList = useOwnedChannels(); const router = useRouter(); + const channelSettingsFormRef = useRef(null); const handleStreamInfoActionComplete = useCallback((result: any) => { if (result?.success) { @@ -291,6 +292,7 @@ export default function ChannelSettingsClient({ { + channelSettingsFormRef.current?.requestSubmit(); + }, 0); } }} onUploadError={(error) => { diff --git a/apps/web/src/components/app/UniversalForm/UniversalForm.tsx b/apps/web/src/components/app/UniversalForm/UniversalForm.tsx index 0fda06a..de3520a 100644 --- a/apps/web/src/components/app/UniversalForm/UniversalForm.tsx +++ b/apps/web/src/components/app/UniversalForm/UniversalForm.tsx @@ -47,6 +47,7 @@ export function UniversalForm({ action, onActionComplete, defaultValues, + formRef, submitText = 'Submit', submitClassname, otherSubmitButton, @@ -87,7 +88,7 @@ export function UniversalForm({ return (
- + {fields.map((field) => ( = { name: string; label?: string; type?: HTMLInputTypeAttribute; placeholder?: string; description?: string; - value?: any; + value?: z.input[keyof z.input]; textArea?: boolean; textAreaRows?: number; maxChars?: number; inputFilter?: RegExp; - component?: (props: { field: ControllerRenderProps } & any) => React.ReactNode; + component?: ( + props: { + field: ControllerRenderProps>; + } & Record + ) => React.ReactNode; componentProps?: Record; required?: boolean; }; export type UniversalFormProps = { - fields: FormFieldConfig[]; + fields: FormFieldConfig[]; schemaName: (typeof schemaDb)[number]['name']; action: (prev: any, formData: FormData) => void; onActionComplete?: (result: any) => void; defaultValues?: Partial>; + formRef?: Ref; submitText?: string; submitClassname?: string; otherSubmitButton?: React.ReactNode; submitButtonDivClassname?: string; -}; \ No newline at end of file +}; diff --git a/apps/web/src/lib/form/actions.ts b/apps/web/src/lib/form/actions.ts index 4558fe6..8af8068 100644 --- a/apps/web/src/lib/form/actions.ts +++ b/apps/web/src/lib/form/actions.ts @@ -614,7 +614,7 @@ export async function createBot(prev: any, formData: FormData) { slug: zod.data.slug, ownerId: user.id, description: zod.data.description, - pfpUrl: await genIdenticonUpload(zod.data.slug, 'botpfp'), + pfpUrl: await genIdenticonUpload(zod.data.slug, 'botpfp'), }, }); @@ -647,14 +647,21 @@ export async function editBot(prev: any, formData: FormData) { if (botExists) { return { success: false, error: 'Bot slug already exists' }; } + } + + if (zod.data.pfpUrl === '') { + const identicon = await genIdenticonUpload(zod.data.name, 'pfp'); + zod.data.pfpUrl = identicon; } + // i feel like you could just append the data instead of manually changing each field but oh well const updatedBot = await prisma.botAccount.update({ where: { id: zod.data.from }, data: { displayName: zod.data.name, slug: zod.data.slug, description: zod.data.description, + pfpUrl: zod.data.pfpUrl, }, }); diff --git a/apps/web/src/lib/form/zod.ts b/apps/web/src/lib/form/zod.ts index 97682b2..80d4a6b 100644 --- a/apps/web/src/lib/form/zod.ts +++ b/apps/web/src/lib/form/zod.ts @@ -50,6 +50,7 @@ export const createBotSchema = z.object({ export const editBotSchema = createBotSchema.and( z.object({ from: z.string().min(1), + pfpUrl: z.string(), }) ); diff --git a/apps/web/src/lib/services/uploadthing/fileRouter.ts b/apps/web/src/lib/services/uploadthing/fileRouter.ts index 2eb01d9..3dfcef7 100644 --- a/apps/web/src/lib/services/uploadthing/fileRouter.ts +++ b/apps/web/src/lib/services/uploadthing/fileRouter.ts @@ -20,7 +20,7 @@ export const ourFileRouter = { */ maxFileSize: "1MB", maxFileCount: 1, - }, + }, }) // Set permissions and file types for this FileRoute .middleware(async () => {