From 17033187034e80c18988f358e25c49ae217a1632 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sun, 20 Jul 2025 16:51:07 +0200 Subject: [PATCH 01/18] feat: featured section WIP --- .../frontend/src/hooks/useBrowser.tsx | 20 +- .../frontend/src/routes/app/browser/index.tsx | 24 ++- .../app/browser/package.$provider.$slug.tsx | 188 +++++++++--------- 3 files changed, 116 insertions(+), 116 deletions(-) diff --git a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx index 522af87b..1e2ab881 100644 --- a/apps/onelauncher/frontend/src/hooks/useBrowser.tsx +++ b/apps/onelauncher/frontend/src/hooks/useBrowser.tsx @@ -3,8 +3,8 @@ import type { UndefinedInitialDataOptions } from '@tanstack/react-query'; import { bindings } from '@/main'; import { PROVIDERS } from '@/utils'; import { useCommand } from '@onelauncher/common'; -import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import { useNavigate } from '@tanstack/react-router'; +import { createContext, useContext, useEffect, useMemo, useState } from 'react'; export interface BrowserControllerType { cluster: ClusterModel | undefined; @@ -29,7 +29,7 @@ export function BrowserProvider(props: any) { const { children } = props; const [cluster, setCluster] = useState(undefined); const [provider, setProvider] = useState(PROVIDERS[0]); - const navigate = useNavigate() + const navigate = useNavigate(); const [query, setQuery] = useState({ filters: null, query: null, @@ -38,10 +38,10 @@ export function BrowserProvider(props: any) { sort: null, }); - useEffect(()=>{ - if(query.filters?.categories || query.query) - navigate({to: "/app/browser/search"}) - },[query]) + useEffect(() => { + if (query.filters?.categories || query.query) + navigate({ to: '/app/browser/search' }); + }, [query]); const context = useMemo(() => ({ cluster, @@ -65,10 +65,10 @@ export function useBrowserSearch(provider: Provider, query: SearchQuery, options return useCommand('searchPackages', () => bindings.core.searchPackages(provider, validFilters ? query : { ...query, filters: null }), options); } -export function usePackageData(provider: Provider, slug: string, options?: Omit, "queryKey" | "queryFn"> | undefined){ - return useCommand("getPackage", () => bindings.core.getPackage(provider, slug), options) +export function usePackageData(provider: Provider, slug: string, options?: Omit, 'queryKey' | 'queryFn'> | undefined) { + return useCommand('getPackage', () => bindings.core.getPackage(provider, slug), options); } -export function usePackageVersions(provider: Provider, slug: string, {mc_versions, loaders, offset, limit, ...options}:{mc_versions?:Array|null, loaders?:Array|null, offset?:number, limit: number} & Omit>, "queryKey" | "queryFn">){ - return useCommand("getPackageVersions", ()=>bindings.core.getPackageVersions(provider, slug, mc_versions??null, loaders??null, (offset??0) as unknown as bigint, limit as unknown as bigint), options) +export function usePackageVersions(provider: Provider, slug: string, { mc_versions, loaders, offset, limit, ...options }: { mc_versions?: Array | null; loaders?: Array | null; offset?: number; limit: number } & Omit>, 'queryKey' | 'queryFn'>) { + return useCommand('getPackageVersions', () => bindings.core.getPackageVersions(provider, slug, mc_versions ?? null, loaders ?? null, (offset ?? 0) as unknown as bigint, limit as unknown as bigint), options); } diff --git a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx index 9ca0ce77..dad33499 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx @@ -1,16 +1,16 @@ import type { Provider } from '@/bindings.gen'; import OneConfigLogo from '@/assets/logos/oneconfig.svg'; import { PackageGrid } from '@/components/content/PackageItem'; -import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser'; +import { useBrowserSearch, usePackageData } from '@/hooks/useBrowser'; import { PROVIDERS } from '@/utils'; import { Button, Show } from '@onelauncher/common/components'; -import { createFileRoute } from '@tanstack/react-router'; +import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { ChevronRightIcon } from '@untitled-theme/icons-react'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { BrowserLayout } from './route'; export const Route = createFileRoute('/app/browser/')({ - component: RouteComponent + component: RouteComponent, }); @@ -26,17 +26,21 @@ function RouteComponent() { } function Featured() { - const context = useBrowserContext(); + // const context = useBrowserContext(); + const [provider, slug] = useMemo<[Provider, string]>(() => ['Modrinth', 'sodium'], []); + const featuredPackage = usePackageData(provider, slug); + const navigate = useNavigate(); + return ( - 0)}> +
Featured
- thumbnail + thumbnail
-

Sodium

+

{featuredPackage.data?.name}

@@ -45,10 +49,10 @@ function Featured() {
-

Peak mod

+

{featuredPackage.data?.short_desc}

- diff --git a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx index e3d3f84b..8d10bcd8 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx @@ -1,45 +1,45 @@ -import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen' -import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser' -import { useClusters } from '@/hooks/useCluster' -import { bindings } from '@/main' -import { abbreviateNumber, formatAsRelative, PROVIDERS } from '@/utils' -import { useCommand } from '@onelauncher/common' -import { Show, Tooltip, Button, Dropdown } from '@onelauncher/common/components' -import { createFileRoute, Link } from '@tanstack/react-router' -import { openUrl } from '@tauri-apps/plugin-opener' -import { Download01Icon, LinkExternal01Icon, File02Icon, CalendarIcon, ClockRewindIcon, ChevronDownIcon, ChevronUpIcon } from '@untitled-theme/icons-react' -import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react' -import { Collection, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components' - +import type { ClusterModel, ManagedPackage, ManagedUser, ManagedVersion, Paginated, Provider } from '@/bindings.gen'; +import { useBrowserContext, usePackageData, usePackageVersions } from '@/hooks/useBrowser'; +import { useClusters } from '@/hooks/useCluster'; +import { bindings } from '@/main'; +import { abbreviateNumber, formatAsRelative, PROVIDERS } from '@/utils'; +import { useCommand } from '@onelauncher/common'; +import { Button, Show, Tooltip } from '@onelauncher/common/components'; +import { createFileRoute, Link } from '@tanstack/react-router'; +import { openUrl } from '@tauri-apps/plugin-opener'; +import { CalendarIcon, ChevronDownIcon, ChevronUpIcon, ClockRewindIcon, Download01Icon, File02Icon, LinkExternal01Icon } from '@untitled-theme/icons-react'; +import { createContext, useContext, useMemo, useRef, useState } from 'react'; +import { Collection, ListBox, ListBoxItem, Popover, Select } from 'react-aria-components'; export const Route = createFileRoute('/app/browser/package/$provider/$slug')({ component: RouteComponent, -}) +}); -function includes(list: { includes: (arg0: C) => boolean }, element: T): element is C { - return list.includes(element as unknown as C) +function includes(list: { includes: (arg0: TArray) => boolean }, element: T): element is TArray { + return list.includes(element as unknown as TArray); } -type PackageContextType = { - pkg: ManagedPackage | undefined - versions: Paginated | undefined +interface PackageContextType { + pkg: ManagedPackage | undefined; + versions: Paginated | undefined; } const PackageContext = createContext({ pkg: undefined, - versions: undefined -}) + versions: undefined, +}); function RouteComponent() { - const { provider, slug } = Route.useParams() - if (!includes(PROVIDERS, provider)) throw new Error("Invalid provider"); - const packageData = usePackageData(provider, slug) - const browserContext = useBrowserContext() + const { provider, slug } = Route.useParams(); + if (!includes(PROVIDERS, provider)) + throw new Error('Invalid provider'); + const packageData = usePackageData(provider, slug); + const browserContext = useBrowserContext(); const { data: versions } = usePackageVersions(provider, slug, { mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : null, loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : null, - limit: 50 - }) + limit: 50, + }); return ( @@ -49,9 +49,8 @@ function RouteComponent() {
- About - Versions - + About + Versions
@@ -62,17 +61,16 @@ function RouteComponent() {
- ) + ); } - function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { - const { provider } = Route.useParams() + const { provider } = Route.useParams(); const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []); const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []); - const authors = useCommand("getUsersFromAuthor", () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)) + const authors = useCommand('getUsersFromAuthor', () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)); return (
@@ -106,7 +104,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
- + {/*

Links

@@ -121,7 +119,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {

Authors

{ authors.isSuccess - ? authors.data.map(author => ) + ? authors.data.map(author => ) :

Loading...

} @@ -133,7 +131,7 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) {
License - + {pkg.license?.name || pkg.license?.id || 'Unknown'}
@@ -168,60 +166,50 @@ function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { ); } - -function colorForType(type: string) { - switch (type.toLowerCase()) { - case 'release': - return 'bg-code-trace'; - case 'snapshot': - return 'bg-code-debug'; - case 'beta': - return 'bg-code-warn'; - case 'alpha': - return 'bg-code-error'; - default: - return 'bg-border/05'; - } -} - -function InstallButton({ ...pkg }: ManagedPackage) { - const triggerRef = useRef(null) - const { provider, slug } = Route.useParams() - const [open, setOpen] = useState(false) - const clusters = useClusters() - const browserContext = useBrowserContext() - const { versions } = useContext(PackageContext) +function InstallButton() { + const triggerRef = useRef(null); + const { provider } = Route.useParams(); + const [open, setOpen] = useState(false); + const clusters = useClusters(); + const browserContext = useBrowserContext(); + const { versions } = useContext(PackageContext); const version = useMemo(() => { - if (!versions || !browserContext.cluster) return undefined; + if (!versions || !browserContext.cluster) + return undefined; return versions.items.findLast(version => version.mc_versions.includes(browserContext.cluster!.mc_version) - && version.loaders.includes(browserContext.cluster!.mc_loader) - ) - }, [browserContext.cluster, versions]) + && version.loaders.includes(browserContext.cluster!.mc_loader)); + }, [browserContext.cluster, versions]); function download() { - if (!version || !browserContext.cluster || !includes(PROVIDERS, provider)) return false; - downloadPackage(browserContext.cluster, provider, version) + if (!version || !browserContext.cluster || !includes(PROVIDERS, provider)) + return false; + downloadPackage(browserContext.cluster, provider, version); } - return (
@@ -229,42 +217,50 @@ function InstallButton({ ...pkg }: ManagedPackage) { - { + const cluster = clusters?.find(cluster => cluster.id as unknown as number === e); + if (cluster) + browserContext.setCluster(cluster); + }} + > +
- + - {item => {item.name}} + {item => ( + + {item.name} + + )}
- - ) + ); } - function Author({ author }: { author: ManagedUser }) { - - //TODO: onclick and fallback avatar url + // TODO: onclick and fallback avatar url return ( { if (author.url) openUrl(author.url) }} + onClick={() => { + if (author.url) + openUrl(author.url); + }} > - {`${author.username}'s + {`${author.username}'s
{author.username} @@ -279,9 +275,9 @@ function Author({ author }: { author: ManagedUser }) { - ) + ); } function downloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) { - return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility) + return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility); } From 6f3a09b52f47530bb53c42ff142a41f86a3ea168 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:03:09 +0200 Subject: [PATCH 02/18] chore: change button label --- .../frontend/src/routes/app/browser/index.tsx | 4 ++-- .../frontend/src/routes/app/browser/route.tsx | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx index dad33499..58a45f37 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/index.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/index.tsx @@ -53,7 +53,7 @@ function Featured() {
@@ -85,7 +85,7 @@ function List({ provider }: { provider: Provider }) { const [expanded, setExpanded] = useState(false); return (
-

{provider}

+
{provider}
{search.isSuccess ? diff --git a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx index ab0ae053..243adaa5 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/route.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/route.tsx @@ -18,9 +18,9 @@ function RouteComponent() {
@@ -122,7 +122,7 @@ function BrowserCategories() {
-
Categories
+
Categories
{categories.map(category => (

} - onChange={e=>setQuery(e.currentTarget.value)} + onChange={e => setQuery(e.currentTarget.value)} onKeyDown={(e) => { if (e.key !== 'Enter') return; @@ -159,6 +158,7 @@ function BrowserToolbar() { context.setQuery({ ...context.query, query: e.currentTarget.value }); }} placeholder="Search for content" + value={query} />
From 7f67d2d7692783202bd29221b82cc8dc0cbc30e4 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:20:27 +0200 Subject: [PATCH 03/18] chore: switch to hash routing --- apps/onelauncher/frontend/src/main.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/onelauncher/frontend/src/main.tsx b/apps/onelauncher/frontend/src/main.tsx index 930f49bd..547eea8b 100644 --- a/apps/onelauncher/frontend/src/main.tsx +++ b/apps/onelauncher/frontend/src/main.tsx @@ -1,6 +1,6 @@ import { isDev, registerDevExperience, registerNativeExperience } from '@onelauncher/common'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { createRouter, RouterProvider } from '@tanstack/react-router'; +import { createHashHistory, createRouter, RouterProvider } from '@tanstack/react-router'; import { StrictMode } from 'react'; import ReactDOM from 'react-dom/client'; import { createTauRPCProxy } from './bindings.gen'; @@ -25,6 +25,8 @@ const queryClient = new QueryClient({ }, }); +const hashHistory = createHashHistory(); + // Tanstack Router const router = createRouter({ routeTree, @@ -36,6 +38,7 @@ const router = createRouter({ defaultStructuralSharing: true, defaultPreloadStaleTime: 0, defaultNotFoundComponent: () =>
Not Found
, + history: hashHistory, }); declare module '@tanstack/react-router' { From 52a8382fcd27275fcf42bbeaf474737cc63de8a4 Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:20:40 +0200 Subject: [PATCH 04/18] fix: fix package image blur --- .../frontend/src/components/content/PackageItem.tsx | 6 +++--- .../src/routes/app/browser/package.$provider.$slug.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx index 63e9830f..a3a9d522 100644 --- a/apps/onelauncher/frontend/src/components/content/PackageItem.tsx +++ b/apps/onelauncher/frontend/src/components/content/PackageItem.tsx @@ -15,9 +15,9 @@ export function PackageGrid({ items, provider }: { items: Array; p } export function PackageItem({ provider, ...item }: SearchResult & { provider: Provider }) { - const navigate = useNavigate() + const navigate = useNavigate(); function redirect() { - navigate({to: "/app/browser/package/$provider/$slug", params: {provider, slug: item.slug}}) + navigate({ to: '/app/browser/package/$provider/$slug', params: { provider, slug: item.slug } }); } return ( @@ -37,7 +37,7 @@ export function PackageItem({ provider, ...item }: SearchResult & { provider: Pr )} when={item.icon_url} > - {`Icon + {`Icon {`Icon
- {`Icon + {`Icon {`Icon
From 142e40856d7cf82f2f2f1dd2a317c30e62479be7 Mon Sep 17 00:00:00 2001 From: emirsassan Date: Sun, 20 Jul 2025 18:55:13 +0300 Subject: [PATCH 05/18] 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({