mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
feat(bot): upload profile picture
This commit is contained in:
@@ -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<string | null>(null);
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -21,6 +24,7 @@ export function GeneralSettings(props: BotAccount) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<UniversalForm
|
||||
formRef={formRef}
|
||||
fields={[
|
||||
{
|
||||
name: 'from',
|
||||
@@ -42,7 +46,7 @@ export function GeneralSettings(props: BotAccount) {
|
||||
label: 'Bot Slug',
|
||||
placeholder: 'Enter bot slug',
|
||||
required: true,
|
||||
value: props.slug
|
||||
value: props.slug,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
@@ -52,11 +56,96 @@ export function GeneralSettings(props: BotAccount) {
|
||||
value: props.description,
|
||||
textArea: true,
|
||||
},
|
||||
{
|
||||
name: 'pfpUrl',
|
||||
label: 'Profile Picture',
|
||||
type: 'url',
|
||||
value: props.pfpUrl,
|
||||
component: ({ field }) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<input type="hidden" {...field} />
|
||||
|
||||
{field.value && (
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarImage src={field.value} alt="Current profile picture" />
|
||||
<AvatarFallback>{props.displayName[0]?.toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Current profile picture</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Click "Upload new image" to replace
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
field.onChange('');
|
||||
setUploadError(null);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<UploadButton
|
||||
endpoint="pfpUpload"
|
||||
className="mt-2 ut-button:bg-mantle ut-button:text-mantle-foreground ut-allowed-content:text-muted-foreground/70"
|
||||
content={{
|
||||
button: field.value ? 'Upload new image' : 'Upload profile picture',
|
||||
allowedContent: 'Image (1MB max)',
|
||||
}}
|
||||
onUploadBegin={() => {
|
||||
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 && <p className="mt-2 text-sm text-primary">Uploading...</p>}
|
||||
|
||||
{uploadError && <p className="mt-2 text-sm text-red-600">{uploadError}</p>}
|
||||
|
||||
{!field.value && !isUploading && !uploadError && (
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Upload a profile picture for your channel.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
schemaName={'editBot'}
|
||||
action={editBot}
|
||||
onActionComplete={(result) => {
|
||||
if (result?.success) {
|
||||
router.refresh();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<MediaMTXRegion>('hq');
|
||||
const channelList = useOwnedChannels();
|
||||
const router = useRouter();
|
||||
const channelSettingsFormRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const handleStreamInfoActionComplete = useCallback((result: any) => {
|
||||
if (result?.success) {
|
||||
@@ -291,6 +292,7 @@ export default function ChannelSettingsClient({
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<UniversalForm
|
||||
formRef={channelSettingsFormRef}
|
||||
fields={[
|
||||
{ name: 'channelId', type: 'hidden', value: channel.id, label: 'Channel ID' },
|
||||
{
|
||||
@@ -346,6 +348,9 @@ export default function ChannelSettingsClient({
|
||||
if (res && res[0]) {
|
||||
field.onChange(res[0].ufsUrl);
|
||||
toast.success('Profile picture uploaded successfully!');
|
||||
setTimeout(() => {
|
||||
channelSettingsFormRef.current?.requestSubmit();
|
||||
}, 0);
|
||||
}
|
||||
}}
|
||||
onUploadError={(error) => {
|
||||
|
||||
@@ -47,6 +47,7 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
action,
|
||||
onActionComplete,
|
||||
defaultValues,
|
||||
formRef,
|
||||
submitText = 'Submit',
|
||||
submitClassname,
|
||||
otherSubmitButton,
|
||||
@@ -87,7 +88,7 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form action={formAction} className="space-y-2">
|
||||
<form ref={formRef} action={formAction} className="space-y-2">
|
||||
{fields.map((field) => (
|
||||
<FormField
|
||||
key={field.name}
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
import { z } from 'zod';
|
||||
import { HTMLInputTypeAttribute } from 'react';
|
||||
import type { HTMLInputTypeAttribute, Ref } from 'react';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import { schemaDb } from './UniversalForm';
|
||||
|
||||
export type FormFieldConfig = {
|
||||
export type FormFieldConfig<T extends z.ZodType> = {
|
||||
name: string;
|
||||
label?: string;
|
||||
type?: HTMLInputTypeAttribute;
|
||||
placeholder?: string;
|
||||
description?: string;
|
||||
value?: any;
|
||||
value?: z.input<T>[keyof z.input<T>];
|
||||
textArea?: boolean;
|
||||
textAreaRows?: number;
|
||||
maxChars?: number;
|
||||
inputFilter?: RegExp;
|
||||
component?: (props: { field: ControllerRenderProps<any, any> } & any) => React.ReactNode;
|
||||
component?: (
|
||||
props: {
|
||||
field: ControllerRenderProps<z.infer<T>>;
|
||||
} & Record<string, unknown>
|
||||
) => React.ReactNode;
|
||||
componentProps?: Record<string, any>;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
export type UniversalFormProps<T extends z.ZodType> = {
|
||||
fields: FormFieldConfig[];
|
||||
fields: FormFieldConfig<T>[];
|
||||
schemaName: (typeof schemaDb)[number]['name'];
|
||||
action: (prev: any, formData: FormData) => void;
|
||||
onActionComplete?: (result: any) => void;
|
||||
defaultValues?: Partial<z.infer<T>>;
|
||||
formRef?: Ref<HTMLFormElement>;
|
||||
submitText?: string;
|
||||
submitClassname?: string;
|
||||
otherSubmitButton?: React.ReactNode;
|
||||
submitButtonDivClassname?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ export const createBotSchema = z.object({
|
||||
export const editBotSchema = createBotSchema.and(
|
||||
z.object({
|
||||
from: z.string().min(1),
|
||||
pfpUrl: z.string(),
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ export const ourFileRouter = {
|
||||
*/
|
||||
maxFileSize: "1MB",
|
||||
maxFileCount: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
// Set permissions and file types for this FileRoute
|
||||
.middleware(async () => {
|
||||
|
||||
Reference in New Issue
Block a user