diff options
Diffstat (limited to 'components/settings')
| -rw-r--r-- | components/settings/account-form.tsx | 263 | ||||
| -rw-r--r-- | components/settings/appearance-form.tsx | 244 |
2 files changed, 507 insertions, 0 deletions
diff --git a/components/settings/account-form.tsx b/components/settings/account-form.tsx new file mode 100644 index 00000000..97cad9e5 --- /dev/null +++ b/components/settings/account-form.tsx @@ -0,0 +1,263 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { toast } from "@/hooks/use-toast" +import { Button } from "@/components/ui/button" + +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" + +import { findUserById } from "@/lib/admin-users/service" +import { useSession } from "next-auth/react"; + +import { updateUserProfileImage } from "@/lib/users/service" + + + +const accountFormSchema = z.object({ + name: z + .string() + .min(2, { + message: "Name must be at least 2 characters.", + }) + .max(30, { + message: "Name must not be longer than 30 characters.", + }), + email: z.string().email(), + company: z + .string() + .min(2, { + message: "Name must be at least 2 characters.", + }) + .max(30, { + message: "Name must not be longer than 30 characters.", + }), + + imageFile: z.any().optional(), + +}) + +type AccountFormValues = z.infer<typeof accountFormSchema> + + + +export function AccountForm() { + + const { data: session } = useSession(); + const userId = session?.user.id || "" + + + const [previewUrl, setPreviewUrl] = React.useState<string | null>(null) + + const form = useForm<AccountFormValues>({ + resolver: zodResolver(accountFormSchema), + defaultValues: { + name: "", + company: "", + email: "", + imageFile: null, + }, + }) + + // Fetch data in useEffect + React.useEffect(() => { + console.log("Form state changed: ", form.getValues()); + + async function fetchUser() { + try { + const data = await findUserById(Number(userId)) + if (data) { + // Also reset the form's default values + form.reset({ + name: data.user_name || "", + company: data.company_name || "", + email: data.user_email || "", + imageFile: data.user_image, // no file to begin with + }) + } + } catch (error) { + console.error("Failed to fetch user data:", error) + } + } + + if (userId) { + fetchUser() + } + }, [userId, form]) + + + async function onSubmit(data: AccountFormValues) { + // RHF가 추적한 dirtyFields를 가져옵니다. + const { dirtyFields } = form.formState + + // 변경된 필드가 전혀 없다면 => 업데이트 스킵 + if (Object.keys(dirtyFields).length === 0) { + toast({ + title: "No changes", + description: "Nothing to update", + }) + return + } + + // 바뀐 파일만 업로드 + let imageFile: File | null = null + if (dirtyFields.imageFile && data.imageFile && data.imageFile.length > 0) { + // 새로 업로드한 파일 + imageFile = data.imageFile[0] + } + + // FormData 생성 + const formData = new FormData() + formData.append("userId", userId) + formData.append("name", data.name) + formData.append("company", data.company) + formData.append("email", data.email) + + if (imageFile) { + formData.append("file", imageFile) + } + + try { + // 서버 액션(또는 API) 호출 + await updateUserProfileImage(formData) + + toast({ + title: "Account updated", + description: "User updated successfully!", + }) + + } catch (error: any) { + toast({ + title: "Error", + description: `Error: ${error.message ?? error}`, + variant: "destructive", + }) + } + } + + + return ( + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> + <FormField + control={form.control} + name="name" + render={({ field }) => ( + <FormItem> + <FormLabel>Name</FormLabel> + <FormControl> + <Input placeholder="Your name" {...field} /> + </FormControl> + <FormDescription> + This is the name that will be displayed on your profile and in + emails. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="email" + render={({ field }) => ( + <FormItem> + <FormLabel>Email</FormLabel> + <FormControl> + <Input placeholder="Your Email" {...field} /> + </FormControl> + <FormDescription> + This is the email that will be used on login. If you want change it, please be careful. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="company" + render={({ field }) => ( + <FormItem> + <FormLabel>Company</FormLabel> + <FormControl> + <Input + placeholder="Your Company name" + {...field} + readOnly + className="cursor-not-allowed bg-slate-50" + /> + </FormControl> + <FormDescription> + This is the name that will be displayed on your profile and in + emails. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + + + {/* 이미지 업로드 */} + <FormField + control={form.control} + name="imageFile" + render={({ field }) => ( + <FormItem> + <FormLabel>Profile Image</FormLabel> + <FormControl> + <div className="space-y-2"> + <Input + type="file" + accept="image/*" + onChange={(e) => { + field.onChange(e.target.files) + if (e.target.files && e.target.files.length > 0) { + // 로컬 미리보기 URL + const file = e.target.files[0] + const url = URL.createObjectURL(file) + setPreviewUrl(url) + } + }} + /> + + {previewUrl ? ( + <img src={previewUrl} alt="Local Preview" width={200}/> + ) : ( + typeof field.value === "string" && + field.value && ( + <img + src={`/profiles/${field.value}`} + alt="Server Image" + width={200} + /> + ) + )} + </div> + </FormControl> + <FormDescription> + Upload your profile image. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <Button type="submit">Update account</Button> + </form> + </Form> + ) +} diff --git a/components/settings/appearance-form.tsx b/components/settings/appearance-form.tsx new file mode 100644 index 00000000..8f843fd6 --- /dev/null +++ b/components/settings/appearance-form.tsx @@ -0,0 +1,244 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { useTheme } from "next-themes" +import { toast } from "@/hooks/use-toast" +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" +import { useMetaColor } from "@/hooks/use-meta-color" +import { META_THEME_COLORS } from "@/config/site" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" +import { Check, ChevronsUpDown } from "lucide-react" +import { cn } from "@/lib/utils" +import { useTranslation } from '@/i18n/client' +import { useRouter, useParams, usePathname } from 'next/navigation'; + +const appearanceFormSchema = z.object({ + theme: z.enum(["light", "dark"], { + required_error: "Please select a theme.", + }), + language: z.string({ + required_error: "Please select a language.", + }), + +}) + +type AppearanceFormValues = z.infer<typeof appearanceFormSchema> + +// This can come from your database or API. +const defaultValues: Partial<AppearanceFormValues> = { + theme: "light", + language:'ko' +} +const languages = [ + { label: "English", value: "en" }, + { label: "한국어", value: "ko" }, +] as const + +export function AppearanceForm() { + const { setTheme, resolvedTheme } = useTheme() + const { setMetaColor } = useMetaColor() + + const pathname = usePathname(); + const router = useRouter(); + + const params = useParams(); + const lng = params.lng as string; + const { t, i18n } = useTranslation(lng, 'translation'); + + const form = useForm<AppearanceFormValues>({ + resolver: zodResolver(appearanceFormSchema), + defaultValues, + }) + + function onSubmit(data: AppearanceFormValues) { + setTheme(data.theme) + setMetaColor( + resolvedTheme === "dark" + ? META_THEME_COLORS.light + : META_THEME_COLORS.dark + ) + + const segments = pathname.split('/'); + segments[1] = data.language; + router.push(segments.join('/')); + + toast({ + title: "Updated Successfully", + // description: ( + // <div className="mt-2 w-[340px] rounded-md bg-slate-950 p-4"> + // <div className="text-white">{JSON.stringify(data, null, 2)}</div> + // </div> + // ), + }) + } + + return ( + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8"> + + <FormField + control={form.control} + name="theme" + render={({ field }) => ( + <FormItem className="space-y-1"> + <FormLabel>Theme</FormLabel> + <FormDescription> + Customize the appearance of the app. Automatically switch between day + and night themes. + </FormDescription> + <FormMessage /> + <RadioGroup + onValueChange={field.onChange} + defaultValue={field.value} + className="grid max-w-md grid-cols-2 gap-8 pt-2" + > + <FormItem> + <FormLabel className="[&:has([data-state=checked])>div]:border-primary"> + <FormControl> + <RadioGroupItem value="light" className="sr-only" /> + </FormControl> + <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent"> + <div className="space-y-2 rounded-sm bg-[#ecedef] p-2"> + <div className="space-y-2 rounded-md bg-white p-2 shadow-sm"> + <div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" /> + <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" /> + </div> + <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm"> + <div className="h-4 w-4 rounded-full bg-[#ecedef]" /> + <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" /> + </div> + <div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm"> + <div className="h-4 w-4 rounded-full bg-[#ecedef]" /> + <div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" /> + </div> + </div> + </div> + <span className="block w-full p-2 text-center font-normal"> + Light + </span> + </FormLabel> + </FormItem> + <FormItem> + <FormLabel className="[&:has([data-state=checked])>div]:border-primary"> + <FormControl> + <RadioGroupItem value="dark" className="sr-only" /> + </FormControl> + <div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground"> + <div className="space-y-2 rounded-sm bg-slate-950 p-2"> + <div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm"> + <div className="h-2 w-[80px] rounded-lg bg-slate-400" /> + <div className="h-2 w-[100px] rounded-lg bg-slate-400" /> + </div> + <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm"> + <div className="h-4 w-4 rounded-full bg-slate-400" /> + <div className="h-2 w-[100px] rounded-lg bg-slate-400" /> + </div> + <div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm"> + <div className="h-4 w-4 rounded-full bg-slate-400" /> + <div className="h-2 w-[100px] rounded-lg bg-slate-400" /> + </div> + </div> + </div> + <span className="block w-full p-2 text-center font-normal"> + Dark + </span> + </FormLabel> + </FormItem> + </RadioGroup> + </FormItem> + )} + /> + + <FormField + control={form.control} + name="language" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel>Language</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant="outline" + role="combobox" + className={cn( + "w-[200px] justify-between", + !field.value && "text-muted-foreground" + )} + > + {field.value + ? languages.find( + (language) => language.value === field.value + )?.label + : "Select language"} + <ChevronsUpDown className="opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-[200px] p-0"> + <Command> + <CommandInput placeholder="Search language..." /> + <CommandList> + <CommandEmpty>No language found.</CommandEmpty> + <CommandGroup> + {languages.map((language) => ( + <CommandItem + value={language.label} + key={language.value} + onSelect={() => { + form.setValue("language", language.value) + }} + > + <Check + className={cn( + "mr-2", + language.value === field.value + ? "opacity-100" + : "opacity-0" + )} + /> + {language.label} + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + <FormDescription> + This is the language that will be used in the system. + </FormDescription> + <FormMessage /> + </FormItem> + )} + /> + + <Button type="submit">Update preferences</Button> + </form> + </Form> + ) +} |
