From 790bd2e28c2e28ec15b631e472cbc452285e1102 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 16 Jun 2026 20:15:36 +0200 Subject: [PATCH] refactor(v5): migrate frontend from JSX to TypeScript Convert all v5 JS/JSX source files to TS/TSX, add tsconfig.json, install typescript and @types/react* dev deps, add typecheck npm script, create types.ts with shared prop types, and update Tailwind CSS source glob and shadcn components.json to include TS extensions. --- components.json | 2 +- jsconfig.json | 9 - package-lock.json | 61 +++++- package.json | 6 +- resources/css/v5/app.css | 2 +- resources/js/v5/Pages/{Home.jsx => Home.tsx} | 3 +- resources/js/v5/{app.jsx => app.tsx} | 2 +- .../{app-navbar.jsx => app-navbar.tsx} | 31 ++- resources/js/v5/components/ui/button.jsx | 55 ----- resources/js/v5/components/ui/button.tsx | 46 +++++ resources/js/v5/components/ui/select.jsx | 194 ------------------ resources/js/v5/components/ui/select.tsx | 178 ++++++++++++++++ resources/js/v5/components/ui/separator.jsx | 24 --- resources/js/v5/components/ui/separator.tsx | 24 +++ resources/js/v5/lib/{csrf.js => csrf.ts} | 2 +- resources/js/v5/lib/utils.js | 6 - resources/js/v5/lib/utils.ts | 6 + resources/js/v5/types.ts | 46 +++++ resources/js/v5/vite-env.d.ts | 1 + resources/views/v5/app.blade.php | 4 +- tests/Feature/V5/HomeTest.php | 23 ++- tsconfig.json | 35 ++++ vite.config.js | 2 +- 23 files changed, 442 insertions(+), 320 deletions(-) delete mode 100644 jsconfig.json rename resources/js/v5/Pages/{Home.jsx => Home.tsx} (95%) rename resources/js/v5/{app.jsx => app.tsx} (87%) rename resources/js/v5/components/{app-navbar.jsx => app-navbar.tsx} (83%) delete mode 100644 resources/js/v5/components/ui/button.jsx create mode 100644 resources/js/v5/components/ui/button.tsx delete mode 100644 resources/js/v5/components/ui/select.jsx create mode 100644 resources/js/v5/components/ui/select.tsx delete mode 100644 resources/js/v5/components/ui/separator.jsx create mode 100644 resources/js/v5/components/ui/separator.tsx rename resources/js/v5/lib/{csrf.js => csrf.ts} (71%) delete mode 100644 resources/js/v5/lib/utils.js create mode 100644 resources/js/v5/lib/utils.ts create mode 100644 resources/js/v5/types.ts create mode 100644 resources/js/v5/vite-env.d.ts create mode 100644 tsconfig.json diff --git a/components.json b/components.json index 957eb979e..e05f45629 100644 --- a/components.json +++ b/components.json @@ -2,7 +2,7 @@ "$schema": "https://ui.shadcn.com/schema.json", "style": "base-lyra", "rsc": false, - "tsx": false, + "tsx": true, "tailwind": { "config": "", "css": "resources/css/v5/app.css", diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 4c5007b0c..000000000 --- a/jsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@/*": ["resources/js/v5/*"] - } - }, - "include": ["resources/js/v5/**/*.js", "resources/js/v5/**/*.jsx"] -} diff --git a/package-lock.json b/package-lock.json index 7e133b342..edb3318ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,12 +25,15 @@ }, "devDependencies": { "@tailwindcss/postcss": "4.1.18", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.2.0", "laravel-vite-plugin": "2.0.1", "postcss": "8.5.15", "shadcn": "^4.11.0", "tailwind-scrollbar": "4.0.2", "tailwindcss": "4.1.18", + "typescript": "^6.0.3", "vite": "7.3.2" } }, @@ -78,6 +81,7 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1401,6 +1405,7 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2268,6 +2273,27 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "devOptional": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, "node_modules/@types/validate-npm-package-name": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/validate-npm-package-name/-/validate-npm-package-name-4.0.2.tgz", @@ -2309,7 +2335,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/accepts": { "version": "2.0.0", @@ -2528,6 +2555,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -2919,6 +2947,13 @@ "node": ">=4" } }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -3383,6 +3418,7 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3832,6 +3868,7 @@ "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -5484,6 +5521,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5493,6 +5531,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6163,7 +6202,8 @@ "version": "4.1.18", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tapable": { "version": "2.3.0", @@ -6300,6 +6340,21 @@ "url": "https://opencollective.com/express" } }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/undici": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", @@ -6414,6 +6469,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -6599,6 +6655,7 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 3fe770230..e9da5d5c2 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,20 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build" + "build": "vite build", + "typecheck": "tsc --noEmit" }, "devDependencies": { "@tailwindcss/postcss": "4.1.18", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.2.0", "laravel-vite-plugin": "2.0.1", "postcss": "8.5.15", "shadcn": "^4.11.0", "tailwind-scrollbar": "4.0.2", "tailwindcss": "4.1.18", + "typescript": "^6.0.3", "vite": "7.3.2" }, "dependencies": { diff --git a/resources/css/v5/app.css b/resources/css/v5/app.css index 2495b0f38..124e6e4e1 100644 --- a/resources/css/v5/app.css +++ b/resources/css/v5/app.css @@ -3,7 +3,7 @@ @import "shadcn/tailwind.css"; @import "@fontsource-variable/geist"; -@source '../../js/v5/**/*.{js,jsx}'; +@source '../../js/v5/**/*.{js,jsx,ts,tsx}'; @source '../../../storage/framework/views/*.php'; @custom-variant dark (&:where(.dark, .dark *)); diff --git a/resources/js/v5/Pages/Home.jsx b/resources/js/v5/Pages/Home.tsx similarity index 95% rename from resources/js/v5/Pages/Home.jsx rename to resources/js/v5/Pages/Home.tsx index c2dc732c8..9092f6237 100644 --- a/resources/js/v5/Pages/Home.jsx +++ b/resources/js/v5/Pages/Home.tsx @@ -1,6 +1,7 @@ import { Head } from '@inertiajs/react'; import { AppNavbar } from '@/components/app-navbar'; +import type { V5HomeProps } from '@/types'; export default function Home({ flux, @@ -8,7 +9,7 @@ export default function Home({ projects = [], selectedProjectUuid = null, selectedEnvironmentUuid = null, -}) { +}: V5HomeProps) { return ( <> diff --git a/resources/js/v5/app.jsx b/resources/js/v5/app.tsx similarity index 87% rename from resources/js/v5/app.jsx rename to resources/js/v5/app.tsx index fa391dab8..d14f53e73 100644 --- a/resources/js/v5/app.jsx +++ b/resources/js/v5/app.tsx @@ -5,7 +5,7 @@ createInertiaApp({ id: 'v5-app', pages: { path: './Pages', - extension: '.jsx', + extension: '.tsx', }, strictMode: true, }); diff --git a/resources/js/v5/components/app-navbar.jsx b/resources/js/v5/components/app-navbar.tsx similarity index 83% rename from resources/js/v5/components/app-navbar.jsx rename to resources/js/v5/components/app-navbar.tsx index f43b9d503..72cd57f02 100644 --- a/resources/js/v5/components/app-navbar.jsx +++ b/resources/js/v5/components/app-navbar.tsx @@ -2,8 +2,9 @@ import { useMemo, useState } from 'react'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { csrfToken } from '@/lib/csrf'; +import type { SelectItemOption, V5HomeProps, V5Project } from '@/types'; -function persistSelection(projectUuid, environmentUuid) { +function persistSelection(projectUuid: string, environmentUuid: string): void { void fetch('/v5/selection', { method: 'POST', credentials: 'same-origin', @@ -19,27 +20,33 @@ function persistSelection(projectUuid, environmentUuid) { }); } +type AppNavbarProps = V5HomeProps; + export function AppNavbar({ flux, clusters = [], projects = [], selectedProjectUuid = null, selectedEnvironmentUuid = null, -}) { +}: AppNavbarProps) { const firstProject = projects[0] ?? null; - const [projectUuid, setProjectUuid] = useState(selectedProjectUuid ?? firstProject?.uuid ?? ''); - const selectedProject = useMemo( + const [projectUuid, setProjectUuid] = useState(selectedProjectUuid ?? firstProject?.uuid ?? ''); + const selectedProject = useMemo( () => projects.find((project) => project.uuid === projectUuid) ?? firstProject, [firstProject, projectUuid, projects], ); const firstEnvironment = selectedProject?.environments?.[0] ?? null; - const [environmentUuid, setEnvironmentUuid] = useState(selectedEnvironmentUuid ?? firstEnvironment?.uuid ?? ''); + const [environmentUuid, setEnvironmentUuid] = useState(selectedEnvironmentUuid ?? firstEnvironment?.uuid ?? ''); const selectedEnvironment = useMemo( () => selectedProject?.environments?.find((environment) => environment.uuid === environmentUuid) ?? firstEnvironment, [environmentUuid, firstEnvironment, selectedProject], ); - function selectProject(nextProjectUuid) { + function selectProject(nextProjectUuid: string | null): void { + if (nextProjectUuid === null) { + return; + } + const nextProject = projects.find((project) => project.uuid === nextProjectUuid); const nextEnvironmentUuid = nextProject?.environments?.[0]?.uuid ?? ''; @@ -48,16 +55,20 @@ export function AppNavbar({ persistSelection(nextProjectUuid, nextEnvironmentUuid); } - function selectEnvironment(nextEnvironmentUuid) { + function selectEnvironment(nextEnvironmentUuid: string | null): void { + if (nextEnvironmentUuid === null) { + return; + } + setEnvironmentUuid(nextEnvironmentUuid); persistSelection(projectUuid, nextEnvironmentUuid); } - const projectItems = projects.map((project) => ({ + const projectItems: SelectItemOption[] = projects.map((project) => ({ label: project.name, value: project.uuid, })); - const environmentItems = (selectedProject?.environments ?? []).map((environment) => ({ + const environmentItems: SelectItemOption[] = (selectedProject?.environments ?? []).map((environment) => ({ label: environment.name, value: environment.uuid, })); @@ -120,7 +131,7 @@ export function AppNavbar({
Flux: {flux?.label ?? 'Unknown'} ยท {clusters.length} clusters
diff --git a/resources/js/v5/components/ui/button.jsx b/resources/js/v5/components/ui/button.jsx deleted file mode 100644 index aecb23ca4..000000000 --- a/resources/js/v5/components/ui/button.jsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button" -import { cva } from "class-variance-authority"; - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", - destructive: - "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - icon: "size-8", - "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-7 rounded-none", - "icon-lg": "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -function Button({ - className, - variant = "default", - size = "default", - ...props -}) { - return ( - - ); -} - -export { Button, buttonVariants } diff --git a/resources/js/v5/components/ui/button.tsx b/resources/js/v5/components/ui/button.tsx new file mode 100644 index 000000000..d5fae298f --- /dev/null +++ b/resources/js/v5/components/ui/button.tsx @@ -0,0 +1,46 @@ +import { Button as ButtonPrimitive } from '@base-ui/react/button'; +import { cva, type VariantProps } from 'class-variance-authority'; +import type * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/80', + outline: + 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', + ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50', + destructive: + 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", + lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + icon: 'size-8', + 'icon-xs': "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", + 'icon-sm': 'size-7 rounded-none', + 'icon-lg': 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +type ButtonProps = React.ComponentProps & VariantProps; + +function Button({ className, variant = 'default', size = 'default', ...props }: ButtonProps) { + return ; +} + +export { Button, buttonVariants }; diff --git a/resources/js/v5/components/ui/select.jsx b/resources/js/v5/components/ui/select.jsx deleted file mode 100644 index 8be7b3b4b..000000000 --- a/resources/js/v5/components/ui/select.jsx +++ /dev/null @@ -1,194 +0,0 @@ -import * as React from "react" -import { Select as SelectPrimitive } from "@base-ui/react/select" - -import { cn } from "@/lib/utils" -import { CaretDownIcon, CheckIcon, CaretUpIcon } from "@phosphor-icons/react" - -const Select = SelectPrimitive.Root - -function SelectGroup({ - className, - ...props -}) { - return ( - - ); -} - -function SelectValue({ - className, - ...props -}) { - return ( - - ); -} - -function SelectTrigger({ - className, - size = "default", - variant = "default", - children, - ...props -}) { - return ( - - {children} - - } /> - - ); -} - -function SelectContent({ - className, - children, - position, - side = "bottom", - sideOffset = 4, - align = "center", - alignOffset = 0, - alignItemWithTrigger = position === "popper" ? false : true, - ...props -}) { - return ( - - - - - {children} - - - - - ); -} - -function SelectLabel({ - className, - ...props -}) { - return ( - - ); -} - -function SelectItem({ - className, - children, - ...props -}) { - return ( - - - {children} - - - }> - - - - ); -} - -function SelectSeparator({ - className, - ...props -}) { - return ( - - ); -} - -function SelectScrollUpButton({ - className, - ...props -}) { - return ( - - - - ); -} - -function SelectScrollDownButton({ - className, - ...props -}) { - return ( - - - - ); -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -} diff --git a/resources/js/v5/components/ui/select.tsx b/resources/js/v5/components/ui/select.tsx new file mode 100644 index 000000000..36f011c2d --- /dev/null +++ b/resources/js/v5/components/ui/select.tsx @@ -0,0 +1,178 @@ +import { Select as SelectPrimitive } from '@base-ui/react/select'; +import { CaretDownIcon, CaretUpIcon, CheckIcon } from '@phosphor-icons/react'; +import type * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Select = SelectPrimitive.Root; + +type SelectGroupProps = React.ComponentProps; + +function SelectGroup({ className, ...props }: SelectGroupProps) { + return ; +} + +type SelectValueProps = React.ComponentProps; + +function SelectValue({ className, ...props }: SelectValueProps) { + return ; +} + +type SelectTriggerProps = React.ComponentProps & { + size?: 'default' | 'sm'; + variant?: 'default' | 'ghost'; +}; + +function SelectTrigger({ className, size = 'default', variant = 'default', children, ...props }: SelectTriggerProps) { + return ( + + {children} + } /> + + ); +} + +type SelectPositionerProps = React.ComponentProps; +type SelectPopupProps = React.ComponentProps; + +type SelectContentProps = SelectPopupProps & { + position?: 'popper' | 'item-aligned'; + side?: SelectPositionerProps['side']; + sideOffset?: SelectPositionerProps['sideOffset']; + align?: SelectPositionerProps['align']; + alignOffset?: SelectPositionerProps['alignOffset']; + alignItemWithTrigger?: SelectPositionerProps['alignItemWithTrigger']; +}; + +function SelectContent({ + className, + children, + position, + side = 'bottom', + sideOffset = 4, + align = 'center', + alignOffset = 0, + alignItemWithTrigger = position === 'popper' ? false : true, + ...props +}: SelectContentProps) { + return ( + + + + + {children} + + + + + ); +} + +type SelectLabelProps = React.ComponentProps; + +function SelectLabel({ className, ...props }: SelectLabelProps) { + return ( + + ); +} + +type SelectItemProps = React.ComponentProps; + +function SelectItem({ className, children, ...props }: SelectItemProps) { + return ( + + {children} + } + > + + + + ); +} + +type SelectSeparatorProps = React.ComponentProps; + +function SelectSeparator({ className, ...props }: SelectSeparatorProps) { + return ; +} + +type SelectScrollUpButtonProps = React.ComponentProps; + +function SelectScrollUpButton({ className, ...props }: SelectScrollUpButtonProps) { + return ( + + + + ); +} + +type SelectScrollDownButtonProps = React.ComponentProps; + +function SelectScrollDownButton({ className, ...props }: SelectScrollDownButtonProps) { + return ( + + + + ); +} + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/resources/js/v5/components/ui/separator.jsx b/resources/js/v5/components/ui/separator.jsx deleted file mode 100644 index f4276385a..000000000 --- a/resources/js/v5/components/ui/separator.jsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client" - -import { Separator as SeparatorPrimitive } from "@base-ui/react/separator" - -import { cn } from "@/lib/utils" - -function Separator({ - className, - orientation = "horizontal", - ...props -}) { - return ( - - ); -} - -export { Separator } diff --git a/resources/js/v5/components/ui/separator.tsx b/resources/js/v5/components/ui/separator.tsx new file mode 100644 index 000000000..5b5eb7eae --- /dev/null +++ b/resources/js/v5/components/ui/separator.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { Separator as SeparatorPrimitive } from '@base-ui/react/separator'; +import type * as React from 'react'; + +import { cn } from '@/lib/utils'; + +type SeparatorProps = React.ComponentProps; + +function Separator({ className, orientation = 'horizontal', ...props }: SeparatorProps) { + return ( + + ); +} + +export { Separator }; diff --git a/resources/js/v5/lib/csrf.js b/resources/js/v5/lib/csrf.ts similarity index 71% rename from resources/js/v5/lib/csrf.js rename to resources/js/v5/lib/csrf.ts index 2a23b52e5..88b519fa1 100644 --- a/resources/js/v5/lib/csrf.js +++ b/resources/js/v5/lib/csrf.ts @@ -1,3 +1,3 @@ -export function csrfToken() { +export function csrfToken(): string { return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? ''; } diff --git a/resources/js/v5/lib/utils.js b/resources/js/v5/lib/utils.js deleted file mode 100644 index a25ae45da..000000000 --- a/resources/js/v5/lib/utils.js +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; - -export function cn(...inputs) { - return twMerge(clsx(inputs)); -} diff --git a/resources/js/v5/lib/utils.ts b/resources/js/v5/lib/utils.ts new file mode 100644 index 000000000..6c5c2752f --- /dev/null +++ b/resources/js/v5/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]): string { + return twMerge(clsx(inputs)); +} diff --git a/resources/js/v5/types.ts b/resources/js/v5/types.ts new file mode 100644 index 000000000..51476c1a4 --- /dev/null +++ b/resources/js/v5/types.ts @@ -0,0 +1,46 @@ +export type FluxStatus = { + available?: boolean; + label?: string | null; + message?: string | null; + socket?: string | null; +}; + +export type V5Server = { + id: string; + name: string; + host: string; + status: string; + capabilities: string[]; +}; + +export type V5Cluster = { + id: string; + name: string; + description: string | null; + serversCount: number; + servers: V5Server[]; +}; + +export type V5Environment = { + uuid: string; + name: string; +}; + +export type V5Project = { + uuid: string; + name: string; + environments: V5Environment[]; +}; + +export type V5HomeProps = { + flux: FluxStatus | null; + clusters?: V5Cluster[]; + projects?: V5Project[]; + selectedProjectUuid?: string | null; + selectedEnvironmentUuid?: string | null; +}; + +export type SelectItemOption = { + label: string; + value: string; +}; diff --git a/resources/js/v5/vite-env.d.ts b/resources/js/v5/vite-env.d.ts new file mode 100644 index 000000000..35306c6fc --- /dev/null +++ b/resources/js/v5/vite-env.d.ts @@ -0,0 +1 @@ +declare module '*.css'; diff --git a/resources/views/v5/app.blade.php b/resources/views/v5/app.blade.php index 07db19fac..2fb234dce 100644 --- a/resources/views/v5/app.blade.php +++ b/resources/views/v5/app.blade.php @@ -28,10 +28,10 @@ window.__vite_plugin_react_preamble_installed__ = true - + @else @viteReactRefresh - @vite('resources/js/v5/app.jsx') + @vite('resources/js/v5/app.tsx') @endif diff --git a/tests/Feature/V5/HomeTest.php b/tests/Feature/V5/HomeTest.php index 597e5b902..e3ff2badd 100644 --- a/tests/Feature/V5/HomeTest.php +++ b/tests/Feature/V5/HomeTest.php @@ -317,8 +317,8 @@ it('rejects persisted v5 selections outside the current team', function () { }); it('defines the v5 home page as a shadcn styled canvas shell', function () { - $homePage = file_get_contents(resource_path('js/v5/Pages/Home.jsx')); - $navbarPath = resource_path('js/v5/components/app-navbar.jsx'); + $homePage = file_get_contents(resource_path('js/v5/Pages/Home.tsx')); + $navbarPath = resource_path('js/v5/components/app-navbar.tsx'); expect(file_exists($navbarPath))->toBeTrue(); @@ -377,18 +377,18 @@ it('defines the v5 home page as a shadcn styled canvas shell', function () { }); it('defines a ghost variant for compact v5 select triggers', function () { - $select = file_get_contents(resource_path('js/v5/components/ui/select.jsx')); + $select = file_get_contents(resource_path('js/v5/components/ui/select.tsx')); expect($select) ->toContain('@base-ui/react/select') ->not->toContain('radix-ui') - ->toContain('variant = "default"') - ->toContain('variant === "default"') - ->toContain('variant === "ghost"') + ->toContain("variant = 'default'") + ->toContain("variant === 'default'") + ->toContain("variant === 'ghost'") ->toContain('border-transparent') ->toContain('h-auto') ->toContain('text-sm') - ->toContain('position === "popper" ? false : true'); + ->toContain("position === 'popper' ? false : true"); }); it('uses the requested shadcn preset configuration for v5', function () { @@ -397,6 +397,7 @@ it('uses the requested shadcn preset configuration for v5', function () { expect($components['style']) ->toBe('base-lyra') + ->and($components['tsx'])->toBeTrue() ->and($components['iconLibrary'])->toBe('phosphor') ->and($components['tailwind']['css'])->toBe('resources/css/v5/app.css') ->and($components['tailwind']['baseColor'])->toBe('zinc') @@ -454,7 +455,7 @@ it('serves v5 dev assets from the current request host', function (string $url, ->assertSuccessful() ->assertSee("import RefreshRuntime from 'http://{$viteHost}:5173/@react-refresh'", false) ->assertSee("src=\"http://{$viteHost}:5173/@vite/client\"", false) - ->assertSee("src=\"http://{$viteHost}:5173/resources/js/v5/app.jsx\"", false) + ->assertSee("src=\"http://{$viteHost}:5173/resources/js/v5/app.tsx\"", false) ->assertDontSee('configured-vite-host.test', false); } finally { if ($originalHotFile === null) { @@ -509,7 +510,7 @@ it('shows when flux is unavailable', function () { }); it('does not include coolify version controls on the v5 home page', function () { - $homePage = file_get_contents(resource_path('js/v5/Pages/Home.jsx')); + $homePage = file_get_contents(resource_path('js/v5/Pages/Home.tsx')); expect($homePage) ->not->toContain('Check coolify version') @@ -518,11 +519,11 @@ it('does not include coolify version controls on the v5 home page', function () }); it('renders flux status as a compact summary', function () { - $navbar = file_get_contents(resource_path('js/v5/components/app-navbar.jsx')); + $navbar = file_get_contents(resource_path('js/v5/components/app-navbar.tsx')); expect($navbar) ->toContain('Flux: {flux?.label ??') - ->toContain('title={flux?.socket ?? flux?.message}') + ->toContain('title={flux?.socket ?? flux?.message ?? undefined}') ->toContain('{clusters.length} clusters') ->not->toContain('

Flux status

') ->not->toContain('

{flux.message}

') diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..0d9072e9b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ES2022" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "paths": { + "@/*": [ + "resources/js/v5/*" + ] + }, + "baseUrl": ".", + "ignoreDeprecations": "6.0" + }, + "include": [ + "resources/js/v5/**/*.ts", + "resources/js/v5/**/*.tsx" + ], + "references": [] +} diff --git a/vite.config.js b/vite.config.js index 44560bf10..560d4a0bd 100644 --- a/vite.config.js +++ b/vite.config.js @@ -36,7 +36,7 @@ export default defineConfig(({ mode }) => { input: [ "resources/css/app.css", "resources/js/app.js", - "resources/js/v5/app.jsx", + "resources/js/v5/app.tsx", ], refresh: true, }),