Skip to content

Commit 542e2c3

Browse files
feat: faculty section edit
1 parent 0074b14 commit 542e2c3

File tree

11 files changed

+875
-80
lines changed

11 files changed

+875
-80
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client';
2+
import { Button } from '~/components/buttons';
3+
import { Dialog } from '~/components/dialog';
4+
import { Card, CardFooter, CardHeader } from '~/components/ui';
5+
import { toast } from '~/lib/hooks';
6+
import { cn } from '~/lib/utils';
7+
import { deleteFacultySelectionElement } from '~/server/actions';
8+
9+
export default function Page({
10+
params: { locale },
11+
searchParams: { topic, id },
12+
}: {
13+
params: { locale: string };
14+
searchParams: { topic?: string; id?: string };
15+
}) {
16+
return (
17+
<Dialog
18+
className={cn(
19+
'container p-0',
20+
'max-w-[calc(100vw-2rem)] sm:max-w-[512px] md:max-w-[640px] lg:max-w-[640px]'
21+
)}
22+
>
23+
<Card className="rounded-lg border bg-background shadow-sm">
24+
<CardHeader className="border-b px-6 py-4">
25+
<h2>Do you really want to delete this information?</h2>
26+
</CardHeader>
27+
<CardFooter className="flex justify-end gap-2 p-6">
28+
<Button
29+
variant="outline"
30+
className="p-1"
31+
onClick={() => window.history.back()}
32+
>
33+
Cancel
34+
</Button>
35+
<Button
36+
variant="primary"
37+
className="p-1"
38+
onClick={async () => {
39+
const result = await deleteFacultySelectionElement(topic, id);
40+
toast({
41+
title: result.success ? 'Deleted' : 'Error',
42+
description: result.message,
43+
variant: result.success ? 'success' : 'error',
44+
});
45+
window.history.back();
46+
}}
47+
>
48+
Delete
49+
</Button>
50+
</CardFooter>
51+
</Card>
52+
</Dialog>
53+
);
54+
}
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
'use client';
2+
import { zodResolver } from '@hookform/resolvers/zod';
3+
import { Control, FieldPath, useForm } from 'react-hook-form';
4+
import z, { ZodFirstPartyTypeKind, ZodSchema } from 'zod';
5+
6+
import { Button } from '~/components/buttons';
7+
import { Input } from '~/components/inputs';
8+
import {
9+
Select,
10+
SelectContent,
11+
SelectItem,
12+
SelectTrigger,
13+
SelectValue,
14+
} from '~/components/inputs/select';
15+
import { CardContent, CardFooter } from '~/components/ui';
16+
import {
17+
FormControl,
18+
FormField,
19+
FormItem,
20+
FormLabel,
21+
FormMessage,
22+
FormProvider,
23+
} from '~/components/ui/form';
24+
import { toast } from '~/lib/hooks';
25+
import {
26+
facultyPersonalDetailsSchema,
27+
facultyProfileSchemas,
28+
} from '~/lib/schemas/faculty-profile';
29+
import {
30+
editFacultyProfilePersonalDetails,
31+
upsertFacultySection,
32+
} from '~/server/actions/faculty-profile';
33+
34+
export function FacultyForm({
35+
locale,
36+
topic,
37+
id,
38+
existingDetails,
39+
}: {
40+
locale: string;
41+
topic: string;
42+
id?: number;
43+
existingDetails?: z.infer<
44+
(typeof facultyProfileSchemas)[keyof typeof facultyProfileSchemas]
45+
> | null;
46+
}) {
47+
const schema =
48+
facultyProfileSchemas[topic as keyof typeof facultyProfileSchemas];
49+
50+
// Set up the form with React Hook Form directly - always call hooks at top level
51+
const form = useForm<z.infer<typeof schema>>({
52+
resolver: zodResolver(schema),
53+
defaultValues:
54+
existingDetails ??
55+
(Object.keys(schema.shape).reduce(
56+
(acc, key) => {
57+
acc[key as keyof typeof schema.shape] = undefined;
58+
return acc;
59+
},
60+
{} as Record<string, unknown>
61+
) as z.infer<typeof schema>),
62+
});
63+
64+
const onSubmit = async (values: z.infer<typeof schema>) => {
65+
const result = await upsertFacultySection(topic, id ?? null, values);
66+
toast({
67+
title: result.success ? 'Success' : 'Error',
68+
description: result.success
69+
? `Successfully ${id ? 'updated' : 'created'} ${topic} details.`
70+
: result.message,
71+
variant: result.success ? 'success' : 'error',
72+
});
73+
window.history.go(-1);
74+
};
75+
76+
return (
77+
<FormProvider {...form}>
78+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
79+
<CardContent className="grid gap-4 p-6 md:grid-cols-2">
80+
{renderFields(form.control, schema.shape)}
81+
</CardContent>
82+
<CardFooter className="flex justify-end border-t pt-4 ">
83+
<Button
84+
type="button"
85+
variant="secondary"
86+
className="mr-2 p-1"
87+
onClick={() => window.history.back()}
88+
>
89+
Cancel
90+
</Button>
91+
<Button type="submit" variant="primary" className="p-1">
92+
{id ? 'Update' : 'Create'}
93+
</Button>
94+
</CardFooter>
95+
</form>
96+
</FormProvider>
97+
);
98+
}
99+
100+
// Remove the extra closing curly brace
101+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
102+
const renderFields = <T extends Record<string, any>>(
103+
formControl: Control<T>,
104+
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
105+
schemaShape: Record<string, ZodSchema<unknown> | never>
106+
) => {
107+
const renderField = (fieldName: string) => {
108+
const fieldSchema = schemaShape[fieldName] as
109+
| z.ZodOptional<z.ZodString>
110+
| z.ZodString
111+
| z.ZodNumber
112+
| z.ZodDate
113+
| z.ZodEnum<[string, ...string[]]>
114+
| z.ZodUnion<[z.ZodDate, z.ZodString]>
115+
| z.ZodEffects<z.ZodDate | z.ZodString>;
116+
const isOptional = fieldSchema.isOptional();
117+
118+
// Generate appropriate label from field name
119+
const label = fieldName
120+
.replace(/([A-Z])/g, ' $1')
121+
.replace(/^./, (str: string) => str.toUpperCase())
122+
.trim();
123+
124+
// Check if it's an enum field
125+
if (fieldSchema._def.typeName === ZodFirstPartyTypeKind.ZodEnum) {
126+
const enumValues = fieldSchema._def.values;
127+
return (
128+
<FormField
129+
key={fieldName}
130+
control={formControl}
131+
name={fieldName as unknown as FieldPath<T>}
132+
render={({ field }) => (
133+
<FormItem>
134+
<FormLabel>{label}</FormLabel>
135+
<FormControl>
136+
<Select
137+
value={field.value}
138+
onValueChange={field.onChange}
139+
variant="form"
140+
>
141+
<SelectTrigger className="w-full">
142+
<SelectValue placeholder={`Select ${label}`} />
143+
</SelectTrigger>
144+
<SelectContent className="z-elevated">
145+
{enumValues.map((value: string) => (
146+
<SelectItem key={value} value={value}>
147+
{value.charAt(0).toUpperCase() + value.slice(1)}
148+
</SelectItem>
149+
))}
150+
</SelectContent>
151+
</Select>
152+
</FormControl>
153+
<FormMessage />
154+
</FormItem>
155+
)}
156+
/>
157+
);
158+
}
159+
160+
const isDateField =
161+
fieldSchema._def.typeName === ZodFirstPartyTypeKind.ZodDate ||
162+
(fieldSchema._def.typeName === ZodFirstPartyTypeKind.ZodUnion &&
163+
Array.isArray(fieldSchema._def.options) &&
164+
fieldSchema._def.options.some(
165+
(option) =>
166+
option._def?.typeName === ZodFirstPartyTypeKind.ZodDate ||
167+
option._def?.typeName === ZodFirstPartyTypeKind.ZodString
168+
)) ||
169+
fieldName.toLowerCase().includes('date');
170+
const isNumberField =
171+
fieldSchema._def.typeName === ZodFirstPartyTypeKind.ZodNumber;
172+
173+
// Default to text input
174+
return (
175+
<FormField
176+
key={fieldName}
177+
control={formControl}
178+
name={fieldName as unknown as FieldPath<T>}
179+
render={({ field }) => (
180+
<FormItem>
181+
<FormControl>
182+
<Input
183+
id={fieldName}
184+
label={label}
185+
type={isDateField ? 'date' : isNumberField ? 'number' : 'text'}
186+
required={!isOptional}
187+
{...field}
188+
/>
189+
</FormControl>
190+
<FormMessage />
191+
</FormItem>
192+
)}
193+
/>
194+
);
195+
};
196+
197+
return Object.keys(schemaShape).map((fieldName) => {
198+
// Make certain fields span full width
199+
const isFullWidth = ['description', 'about', 'title', 'people'].includes(
200+
fieldName.toLowerCase()
201+
);
202+
203+
return (
204+
<div key={fieldName} className={isFullWidth ? 'md:col-span-2' : ''}>
205+
{renderField(fieldName)}
206+
</div>
207+
);
208+
});
209+
};
210+
211+
export function FacultyPersonalDetailsForm({
212+
locale,
213+
existingDetails,
214+
}: {
215+
locale: string;
216+
existingDetails: z.infer<typeof facultyPersonalDetailsSchema>;
217+
}) {
218+
const form = useForm<z.infer<typeof facultyPersonalDetailsSchema>>({
219+
resolver: zodResolver(facultyPersonalDetailsSchema),
220+
defaultValues: existingDetails,
221+
});
222+
223+
const onSubmit = async (
224+
values: z.infer<typeof facultyPersonalDetailsSchema>
225+
) => {
226+
const result = await editFacultyProfilePersonalDetails(values);
227+
toast({
228+
title: result.success ? 'Success' : 'Error',
229+
description: result.success
230+
? `Successfully updated personal details.`
231+
: result.message,
232+
variant: result.success ? 'success' : 'error',
233+
});
234+
window.history.go(-1);
235+
};
236+
const schemaShape = facultyPersonalDetailsSchema.shape;
237+
238+
return (
239+
<FormProvider {...form}>
240+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
241+
<CardContent className="grid gap-4 p-6 md:grid-cols-2">
242+
{renderFields(form.control, facultyPersonalDetailsSchema.shape)}
243+
</CardContent>
244+
<CardFooter className="flex justify-end border-t pt-4 ">
245+
<Button
246+
type="button"
247+
variant="secondary"
248+
className="mr-2 p-1"
249+
onClick={() => window.history.back()}
250+
>
251+
Cancel
252+
</Button>
253+
<Button type="submit" variant="primary" className="p-1">
254+
{'Update'}
255+
</Button>
256+
</CardFooter>
257+
</form>
258+
</FormProvider>
259+
);
260+
}

0 commit comments

Comments
 (0)