From 142e40856d7cf82f2f2f1dd2a317c30e62479be7 Mon Sep 17 00:00:00 2001 From: emirsassan Date: Sun, 20 Jul 2025 18:55:13 +0300 Subject: [PATCH] refactor: improve form validation and component structure --- .../launcher/cluster/ClusterCreation.tsx | 416 +++++++++++------- 1 file changed, 250 insertions(+), 166 deletions(-) diff --git a/apps/onelauncher/frontend/src/components/launcher/cluster/ClusterCreation.tsx b/apps/onelauncher/frontend/src/components/launcher/cluster/ClusterCreation.tsx index 8d0a8e8e..65b8dff7 100644 --- a/apps/onelauncher/frontend/src/components/launcher/cluster/ClusterCreation.tsx +++ b/apps/onelauncher/frontend/src/components/launcher/cluster/ClusterCreation.tsx @@ -1,4 +1,4 @@ -import type { GameLoader, Version, VersionType } from '@/bindings.gen'; +import type { GameLoader, Version } from '@/bindings.gen'; import type { JSX, RefAttributes } from 'react'; import DefaultBanner from '@/assets/images/default_banner.png'; import LauncherIcon from '@/components/content/LauncherIcon'; @@ -10,17 +10,17 @@ import { useCommand, useCommandMut } from '@onelauncher/common'; import { Button, Dropdown, SelectList, TextField } from '@onelauncher/common/components'; import { useQueryClient } from '@tanstack/react-query'; import { ArrowRightIcon, PlusIcon, SearchMdIcon, Server01Icon, TextInputIcon, User03Icon } from '@untitled-theme/icons-react'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { Checkbox } from 'react-aria-components'; import LoaderIcon from '../LoaderIcon'; const STEPS = [ { - id: 'loader', + id: 'loader' as const, title: 'Select Provider', }, { - id: 'info', + id: 'info' as const, title: 'Game Setup', }, ] as const; @@ -32,6 +32,12 @@ interface ClusterFormData { clusterProvider: string; } +interface FormValidation { + isProviderValid: boolean; + isNameValid: boolean; + isVersionValid: boolean; +} + export function NewClusterCreate() { const queryClient = useQueryClient(); const [currentStepIndex, setCurrentStepIndex] = useState(0); @@ -42,6 +48,12 @@ export function NewClusterCreate() { clusterProvider: '', }); + const validation = useMemo((): FormValidation => ({ + isProviderValid: Boolean(formData.clusterProvider), + isNameValid: Boolean(formData.clusterName.trim()), + isVersionValid: Boolean(formData.clusterVersion.trim()), + }), [formData.clusterProvider, formData.clusterName, formData.clusterVersion]); + const create = useCommandMut(() => bindings.core.createCluster({ icon: null, mc_loader: formData.loader, @@ -54,126 +66,183 @@ export function NewClusterCreate() { }, }); - function handleCreate() { - if (!isLastInputStep && !formData.clusterName) + const handleCreate = useCallback(() => { + if (!validation.isNameValid || !validation.isVersionValid) { + console.warn('Form validation failed'); return; + } create.mutate(); + }, [create, validation.isNameValid, validation.isVersionValid]); - if (create.isError) - console.error(create.error.message); - } - - const handleNext = useCallback(async () => { - if (currentStepIndex === STEPS.length - 1) + const handleNext = useCallback(() => { + if (currentStepIndex === STEPS.length - 1) { handleCreate(); + return; + } if (currentStepIndex < STEPS.length - 1) - setCurrentStepIndex(currentStepIndex + 1); - }, [currentStepIndex]); + setCurrentStepIndex(prev => prev + 1); + }, [currentStepIndex, handleCreate]); const handleBack = useCallback(() => { if (currentStepIndex > 0) - setCurrentStepIndex(currentStepIndex - 1); + setCurrentStepIndex(prev => prev - 1); }, [currentStepIndex]); + const formHandlers = useMemo(() => ({ + onProviderChange: (provider: string) => + setFormData(prev => ({ ...prev, clusterProvider: provider })), + onLoaderChange: (loader: GameLoader) => + setFormData(prev => ({ ...prev, loader })), + onNameChange: (name: string) => + setFormData(prev => ({ ...prev, clusterName: name })), + onVersionChange: (version: string) => + setFormData(prev => ({ ...prev, clusterVersion: version })), + }), []); + const currentStepConfig = STEPS[currentStepIndex]; - let stepContent: JSX.Element; - switch (currentStepConfig.id) { - case 'loader': { - stepContent = ( - setFormData(prev => ({ ...prev, clusterProvider: provider }))} selectedProvider={formData.clusterProvider} /> - ); - break; + const stepContent = useMemo((): JSX.Element => { + switch (currentStepConfig.id) { + case 'loader': + return ( + + ); + case 'info': + return ( + + ); + default: + return
Unknown step
; } - - case 'info': { - stepContent = ( - setFormData(prev => ({ ...prev, loader }))} - onClusterNameChange={name => setFormData(prev => ({ ...prev, clusterName: name }))} - onClusterVersionChange={version => - setFormData(prev => ({ ...prev, clusterVersion: version }))} - /> - ); - break; - } - } + }, [currentStepConfig.id, formData, formHandlers]); const isFirstStep = currentStepIndex === 0; const isLastInputStep = currentStepIndex === STEPS.length - 1; const nextButtonText = isLastInputStep ? 'Create' : 'Next'; - let isNextDisabled = false; - if (currentStepConfig.id === 'loader' && !formData.clusterProvider) - isNextDisabled = true; - else if ( - currentStepConfig.id === 'info' - && (!formData.clusterName.trim() || !formData.clusterVersion.trim()) - ) - isNextDisabled = false; + const isNextDisabled = useMemo(() => { + switch (currentStepConfig.id) { + case 'loader': + return !validation.isProviderValid; + case 'info': + return !validation.isNameValid || !validation.isVersionValid; + default: + return false; + } + }, [currentStepConfig.id, validation]); return ( - <> -
-
- } placeholder="Search for something..." /> -
- - - - - -
-
-
- Header Image -
-
- -
-

New Cluster

- {STEPS[currentStepIndex].title} -
-
-
-
-
- {stepContent} -
- -
- -
-
-
-
-
+
+
+ } + placeholder="Search for something..." + />
- + + + + + +
+ + +
+
+ {stepContent} +
+ + +
+
+
+
+
+ ); +} + +interface ModalHeaderProps { + currentStep: typeof STEPS[number]; +} + +function ModalHeader({ currentStep }: ModalHeaderProps) { + return ( +
+
+ Header Image +
+
+ +
+

New Cluster

+ {currentStep.title} +
+
+
+ ); +} + +interface ModalFooterProps { + isFirstStep: boolean; + isNextDisabled: boolean; + nextButtonText: string; + onBack: () => void; + onNext: () => void; +} + +function ModalFooter({ + isFirstStep, + isNextDisabled, + nextButtonText, + onBack, + onNext, +}: ModalFooterProps) { + return ( +
+ + +
); } @@ -183,49 +252,60 @@ interface ClusterLoaderProps { } function ClusterLoader({ selectedProvider, onSelectProvider }: ClusterLoaderProps) { - return ( - <> -
- } - name="New" - selected={selectedProvider === 'new'} - setSelected={() => onSelectProvider('new')} - /> + const providers = useMemo(() => [ + { id: 'new', name: 'New', icon: }, + ...LAUNCHER_IMPORT_TYPES.map(data => ({ + id: data, + name: data, + icon: , + })), + ], []); - {LAUNCHER_IMPORT_TYPES.map((data, i) => ( - } - key={i} - name={data} - selected={selectedProvider === data} - setSelected={() => onSelectProvider(data)} - /> - ))} -
- + return ( +
+ {providers.map(provider => ( + onSelectProvider(provider.id)} + selected={selectedProvider === provider.id} + /> + ))} +
); } interface ProviderCardProps { - setSelected: () => void; + onSelect: () => void; selected: boolean; icon: JSX.Element; name: string; } -function ProviderCard(props: ProviderCardProps) { - const { selected, setSelected, icon, name } = props; +function ProviderCard({ selected, onSelect, icon, name }: ProviderCardProps) { + const cardClassName = useMemo(() => { + const baseClasses = 'flex flex-col justify-center items-center gap-y-3 py-2 px-4 hover:bg-component-bg-hover active:bg-component-bg-pressed rounded-lg cursor-pointer transition-colors'; + return selected ? `${baseClasses} bg-component-bg` : baseClasses; + }, [selected]); return (
setSelected()} + className={cardClassName} + onClick={onSelect} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + onSelect(); + } + }} + role="button" + tabIndex={0} >
{icon}
- {name} + {name}
); } @@ -253,6 +333,7 @@ function ClusterInformation({