From cd90281cf92969e21945a1f008705aa79a4f1497 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Wed, 25 Jun 2025 22:48:12 +0200 Subject: [PATCH] feat: upload pfp --- .../channel/[channelName]/page.client.tsx | 129 ++++++++++++++---- apps/web/src/lib/form/actions.ts | 24 ++-- apps/web/src/lib/form/zod.ts | 5 +- .../lib/services/uploadthing/fileRouter.ts | 4 +- 4 files changed, 120 insertions(+), 42 deletions(-) diff --git a/apps/web/src/app/(protected)/settings/channel/[channelName]/page.client.tsx b/apps/web/src/app/(protected)/settings/channel/[channelName]/page.client.tsx index f45f442..fb0b062 100644 --- a/apps/web/src/app/(protected)/settings/channel/[channelName]/page.client.tsx +++ b/apps/web/src/app/(protected)/settings/channel/[channelName]/page.client.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } 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'; @@ -43,6 +43,7 @@ import { UserCombobox } from '@/components/app/UserCombobox/UserCombobox'; import { parseAsString, useQueryState } from 'nuqs'; import { Write } from '@/components/ui/channel-desc-fancy-area/write'; import { Preview } from '@/components/ui/channel-desc-fancy-area/preview'; +import { UploadButton } from '@/lib/uploadthing'; interface ChannelSettingsClientProps { channel: Channel & { @@ -70,7 +71,8 @@ export default function ChannelSettingsClient({ const [keyVisible, setKeyVisible] = useState(false); const [copied, setCopied] = useState(false); const [selTab, setSelTab] = useQueryState('tabs', parseAsString.withDefault('general')); - const [textValue, setTextValue] = useState(channel.description); + const [isUploading, setIsUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); const copyStreamKey = async () => { if (streamKey) { @@ -111,7 +113,7 @@ export default function ChannelSettingsClient({

{channel.name}

-

Channel Settings

+

Channel Settings

{channel.followers.length} follower{channel.followers.length !== 1 ? 's' : ''} @@ -156,12 +158,88 @@ export default function ChannelSettingsClient({ { + return ( +
+ + + {field.value && ( +
+ + + {channel.name[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!'); + } + }} + 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. +

+ )} +
+
+ ); + }, }, { name: 'description', @@ -176,16 +254,15 @@ export default function ChannelSettingsClient({ Preview - { field.onChange(value); - setTextValue(value); - }} + }} /> - +
@@ -249,7 +326,7 @@ export default function ChannelSettingsClient({

Stream Key

-

+

Use this key to start streaming to your channel. Keep it secure!

@@ -258,7 +335,7 @@ export default function ChannelSettingsClient({ type={keyVisible ? 'text' : 'password'} value={streamKey} readOnly - className="w-full px-3 py-2 border rounded-md bg-muted font-mono text-sm" + className="w-full px-3 py-2 border rounded-md bg-mantle font-mono text-sm" />
) : ( -

No stream information available.

+

No stream information available.

)}
@@ -345,10 +422,7 @@ export default function ChannelSettingsClient({ {isOwner && ( m.id), - channel.owner.id, - ]} + existingManagers={[...channel.managers.map((m) => m.id), channel.owner.id]} /> )}
@@ -365,7 +439,7 @@ export default function ChannelSettingsClient({

{channel.ownerPersonalChannel?.name}

-

Channel Owner

+

Channel Owner

@@ -391,7 +465,7 @@ export default function ChannelSettingsClient({

{personalChannel?.name}

-

Manager

+

Manager

{isOwner && ( @@ -412,7 +486,7 @@ export default function ChannelSettingsClient({ })} {channel.managers.length === 0 && ( -

+

No managers added yet.

)} @@ -436,7 +510,7 @@ export default function ChannelSettingsClient({

Stream Notifications

-

+

Send notifications to followers when you go live

@@ -446,9 +520,11 @@ export default function ChannelSettingsClient({ toast.promise(toggleGlobalChannelNotifs(channel.id), { loading: 'Updating notifications...', success(data) { - return `${data.toggle ? 'Enabled' : 'Disabled'} global notifications for this channel.` + return `${ + data.toggle ? 'Enabled' : 'Disabled' + } global notifications for this channel.`; }, - }) + }); }} />
@@ -470,7 +546,10 @@ export default function ChannelSettingsClient({
{personalChannel?.name} - + {personalChannel?.name}
@@ -481,7 +560,7 @@ export default function ChannelSettingsClient({ ); })} {channel.followers.length === 0 && ( -

No followers yet.

+

No followers yet.

)} diff --git a/apps/web/src/lib/form/actions.ts b/apps/web/src/lib/form/actions.ts index 5d02506..0fcb435 100644 --- a/apps/web/src/lib/form/actions.ts +++ b/apps/web/src/lib/form/actions.ts @@ -163,11 +163,15 @@ export async function updateChannelSettings(prev: any, formData: FormData) { if (!user) { return { success: false, error: 'Unauthorized' }; } - + const zod = await zodVerify(updateChannelSettingsSchema, formData); + const urlRegex = /(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/gm; if (!zod.success) { return zod; } + if (zod.data.pfpUrl && !urlRegex.test(zod.data.pfpUrl)) { + return { success: false, error: 'Invalid URL for profile picture' }; + } const channel = await prisma.channel.findUnique({ where: { id: zod.data.channelId }, @@ -188,21 +192,17 @@ export async function updateChannelSettings(prev: any, formData: FormData) { return { success: false, error: 'Unauthorized' }; } - // Only owners can change certain settings - const updateData: any = {}; - if (zod.data.name && isOwner) { - updateData.name = zod.data.name; - } - if (zod.data.pfpUrl) { - updateData.pfpUrl = zod.data.pfpUrl; - } - if (zod.data.description !== undefined) { - updateData.description = zod.data.description; + if (zod.data.pfpUrl === '') { + const identicon = await genIdenticonUpload(channel.name, 'pfp'); + zod.data.pfpUrl = identicon; } await prisma.channel.update({ where: { id: zod.data.channelId }, - data: updateData, + data: { + description: zod.data.description || undefined, + pfpUrl: zod.data.pfpUrl, + }, }); revalidatePath(`/settings/channel/${channel.name}`); diff --git a/apps/web/src/lib/form/zod.ts b/apps/web/src/lib/form/zod.ts index a374da5..7a72682 100644 --- a/apps/web/src/lib/form/zod.ts +++ b/apps/web/src/lib/form/zod.ts @@ -22,7 +22,6 @@ export const createChannelSchema = z.object({ export const updateChannelSettingsSchema = z.object({ channelId: z.string().min(1), - name: username.optional(), - pfpUrl: z.string().url().optional(), - description: z.string().optional(), + pfpUrl: z.string(), + description: z.string().min(1).max(500), }); \ No newline at end of file diff --git a/apps/web/src/lib/services/uploadthing/fileRouter.ts b/apps/web/src/lib/services/uploadthing/fileRouter.ts index 854b7c7..2eb01d9 100644 --- a/apps/web/src/lib/services/uploadthing/fileRouter.ts +++ b/apps/web/src/lib/services/uploadthing/fileRouter.ts @@ -12,13 +12,13 @@ const auth = async () => { // FileRouter for your app, can contain multiple FileRoutes export const ourFileRouter = { // Define as many FileRoutes as you like, each with a unique routeSlug - imageUploader: f({ + pfpUpload: f({ image: { /** * For full list of options and defaults, see the File Route API reference * @see https://docs.uploadthing.com/file-routes#route-config */ - maxFileSize: "4MB", + maxFileSize: "1MB", maxFileCount: 1, }, })