From 76872df621990bbe3940d01a6503892ed88156f4 Mon Sep 17 00:00:00 2001 From: Luc Date: Tue, 26 Nov 2024 21:35:54 +0100 Subject: [PATCH] Update to use @tanstack/react-query --- web/.eslintrc.json | 2 +- web/package.json | 4 +- web/pnpm-lock.yaml | 74 +++++++++++++++------ web/src/api/core.ts | 59 ++++++++-------- web/src/api/geoip.ts | 44 +++++------- web/src/api/me.ts | 52 +++++++-------- web/src/components/Navbar.tsx | 3 +- web/src/components/UserProfile.tsx | 11 ++- web/src/components/input/BaseInput.tsx | 49 ++++++++++++++ web/src/components/input/NewItemIdInput.tsx | 62 +++++++++++++++++ web/src/index.css | 61 ++++++----------- web/src/index.tsx | 7 +- web/src/routeTree.gen.ts | 20 +++++- web/src/routes/__root.tsx | 3 +- web/src/routes/create.lazy.tsx | 36 ++++++++++ 15 files changed, 336 insertions(+), 151 deletions(-) create mode 100644 web/src/components/input/BaseInput.tsx create mode 100644 web/src/components/input/NewItemIdInput.tsx create mode 100644 web/src/routes/create.lazy.tsx diff --git a/web/.eslintrc.json b/web/.eslintrc.json index bc3994c..ff6d96a 100644 --- a/web/.eslintrc.json +++ b/web/.eslintrc.json @@ -13,7 +13,7 @@ "v3xlabs" ], "env": { - "node": true + "browser": true }, "rules": {} } \ No newline at end of file diff --git a/web/package.json b/web/package.json index 853252f..b77f390 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,8 @@ "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", + "@radix-ui/react-label": "^2.1.0", + "@tanstack/react-query": "^5.x.x", "@tanstack/react-router": "^1.45.14", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", @@ -29,8 +31,8 @@ "postcss": "^8.4.38", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-icons": "^5.3.0", "react-leaflet": "^4.2.1", - "swr": "^2.2.5", "tailwindcss": "^3.4.3", "ts-pattern": "^5.5.0", "ua-parser-js": "^1.0.38", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index c648df1..c456758 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -26,6 +26,12 @@ importers: '@radix-ui/react-hover-card': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.0 + version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.x.x + version: 5.61.4(react@18.3.1) '@tanstack/react-router': specifier: ^1.45.14 version: 1.45.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -56,12 +62,12 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-icons: + specifier: ^5.3.0 + version: 5.3.0(react@18.3.1) react-leaflet: specifier: ^4.2.1 version: 4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - swr: - specifier: ^2.2.5 - version: 2.2.5(react@18.3.1) tailwindcss: specifier: ^3.4.3 version: 3.4.3 @@ -602,6 +608,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.0': + resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-menu@2.1.2': resolution: {integrity: sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==} peerDependencies: @@ -837,6 +856,14 @@ packages: resolution: {integrity: sha512-n4XXInV9irIq0obRvINIkESkGk280Q+xkIIbswmM0z9nAu2wsIRZNvlmPrtYh6bgNWtItOWWoihFUjLTW8g6Jg==} engines: {node: '>=12'} + '@tanstack/query-core@5.61.4': + resolution: {integrity: sha512-rsnemyhPvEG4ViZe0R2UQDM8NgQS/BNC5/Gf9RTs0TKN5thUhPUwnL2anWG4jxAGKFyDfvG7PXbx6MRq3hxi1w==} + + '@tanstack/react-query@5.61.4': + resolution: {integrity: sha512-Nh5+0V4fRVShSeDHFTVvzJrvwTdafIvqxyZUrad71kJWL7J+J5Wrd/xcHTWfSL1mR/9eoufd2roXOpL3F16ECA==} + peerDependencies: + react: ^18 || ^19 + '@tanstack/react-router@1.45.14': resolution: {integrity: sha512-eGSOaZI2urexpkoJb+SzOAnoZ61IuMzCzBziU6HD9b6lnySXQ2Z4LFGcHx2+BK5rzkGXtkfOoGLtQ2SE6lFTdA==} engines: {node: '>=12'} @@ -1160,9 +1187,6 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2111,6 +2135,11 @@ packages: peerDependencies: react: ^18.3.1 + react-icons@5.3.0: + resolution: {integrity: sha512-DnUk8aFbTyQPSkCfF8dbX6kQjXA9DktMeJqfjrg6cK9vwQVMxmcA3BfP4QoiztVmEHtwlTgLFsPuH2NskKT6eg==} + peerDependencies: + react: '*' + react-leaflet@4.2.1: resolution: {integrity: sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==} peerDependencies: @@ -2338,11 +2367,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - swr@2.2.5: - resolution: {integrity: sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 - tailwindcss@3.4.3: resolution: {integrity: sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==} engines: {node: '>=14.0.0'} @@ -3045,6 +3069,15 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-label@2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-menu@2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -3240,6 +3273,13 @@ snapshots: '@tanstack/history@1.45.3': {} + '@tanstack/query-core@5.61.4': {} + + '@tanstack/react-query@5.61.4(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.61.4 + react: 18.3.1 + '@tanstack/react-router@1.45.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/history': 1.45.3 @@ -3630,8 +3670,6 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - client-only@0.0.1: {} - clsx@2.1.1: {} color-convert@1.9.3: @@ -4666,6 +4704,10 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-icons@5.3.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -4937,12 +4979,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - swr@2.2.5(react@18.3.1): - dependencies: - client-only: 0.0.1 - react: 18.3.1 - use-sync-external-store: 1.2.2(react@18.3.1) - tailwindcss@3.4.3: dependencies: '@alloc/quick-lru': 5.2.0 diff --git a/web/src/api/core.ts b/web/src/api/core.ts index df8d0a5..9b9935d 100644 --- a/web/src/api/core.ts +++ b/web/src/api/core.ts @@ -1,34 +1,37 @@ -import useSWR from 'swr'; +import { useMutation, useQuery } from '@tanstack/react-query'; -import { useAuth } from './auth'; +export const BASE_URL = 'http://localhost:3000'; -const BASE_URL = 'http://localhost:3000'; +// Replace SWR fetcher with axios or fetch implementation +// eslint-disable-next-line no-undef +export async function fetcher(url: string, init?: RequestInit): Promise { + const response = await fetch(url, init); -export const useHttp = (url: string) => { - const { token, clearAuthToken } = useAuth(); + if (!response.ok) { + throw new Error('Network response was not ok'); + } - return useSWR(token && url, async (): Promise => { - const headers = new Headers(); + return response.json(); +} - headers.append('Authorization', 'Bearer ' + token); - - try { - const response = await fetch(BASE_URL + url, { headers }); - - if (response.status === 401) { - console.log('Token expired, clearing token'); - clearAuthToken(); - - return null; - } - - const data = (await response.json()) as K; - - return data as K; - } catch (error) { - console.error(error); - - return null; - } +// Replace useSWR hooks with useQuery +export function useHttp(key: string) { + return useQuery({ + queryKey: [key], + queryFn: () => fetcher(key), + }); +} + +// For mutations (if you have any POST/PUT/DELETE operations) +export function useUpdateData(url: string) { + return useMutation({ + mutationFn: (data: T) => + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }).then((response) => response.json()), }); -}; +} diff --git a/web/src/api/geoip.ts b/web/src/api/geoip.ts index 0f4566c..45333bb 100644 --- a/web/src/api/geoip.ts +++ b/web/src/api/geoip.ts @@ -1,30 +1,20 @@ -import useSWR from 'swr'; +import { useQuery } from '@tanstack/react-query'; -type GeoIpResponse = { - ip_address: string; - latitude: number; - longitude: number; - postal_code: string; - continent_code: string; - continent_name: string; - country_code: string; - country_name: string; - region_code: string; - region_name: string; - province_code: string; - province_name: string; - city_name: string; - timezone: string; -}; +import { BASE_URL, fetcher } from './core'; -export const useGeoIp = (ip: string) => - useSWR( - 'geo:' + ip, - async () => { - const response = await fetch('https://api.geoip.rs/?ip=' + ip); - const data = await response.json(); +interface GeoIPResponse { + ip: string; + country: string; + city?: string; + // Add other fields based on your API response +} - return data as GeoIpResponse; - }, - { errorRetryInterval: 10_000 } - ); +export function useGeoIp() { + return useQuery({ + queryKey: ['geoip'], + queryFn: () => fetcher(`${BASE_URL}/api/geoip`), + // Add any specific options you need: + // staleTime: 5 * 60 * 1000, // Consider data fresh for 5 minutes + // cacheTime: 30 * 60 * 1000, // Keep data in cache for 30 minutes + }); +} diff --git a/web/src/api/me.ts b/web/src/api/me.ts index 933d445..0a06dd9 100644 --- a/web/src/api/me.ts +++ b/web/src/api/me.ts @@ -1,32 +1,26 @@ -import { useHttp } from './core'; +import { useQuery } from '@tanstack/react-query'; -export type ApiMeResponse = { - id: number; - oauth_sub: string; +import { useAuth } from './auth'; +import { BASE_URL, fetcher } from './core'; + +interface MeResponse { + id: string; name: string; - picture: string; - // oauth_data: { - // sub: string; - // name: string; - // given_name: string; - // family_name: string; - // middle_name: null; - // nickname: null; - // preferred_username: null; - // profile: null; - // picture: string; - // website: null; - // email: string; - // email_verified: boolean; - // gender: null; - // birthdate: null; - // zoneinfo: null; - // locale: null; - // phone_number: null; - // phone_number_verified: boolean; - // address: null; - // updated_at: null; - // }; -}; + picture?: string; + // Add other fields as needed +} + +export function useApiMe() { + const { token } = useAuth(); -export const useApiMe = () => useHttp('/api/me'); + return useQuery({ + queryKey: ['me', token], + queryFn: () => + fetcher(`${BASE_URL}/api/me`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }), + enabled: !!token, // Only run query if token exists + }); +} diff --git a/web/src/components/Navbar.tsx b/web/src/components/Navbar.tsx index 567827c..7bed9b7 100644 --- a/web/src/components/Navbar.tsx +++ b/web/src/components/Navbar.tsx @@ -27,6 +27,7 @@ export const Navbar = () => {
{[ ['/', 'Home'], + ['/create', 'Create'], ['/sessions', 'Sessions'], ].map(([path, name]) => ( { ))}
- {meData && ( + {token && meData && (
diff --git a/web/src/components/UserProfile.tsx b/web/src/components/UserProfile.tsx index f85b30c..a237eba 100644 --- a/web/src/components/UserProfile.tsx +++ b/web/src/components/UserProfile.tsx @@ -4,9 +4,11 @@ import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import { FC } from 'react'; import { match } from 'ts-pattern'; +import { useQuery } from '@tanstack/react-query'; import { ApiMeResponse, useApiMe } from '../api/me'; import { useApiUserById } from '../api/user'; +import { fetcher } from '../api/core'; type Properties = { user_id: string; @@ -100,7 +102,14 @@ export const getInitials = (name?: string) => { const UNKNOWN_USER = 'Unknown User'; export const UserProfile: FC = ({ user_id, variant }) => { - const { data: user } = useApiUserById(user_id); + const { data: user, isLoading } = useQuery({ + queryKey: ['user', user_id], + queryFn: () => fetcher(`${BASE_URL}/api/users/${user_id}`), + }); + + if (isLoading) { + return
Loading...
; + } return (
diff --git a/web/src/components/input/BaseInput.tsx b/web/src/components/input/BaseInput.tsx new file mode 100644 index 0000000..9cacf4f --- /dev/null +++ b/web/src/components/input/BaseInput.tsx @@ -0,0 +1,49 @@ +import * as Label from '@radix-ui/react-label'; +import clsx from 'clsx'; +import { ReactNode } from 'react'; + +export type BaseInputProperties = { + label: string; + placeholder?: string; + defaultValue?: string; + id?: string; + className?: string; + type?: HTMLInputElement['type']; + suffix?: ReactNode; + value?: string; + onChange?: (value: string) => void; +}; + +export const BaseInput = ({ + label, + placeholder, + defaultValue, + id, + className, + type, + suffix, + value, + onChange, +}: BaseInputProperties) => { + return ( + <> + + {label} + +
+
+ onChange?.(event.target.value)} + value={value} + /> +
+ {suffix} +
+ + ); +}; diff --git a/web/src/components/input/NewItemIdInput.tsx b/web/src/components/input/NewItemIdInput.tsx new file mode 100644 index 0000000..caa5517 --- /dev/null +++ b/web/src/components/input/NewItemIdInput.tsx @@ -0,0 +1,62 @@ +import { useMutation } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import { FiRefreshCcw } from 'react-icons/fi'; + +import { BaseInput, BaseInputProperties } from './BaseInput'; + +const placeholderValues = ['000001', '123456', 'AA17C', 'ABCDEF', '000013']; + +export const NewItemIdInput = (properties: BaseInputProperties) => { + const [value, setValue] = useState(properties.defaultValue); + const [placeholderValue, setPlaceholderValue] = useState( + placeholderValues[0] + ); + const { mutate: generateId } = useMutation({ + mutationFn: async () => { + // TODO: Hook up to API Endpoint + // Currently just returns a random value + + return placeholderValues[ + Math.floor(Math.random() * placeholderValues.length) + ]; + }, + onSuccess: (data) => { + setValue(data); + }, + }); + + useEffect(() => { + if (value) return; + + const interval = setInterval(() => { + setPlaceholderValue( + placeholderValues[ + Math.floor(Math.random() * placeholderValues.length) + ] + ); + }, 2000); + + return () => clearInterval(interval); + }, [value]); + + return ( + +
+ +
+ + } + /> + ); +}; diff --git a/web/src/index.css b/web/src/index.css index de19b26..f5aef21 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -3,7 +3,11 @@ @tailwind utilities; .btn { - @apply bg-white text-neutral-800 border border-neutral-200 px-2.5 py-0.5 h-fit hover:bg-neutral-50 rounded-md focus:outline focus:outline-2 outline-offset-2 outline-blue-500; + @apply bg-white text-neutral-800 border border-neutral-200 px-3 py-1 h-fit hover:bg-neutral-50 rounded-md focus:outline focus:outline-2 outline-offset-2 outline-blue-500; +} + +.h1 { + @apply text-2xl font-semibold; } .AvatarRoot { @@ -276,46 +280,21 @@ a { color: #8E8C99; } -@keyframes slideUpAndFade { - from { - opacity: 0; - transform: translateY(2px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes slideRightAndFade { - from { - opacity: 0; - transform: translateX(-2px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -@keyframes slideDownAndFade { - from { - opacity: 0; - transform: translateY(-2px); - } - to { - opacity: 1; - transform: translateY(0); - } +.LabelRoot { + font-size: 15px; + font-weight: 500; + line-height: 35px; + color: black; } -@keyframes slideLeftAndFade { - from { - opacity: 0; - transform: translateX(2px); - } - to { - opacity: 1; - transform: translateX(0); - } +.Input { + width: 200px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 10px; + height: 35px; + font-size: 15px; + line-height: 1; + @apply border rounded-md focus:outline focus:outline-2 outline-offset-2 outline-blue-500; } diff --git a/web/src/index.tsx b/web/src/index.tsx index 18d8906..695a6b8 100644 --- a/web/src/index.tsx +++ b/web/src/index.tsx @@ -1,5 +1,6 @@ import './index.css'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { createRouter, RouterProvider } from '@tanstack/react-router'; import React from 'react'; import ReactDOM from 'react-dom/client'; @@ -20,8 +21,12 @@ declare module '@tanstack/react-router' { preflightAuth(); +const queryClient = new QueryClient(); + ReactDOM.createRoot(document.querySelector('#root')!).render( - + + + ); diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts index e5a03a1..2ac8bc2 100644 --- a/web/src/routeTree.gen.ts +++ b/web/src/routeTree.gen.ts @@ -17,11 +17,17 @@ import { Route as SessionsImport } from './routes/sessions' // Create Virtual Routes +const CreateLazyImport = createFileRoute('/create')() const AboutLazyImport = createFileRoute('/about')() const IndexLazyImport = createFileRoute('/')() // Create/Update Routes +const CreateLazyRoute = CreateLazyImport.update({ + path: '/create', + getParentRoute: () => rootRoute, +} as any).lazy(() => import('./routes/create.lazy').then((d) => d.Route)) + const AboutLazyRoute = AboutLazyImport.update({ path: '/about', getParentRoute: () => rootRoute, @@ -62,6 +68,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AboutLazyImport parentRoute: typeof rootRoute } + '/create': { + id: '/create' + path: '/create' + fullPath: '/create' + preLoaderRoute: typeof CreateLazyImport + parentRoute: typeof rootRoute + } } } @@ -71,6 +84,7 @@ export const routeTree = rootRoute.addChildren({ IndexLazyRoute, SessionsRoute, AboutLazyRoute, + CreateLazyRoute, }) /* prettier-ignore-end */ @@ -83,7 +97,8 @@ export const routeTree = rootRoute.addChildren({ "children": [ "/", "/sessions", - "/about" + "/about", + "/create" ] }, "/": { @@ -94,6 +109,9 @@ export const routeTree = rootRoute.addChildren({ }, "/about": { "filePath": "about.lazy.tsx" + }, + "/create": { + "filePath": "create.lazy.tsx" } } } diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx index 8a67195..c1db21e 100644 --- a/web/src/routes/__root.tsx +++ b/web/src/routes/__root.tsx @@ -1,4 +1,5 @@ import { createRootRoute, Outlet } from '@tanstack/react-router'; +import { TanStackRouterDevtools } from '@tanstack/router-devtools'; import { Navbar } from '../components/Navbar'; @@ -7,7 +8,7 @@ export const Route = createRootRoute({ <> - {/* */} + ), }); diff --git a/web/src/routes/create.lazy.tsx b/web/src/routes/create.lazy.tsx new file mode 100644 index 0000000..6cef510 --- /dev/null +++ b/web/src/routes/create.lazy.tsx @@ -0,0 +1,36 @@ +import { createLazyFileRoute } from '@tanstack/react-router'; +import { FiArrowRight } from 'react-icons/fi'; + +import { NewItemIdInput } from '../components/input/NewItemIdInput'; + +const component = () => { + return ( +
+

Create new Item

+
+
+ +
+

+ To create a new item you will need to give it an identifier. + You can choose to use a generated identifier (by clicking + the generate icon) or you can choose to provide your own + (a-zA-Z0-9). +

+
+ +
+
+
+ ); +}; + +export const Route = createLazyFileRoute('/create')({ + component, +});