mirror of
https://github.com/SrIzan10/echospace.git
synced 2026-06-06 00:56:54 +00:00
chore: change create form to universal and other devex fixes
This commit is contained in:
@@ -1,97 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useFormState } from 'react-dom';
|
||||
import { createSchema, createSchemaType } from '@/lib/forms/zod';
|
||||
import { create } from '@/lib/forms/actions';
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import SubmitButton from '@/components/app/SubmitButton/SubmitButton';
|
||||
import { UniversalForm } from '@/components/app/UniversalForm/UniversalForm';
|
||||
|
||||
// TODO: move form to the new universal form component
|
||||
export default function ProfileForm() {
|
||||
export default function CreateForm() {
|
||||
const router = useRouter();
|
||||
const form = useForm<createSchemaType>({
|
||||
resolver: zodResolver(createSchema),
|
||||
defaultValues: {
|
||||
name: '',
|
||||
description: '',
|
||||
github: '',
|
||||
},
|
||||
});
|
||||
|
||||
const [formState, formAction] = useFormState(create, null);
|
||||
React.useEffect(() => {
|
||||
if (formState && formState.id) {
|
||||
router.push(`/project/${formState.id}`);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formState]);
|
||||
|
||||
return (
|
||||
<div className="min-h-[calc(100vh-4rem)] flex items-center justify-center">
|
||||
<div className="max-w-md w-full p-4">
|
||||
<Form {...form}>
|
||||
<form
|
||||
action={formAction}
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
const formData = new FormData();
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
formAction(formData);
|
||||
})}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Project name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Echospace" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>How the project is called.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="A developer-centric user feedback platform." {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Describe the project a bit.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="github"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Github</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://github.com/SrIzan10/echospace" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Your Github repository link. Will come in handy for some integrations!</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton buttonText='Submit' className='w-full' />
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
<UniversalForm
|
||||
action={create}
|
||||
schemaName="create"
|
||||
fields={[
|
||||
{
|
||||
name: 'name',
|
||||
label: 'Project name',
|
||||
placeholder: 'Echospace',
|
||||
description: 'How the project is called.',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
label: 'Description',
|
||||
placeholder: 'A developer-centric user feedback platform.',
|
||||
description: 'Describe the project a bit.',
|
||||
},
|
||||
{
|
||||
name: 'github',
|
||||
label: 'Github',
|
||||
placeholder: 'https://github.com/SrIzan10/echospace',
|
||||
},
|
||||
]}
|
||||
submitText="Submit"
|
||||
submitClassname="w-full"
|
||||
onActionComplete={(res) => {
|
||||
// @ts-ignore yea
|
||||
if (res && res.id) {
|
||||
// @ts-ignore i stopped caring
|
||||
router.push(`/project/${res.id}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,19 @@ export default async function Page() {
|
||||
userId: user!.id,
|
||||
},
|
||||
});
|
||||
if (db.length === 0) {
|
||||
return (
|
||||
<div className="text-center flex flex-col gap-4 pt-4">
|
||||
<h2>No projects found</h2>
|
||||
<p className="text-muted-foreground">Create a project to get started</p>
|
||||
<Link href="/create">
|
||||
<Button size={'sm'}>Create Project</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4">
|
||||
{db.length === 0 && (
|
||||
<div className="col-span-full text-center">
|
||||
<h1 className="text-2xl font-bold">No projects found</h1>
|
||||
<p className="text-muted-foreground">Create a project to get started</p>
|
||||
<Link href="/create">
|
||||
<Button size={'sm'}>Create Project</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{db.map((d) => (
|
||||
<ProjectCard key={d.id} {...d} />
|
||||
))}
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb"
|
||||
} from '@/components/ui/breadcrumb';
|
||||
|
||||
// TODO: refactor to maybe append the no feedback message to the table div
|
||||
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
|
||||
@@ -30,23 +30,29 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return <div>Project not found</div>;
|
||||
return (
|
||||
<>
|
||||
<h2>Project not found</h2>
|
||||
<p>...or maybe you don't have permission!</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// maybe it's not clean enough but who cares
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<Breadcrumb className='pb-5'>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/dashboard">Dashboard</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{project.name}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<Breadcrumb className="pb-5">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href="/dashboard">Dashboard</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>{project.name}</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="flex items-center justify-between w-full pb-10">
|
||||
<h2>{project.name}</h2>
|
||||
<p className="text-muted-foreground ml-2 truncate">{project.description}</p>
|
||||
@@ -54,16 +60,17 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
||||
<Button>Settings</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{project.feedback.length === 0 ? (
|
||||
<div className="border rounded-lg max-h-[32rem] overflow-y-auto text-center p-10">
|
||||
<h2>No feedback!</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Once you start receiving feedback, it will appear here.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg max-h-[32rem] overflow-y-auto">
|
||||
<Table>
|
||||
<div className="border rounded-lg max-h-[32rem] overflow-y-auto">
|
||||
{project.feedback.length === 0 && (
|
||||
<div className="flex flex-col justify-center items-center p-10">
|
||||
<h2>No feedback!</h2>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Once you start receiving feedback, it will appear here.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Table>
|
||||
{project.feedback.length > 0 && (
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[100px]">ID</TableHead>
|
||||
@@ -73,21 +80,25 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{/* using toReversed to not change the upstream array in case of other data treatments needed */}
|
||||
{project.feedback.toReversed().map((feedback) => (
|
||||
<TableRow key={feedback.id}>
|
||||
<TableCell>{feedback.id}</TableCell>
|
||||
<TableCell>{feedback.message}</TableCell>
|
||||
{Object.entries(JSON.parse(feedback.customData)).map(([key, value]) => (
|
||||
<TableCell key={key}>{value as string}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
<TableBody>
|
||||
{/*
|
||||
using toReversed to not change the upstream array in case of
|
||||
other data treatments needed.
|
||||
why js why
|
||||
*/}
|
||||
{project.feedback.toReversed().map((feedback) => (
|
||||
<TableRow key={feedback.id}>
|
||||
<TableCell>{feedback.id}</TableCell>
|
||||
<TableCell>{feedback.message}</TableCell>
|
||||
{Object.entries(JSON.parse(feedback.customData)).map(([key, value]) => (
|
||||
<TableCell key={key}>{value as string}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -21,19 +21,23 @@ import SubmitButton from '../SubmitButton/SubmitButton';
|
||||
import { useFormState } from 'react-dom';
|
||||
import React from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { createSchema } from '@/lib/forms/zod';
|
||||
|
||||
export const schemaDb = [
|
||||
{ name: 'projectSettings', zod: projectSettingsSchema },
|
||||
{ name: 'ratelimitChange', zod: ratelimitChangeSchema },
|
||||
{ name: 'customData', zod: customDataSchema },
|
||||
{ name: 'create', zod: createSchema },
|
||||
] as const;
|
||||
|
||||
export function UniversalForm<T extends z.ZodType>({
|
||||
fields,
|
||||
schemaName,
|
||||
action,
|
||||
onActionComplete,
|
||||
defaultValues,
|
||||
submitText = 'Submit',
|
||||
submitClassname,
|
||||
}: UniversalFormProps<T>) {
|
||||
const [state, formAction] = useFormState(action, null);
|
||||
const schema = schemaDb.find((s) => s.name === schemaName)?.zod;
|
||||
@@ -54,6 +58,9 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
// @ts-ignore
|
||||
toast.error(state.error);
|
||||
}
|
||||
if (state) {
|
||||
onActionComplete?.(state);
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
return (
|
||||
@@ -81,7 +88,7 @@ export function UniversalForm<T extends z.ZodType>({
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<SubmitButton buttonText={submitText} />
|
||||
<SubmitButton buttonText={submitText} className={submitClassname} />
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,8 @@ export type UniversalFormProps<T extends z.ZodType> = {
|
||||
fields: FormFieldConfig[];
|
||||
schemaName: typeof schemaDb[number]['name'];
|
||||
action: (prev: any, formData: FormData) => void;
|
||||
onActionComplete?: (result: unknown) => void;
|
||||
defaultValues?: Partial<z.infer<T>>;
|
||||
submitText?: string;
|
||||
submitClassname?: string;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user