diff --git a/pages/src/components/UninstallForm.tsx b/pages/src/components/UninstallForm.tsx index 66ca5dd2..6b921ff0 100644 --- a/pages/src/components/UninstallForm.tsx +++ b/pages/src/components/UninstallForm.tsx @@ -1,19 +1,19 @@ -'use client' - -import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' -import { Textarea } from '@/components/ui/textarea' -import { Input } from '@/components/ui/input' -import { Image } from '@/components/Image' -import { useLocale } from '@/hooks/useLocale' -import { Send, SquareArrowOutUpRight } from 'lucide-react' -import { useState, useEffect } from 'react' -import type { UninstallFormType } from '@/types' -import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible' -import { UNINSTALL_OTHER_OPTION } from '@/const' -import { cn, sleep } from '@/lib/utils' - -import css from './CommandForm.module.css' +"use client" + +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { Textarea } from "@/components/ui/textarea" +import { Input } from "@/components/ui/input" +import { Image } from "@/components/Image" +import { useLocale } from "@/hooks/useLocale" +import { Send, SquareArrowOutUpRight } from "lucide-react" +import { useState, useEffect } from "react" +import type { UninstallFormType } from "@/types" +import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible" +import { UNINSTALL_OTHER_OPTION, OTHER_OPTION } from "@/const" +import { cn, sleep } from "@/lib/utils" + +import css from "./CommandForm.module.css" type UninstallReason = { id: string @@ -21,49 +21,155 @@ type UninstallReason = { } const SubmitStatus = { - IDLE: 'idle', - SUCCESS: 'success', - ERROR: 'error', + IDLE: "idle", + SUCCESS: "success", + ERROR: "error", } as const type SubmitStatusType = (typeof SubmitStatus)[keyof typeof SubmitStatus] -export function UninstallForm() { - const { dict, lang } = useLocale() - const [selectedReasons, setSelectedReasons] = useState([]) - const [details, setDetails] = useState('') - const [otherReason, setOtherReason] = useState('') +// Helper function to shuffle options and put "Other" at the end +function shuffleOptionsWithOtherLast( + entries: [string, unknown][], + otherOptionKey: string, +): UninstallReason[] { + const otherEntry = entries.find(([id]) => id === otherOptionKey) + const otherEntries = entries.filter(([id]) => id !== otherOptionKey) + + // Shuffle entries except for "Other" option + const shuffledEntries = otherEntries.sort(() => Math.random() - 0.5) + + // Add "Other" option at the end if it exists + const finalEntries = otherEntry + ? [...shuffledEntries, otherEntry] + : shuffledEntries + + return finalEntries.map(([id, label]) => ({ + id, + label: label as string, + })) +} + +// Custom hook for form state management +function useUninstallFormState() { + const [formState, setFormState] = useState({ + selectedReasons: [] as string[], + selectedWantedToUse: [] as string[], + details: "", + otherReason: "", + otherWantedToUse: "", + }) const [isSubmitting, setIsSubmitting] = useState(false) const [submitStatus, setSubmitStatus] = useState( SubmitStatus.IDLE, ) + + const updateFormState = (updates: Partial) => { + setFormState((prev) => ({ ...prev, ...updates })) + } + + const resetForm = () => { + setFormState({ + selectedReasons: [], + selectedWantedToUse: [], + details: "", + otherReason: "", + otherWantedToUse: "", + }) + } + + return { + formState, + updateFormState, + resetForm, + isSubmitting, + setIsSubmitting, + submitStatus, + setSubmitStatus, + } +} + +// CheckboxGroup component for reusable checkbox logic +type CheckboxGroupProps = { + options: UninstallReason[] + selectedValues: string[] + onSelectionChange: (values: string[]) => void + idPrefix?: string +} + +function CheckboxGroup({ + options, + selectedValues, + onSelectionChange, + idPrefix = "", +}: CheckboxGroupProps) { + const handleChange = (optionId: string, checked: boolean) => { + if (checked) { + onSelectionChange([...selectedValues, optionId]) + } else { + onSelectionChange(selectedValues.filter((id) => id !== optionId)) + } + } + + return ( +
+ {options.map((option) => ( +
+ + handleChange(option.id, checked) + } + /> + +
+ ))} +
+ ) +} + +export function UninstallForm() { + const { dict, lang } = useLocale() + const { + formState, + updateFormState, + resetForm, + isSubmitting, + setIsSubmitting, + submitStatus, + setSubmitStatus, + } = useUninstallFormState() + const [uninstallReasons, setUninstallReasons] = useState( [], ) + const [wantedToUseOptions, setWantedToUseOptions] = useState< + UninstallReason[] + >([]) const [isInitialized, setIsInitialized] = useState(false) // Randomize order only on the client side useEffect(() => { - const entries = Object.entries(dict.uninstallForm.reasons) - const otherEntry = entries.find(([id]) => id === UNINSTALL_OTHER_OPTION) - const otherEntries = entries.filter(([id]) => id !== UNINSTALL_OTHER_OPTION) - - // Shuffle entries except for "Other" option - const shuffledEntries = otherEntries.sort(() => Math.random() - 0.5) - - // Add "Other" option at the end if it exists - const finalEntries = otherEntry - ? [...shuffledEntries, otherEntry] - : shuffledEntries - + // Process uninstall reasons + const reasonEntries = Object.entries(dict.uninstallForm.reasons) setUninstallReasons( - finalEntries.map(([id, label]) => ({ - id, - label: label as string, - })), + shuffleOptionsWithOtherLast(reasonEntries, UNINSTALL_OTHER_OPTION), + ) + + // Process wanted to use options + const wantedEntries = Object.entries(dict.uninstallForm.wantedToUse) + setWantedToUseOptions( + shuffleOptionsWithOtherLast(wantedEntries, OTHER_OPTION), ) + setIsInitialized(true) - }, [dict.uninstallForm.reasons]) + }, [dict.uninstallForm.reasons, dict.uninstallForm.wantedToUse]) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -72,25 +178,27 @@ export function UninstallForm() { try { const response = await submit({ - uninstallReason: selectedReasons, - details, - otherReason: selectedReasons.includes(UNINSTALL_OTHER_OPTION) - ? otherReason - : '', + uninstallReason: formState.selectedReasons, + wantedToUse: formState.selectedWantedToUse, + details: formState.details, + otherReason: formState.selectedReasons.includes(UNINSTALL_OTHER_OPTION) + ? formState.otherReason + : "", + otherWantedToUse: formState.selectedWantedToUse.includes(OTHER_OPTION) + ? formState.otherWantedToUse + : "", locale: lang, } as UninstallFormType) if (!response.success) { - throw new Error('Failed to submit form') + throw new Error("Failed to submit form") } await sleep(1000) setSubmitStatus(SubmitStatus.SUCCESS) - setSelectedReasons([]) - setOtherReason('') - setDetails('') + resetForm() } catch (error) { - console.error('Error submitting form:', error) + console.error("Error submitting form:", error) setSubmitStatus(SubmitStatus.ERROR) } finally { setIsSubmitting(false) @@ -100,9 +208,9 @@ export function UninstallForm() { return (
{submitStatus === SubmitStatus.SUCCESS ? ( @@ -144,44 +252,62 @@ export function UninstallForm() {
+ {/* wanted to use */}

- {dict.uninstallForm.reasonTitle} + {dict.uninstallForm.wantedToUseTitle}

-
- {uninstallReasons.map((reason) => ( -
- { - if (checked) { - setSelectedReasons([...selectedReasons, reason.id]) - } else { - setSelectedReasons( - selectedReasons.filter((id) => id !== reason.id), - ) - } - }} + + updateFormState({ selectedWantedToUse: values }) + } + idPrefix="wanted-" + /> + + + +
+ + updateFormState({ otherWantedToUse: e.target.value }) + } + placeholder={dict.uninstallForm.wantedToUsePlaceholder} /> -
- ))} -
+ + +
+ + {/* uninstall reason */} +
+

+ {dict.uninstallForm.reasonTitle} +

+ + updateFormState({ selectedReasons: values }) + } + />
setOtherReason(e.target.value)} + value={formState.otherReason} + onChange={(e) => + updateFormState({ otherReason: e.target.value }) + } placeholder={dict.uninstallForm.otherReasonPlaceholder} />
@@ -189,13 +315,14 @@ export function UninstallForm() {
+ {/* detail */}

{dict.uninstallForm.detailsTitle}