From a81e9b18b70084d2dc25852c1bcd6de2ca38f524 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> Date: Tue, 13 May 2025 23:42:22 +0300 Subject: [PATCH] dashboard layout (#597) --- app/Http/Controllers/ServerController.php | 20 ++ .../Settings/PasswordController.php | 41 ---- .../Settings/ProfileController.php | 24 +- package-lock.json | 144 ++++++++++-- package.json | 4 +- resources/css/app.css | 28 +++ resources/js/components/app-content.tsx | 18 -- resources/js/components/app-header.tsx | 218 ++---------------- resources/js/components/app-shell.tsx | 18 -- .../js/components/app-sidebar-header.tsx | 14 -- resources/js/components/app-sidebar.tsx | 128 ++++++++-- resources/js/components/breadcrumbs.tsx | 34 --- resources/js/components/container.tsx | 5 +- resources/js/components/heading.tsx | 2 +- resources/js/components/nav-footer.tsx | 31 --- resources/js/components/nav-main.tsx | 24 -- resources/js/components/nav-user.tsx | 13 +- resources/js/components/project-switch.tsx | 4 +- resources/js/components/server-switch.tsx | 8 +- resources/js/components/ui/button.tsx | 11 +- .../js/components/{ => ui}/input-error.tsx | 0 resources/js/components/ui/input.tsx | 2 +- resources/js/components/ui/separator.tsx | 2 + resources/js/components/ui/sheet.tsx | 8 +- resources/js/components/ui/skeleton.tsx | 2 +- resources/js/components/ui/tooltip.tsx | 2 + resources/js/components/user-info.tsx | 2 +- resources/js/components/user-menu-content.tsx | 2 +- resources/js/hooks/use-mobile.ts | 19 ++ resources/js/layouts/app-layout.tsx | 4 +- .../js/layouts/app/app-header-layout.tsx | 19 -- .../js/layouts/app/app-sidebar-layout.tsx | 18 -- resources/js/layouts/app/layout.tsx | 35 +++ resources/js/layouts/server/layout.tsx | 102 ++++++++ resources/js/layouts/settings/layout.tsx | 56 +---- resources/js/pages/auth/confirm-password.tsx | 2 +- resources/js/pages/auth/forgot-password.tsx | 2 +- resources/js/pages/auth/login.tsx | 2 +- resources/js/pages/auth/reset-password.tsx | 2 +- .../server-logs/{ => partials}/columns.tsx | 2 +- .../{ => partials}/create-server-provider.tsx | 2 +- resources/js/pages/servers/index.tsx | 14 +- resources/js/pages/servers/installing.tsx | 15 +- resources/js/pages/servers/overview.tsx | 21 ++ .../js/pages/servers/partials/actions.tsx | 27 +++ .../pages/servers/{ => partials}/columns.tsx | 4 +- .../servers/{ => partials}/create-server.tsx | 4 +- .../pages/servers/partials/delete-server.tsx | 63 +++++ .../js/pages/servers/partials/header.tsx | 73 ++++++ .../js/pages/servers/partials/status.tsx | 6 + resources/js/pages/servers/show.tsx | 9 +- resources/js/pages/settings/password.tsx | 128 ---------- resources/js/pages/settings/profile.tsx | 129 ----------- resources/js/pages/settings/profile/index.tsx | 19 ++ .../profile/partials}/delete-user.tsx | 42 ++-- .../profile/partials/update-password.tsx | 116 ++++++++++ .../settings/profile/partials/update-user.tsx | 110 +++++++++ 57 files changed, 1011 insertions(+), 843 deletions(-) delete mode 100644 app/Http/Controllers/Settings/PasswordController.php delete mode 100644 resources/js/components/app-content.tsx delete mode 100644 resources/js/components/app-shell.tsx delete mode 100644 resources/js/components/app-sidebar-header.tsx delete mode 100644 resources/js/components/breadcrumbs.tsx delete mode 100644 resources/js/components/nav-footer.tsx delete mode 100644 resources/js/components/nav-main.tsx rename resources/js/components/{ => ui}/input-error.tsx (100%) create mode 100644 resources/js/hooks/use-mobile.ts delete mode 100644 resources/js/layouts/app/app-header-layout.tsx delete mode 100644 resources/js/layouts/app/app-sidebar-layout.tsx create mode 100644 resources/js/layouts/app/layout.tsx create mode 100644 resources/js/layouts/server/layout.tsx rename resources/js/pages/server-logs/{ => partials}/columns.tsx (93%) rename resources/js/pages/server-providers/{ => partials}/create-server-provider.tsx (98%) create mode 100644 resources/js/pages/servers/overview.tsx create mode 100644 resources/js/pages/servers/partials/actions.tsx rename resources/js/pages/servers/{ => partials}/columns.tsx (89%) rename resources/js/pages/servers/{ => partials}/create-server.tsx (98%) create mode 100644 resources/js/pages/servers/partials/delete-server.tsx create mode 100644 resources/js/pages/servers/partials/header.tsx create mode 100644 resources/js/pages/servers/partials/status.tsx delete mode 100644 resources/js/pages/settings/password.tsx delete mode 100644 resources/js/pages/settings/profile.tsx create mode 100644 resources/js/pages/settings/profile/index.tsx rename resources/js/{components => pages/settings/profile/partials}/delete-user.tsx (72%) create mode 100644 resources/js/pages/settings/profile/partials/update-password.tsx create mode 100644 resources/js/pages/settings/profile/partials/update-user.tsx diff --git a/app/Http/Controllers/ServerController.php b/app/Http/Controllers/ServerController.php index ade783c0..2635c403 100644 --- a/app/Http/Controllers/ServerController.php +++ b/app/Http/Controllers/ServerController.php @@ -10,8 +10,10 @@ use App\Models\ServerProvider; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Validation\Rule; use Inertia\Response; use Inertia\ResponseFactory; +use Spatie\RouteAttributes\Attributes\Delete; use Spatie\RouteAttributes\Attributes\Get; use Spatie\RouteAttributes\Attributes\Middleware; use Spatie\RouteAttributes\Attributes\Post; @@ -67,4 +69,22 @@ public function switch(Server $server): RedirectResponse return redirect()->route('servers.show', ['server' => $server->id]); } + + #[Delete('/{server}', name: 'servers.destroy')] + public function destroy(Server $server, Request $request): RedirectResponse + { + $this->authorize('delete', $server); + + $this->validate($request, [ + 'name' => [ + 'required', + Rule::in([$server->name]), + ], + ]); + + $server->delete(); + + return redirect()->route('servers') + ->with('success', __('Server deleted successfully.')); + } } diff --git a/app/Http/Controllers/Settings/PasswordController.php b/app/Http/Controllers/Settings/PasswordController.php deleted file mode 100644 index 909b267a..00000000 --- a/app/Http/Controllers/Settings/PasswordController.php +++ /dev/null @@ -1,41 +0,0 @@ -validate([ - 'current_password' => ['required', 'current_password'], - 'password' => ['required', Password::defaults(), 'confirmed'], - ]); - - $request->user()->update([ - 'password' => Hash::make($validated['password']), - ]); - - return back(); - } -} diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php index 09d78d22..27fcca14 100644 --- a/app/Http/Controllers/Settings/ProfileController.php +++ b/app/Http/Controllers/Settings/ProfileController.php @@ -8,7 +8,9 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\Password; use Inertia\Inertia; use Inertia\Response; use Spatie\RouteAttributes\Attributes\Delete; @@ -16,15 +18,16 @@ use Spatie\RouteAttributes\Attributes\Middleware; use Spatie\RouteAttributes\Attributes\Patch; use Spatie\RouteAttributes\Attributes\Prefix; +use Spatie\RouteAttributes\Attributes\Put; #[Prefix('settings/profile')] #[Middleware(['auth'])] class ProfileController extends Controller { - #[Get('/', name: 'profile.edit')] + #[Get('/', name: 'profile')] public function edit(Request $request): Response { - return Inertia::render('settings/profile', [ + return Inertia::render('settings/profile/index', [ 'mustVerifyEmail' => $request->user() instanceof MustVerifyEmail, 'status' => $request->session()->get('status'), ]); @@ -49,7 +52,22 @@ public function update(Request $request): RedirectResponse $request->user()->save(); - return to_route('profile.edit'); + return to_route('profile'); + } + + #[Put('/', name: 'profile.password')] + public function password(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back(); } #[Delete('/', name: 'profile.destroy')] diff --git a/package-lock.json b/package-lock.json index 1745e621..4f2b71b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,14 +11,14 @@ "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-progress": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.8", "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-separator": "^1.1.6", "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", @@ -1372,22 +1372,22 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.11.tgz", - "integrity": "sha512-yI7S1ipkP5/+99qhSI6nthfo/tR6bL6Zgxi/+1UO6qPa6UeM6nlafWcQ65vB4rU2XjgjMfMhI3k9Y5MztA62VQ==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz", + "integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.2", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-dismissable-layer": "1.1.9", "@radix-ui/react-focus-guards": "1.1.2", - "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-focus-scope": "1.1.6", "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-portal": "1.1.8", "@radix-ui/react-presence": "1.1.4", - "@radix-ui/react-primitive": "2.1.0", - "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-slot": "1.2.2", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" @@ -1407,21 +1407,102 @@ } } }, - "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", - "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.9.tgz", + "integrity": "sha512-way197PiTvNp+WBP7svMJasHl+vibhWGQDb6Mgf5mhEWJkgb85z7Lfl9TUdkqpWsf8GRNmoopx9ZxCyDzmgRMQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@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 + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.6.tgz", + "integrity": "sha512-r9zpYNUQY+2jWHWZGyddQLL9YHkM/XvSFHVcWs7bdVuxMAnCwTAuy6Pf47Z4nw7dYcUou1vg/VgjjrrH03VeBw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.8.tgz", + "integrity": "sha512-hQsTUIn7p7fxCPvao/q6wpbxmCwgLrlz+nOrJgC+RwfZqWY/WN+UMqkXzrtKbPrF82P43eCTl3ekeKuyAQbFeg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.2" + }, + "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 } } }, @@ -1986,12 +2067,35 @@ } }, "node_modules/@radix-ui/react-separator": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.4.tgz", - "integrity": "sha512-2fTm6PSiUm8YPq9W0E4reYuv01EE3aFSzt8edBiXqPHshF8N9+Kymt/k0/R+F3dkY5lQyB/zPtrP82phskLi7w==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.6.tgz", + "integrity": "sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.1.0" + "@radix-ui/react-primitive": "2.1.2" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz", + "integrity": "sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.2" }, "peerDependencies": { "@types/react": "*", diff --git a/package.json b/package.json index 0688dbe8..f16de223 100644 --- a/package.json +++ b/package.json @@ -32,14 +32,14 @@ "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.4", "@radix-ui/react-navigation-menu": "^1.2.5", "@radix-ui/react-progress": "^1.1.6", "@radix-ui/react-scroll-area": "^1.2.8", "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-separator": "^1.1.6", "@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", diff --git a/resources/css/app.css b/resources/css/app.css index c6f4dc57..0ae51a02 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -198,3 +198,31 @@ @layer base { @apply bg-background text-foreground; } } + +/* + ---break--- +*/ + +@theme inline { + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +/* + ---break--- +*/ + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/resources/js/components/app-content.tsx b/resources/js/components/app-content.tsx deleted file mode 100644 index 6b530df4..00000000 --- a/resources/js/components/app-content.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { SidebarInset } from '@/components/ui/sidebar'; -import * as React from 'react'; - -interface AppContentProps extends React.ComponentProps<'main'> { - variant?: 'header' | 'sidebar'; -} - -export function AppContent({ variant = 'header', children, ...props }: AppContentProps) { - if (variant === 'sidebar') { - return {children}; - } - - return ( -
- {children} -
- ); -} diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx index 011b0b39..d4b3b601 100644 --- a/resources/js/components/app-header.tsx +++ b/resources/js/components/app-header.tsx @@ -1,204 +1,26 @@ -import { Icon } from '@/components/icon'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { Button } from '@/components/ui/button'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { NavigationMenu, NavigationMenuItem, NavigationMenuList, navigationMenuTriggerStyle } from '@/components/ui/navigation-menu'; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; -import { UserMenuContent } from '@/components/user-menu-content'; -import { useInitials } from '@/hooks/use-initials'; -import { cn } from '@/lib/utils'; -import { type NavItem, type SharedData } from '@/types'; -import { Link, usePage } from '@inertiajs/react'; -import { BookOpen, CogIcon, Folder, Menu, Search, ServerIcon, SlashIcon } from 'lucide-react'; -import AppLogo from './app-logo'; -import AppLogoIcon from './app-logo-icon'; -import { ProjectSwitch } from '@/components/project-switch'; -import { ServerSwitch } from '@/components/server-switch'; +import { SidebarTrigger } from '@/components/ui/sidebar'; import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbSeparator } from '@/components/ui/breadcrumb'; - -const mainNavItems: NavItem[] = [ - { - title: 'Servers', - href: '/servers', - activePath: '/servers', - icon: ServerIcon, - }, - { - title: 'Settings', - href: '/settings/profile', - activePath: '/settings', - icon: CogIcon, - }, -]; - -const rightNavItems: NavItem[] = [ - { - title: 'Repository', - href: 'https://github.com/vitodeploy/vito', - icon: Folder, - }, - { - title: 'Documentation', - href: 'https://vitodeploy.com', - icon: BookOpen, - }, -]; - -const activeItemStyles = ''; +import { ProjectSwitch } from '@/components/project-switch'; +import { SlashIcon } from 'lucide-react'; +import { ServerSwitch } from '@/components/server-switch'; export function AppHeader() { - const page = usePage(); - const { auth } = page.props; - const getInitials = useInitials(); return ( - <> -
-
- {/* Mobile Menu */} -
- - - - - - Navigation Menu - - - -
-
-
- {mainNavItems.map((item) => ( - - {item.icon && } - {item.title} - - ))} -
- -
- {rightNavItems.map((item) => ( - - {item.icon && } - {item.title} - - ))} -
-
-
-
-
-
- - - - - - {/* Desktop Navigation */} -
- - - - - - - - - - - - - -
- -
-
- -
- {rightNavItems.map((item) => ( - - - - - {item.title} - {item.icon && } - - - -

{item.title}

-
-
-
- ))} -
-
- - - - - - - - -
-
- {/* Desktop Navigation */} -
- - - {mainNavItems.map((item, index) => ( - - - {item.icon && } - {item.title} - - {item.activePath && page.url.startsWith(item.activePath) && ( -
- )} -
- ))} -
-
-
-
- {/*{breadcrumbs.length > 1 && (*/} - {/*
*/} - {/* */} - {/* */} - {/*
*/} - {/* */} - {/*)}*/} - +
+ + + + + + + + + + + + + + +
); } diff --git a/resources/js/components/app-shell.tsx b/resources/js/components/app-shell.tsx deleted file mode 100644 index cf92f399..00000000 --- a/resources/js/components/app-shell.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { SidebarProvider } from '@/components/ui/sidebar'; -import { SharedData } from '@/types'; -import { usePage } from '@inertiajs/react'; - -interface AppShellProps { - children: React.ReactNode; - variant?: 'header' | 'sidebar'; -} - -export function AppShell({ children, variant = 'header' }: AppShellProps) { - const isOpen = usePage().props.sidebarOpen; - - if (variant === 'header') { - return
{children}
; - } - - return {children}; -} diff --git a/resources/js/components/app-sidebar-header.tsx b/resources/js/components/app-sidebar-header.tsx deleted file mode 100644 index 9eb78171..00000000 --- a/resources/js/components/app-sidebar-header.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Breadcrumbs } from '@/components/breadcrumbs'; -import { SidebarTrigger } from '@/components/ui/sidebar'; -import { type BreadcrumbItem as BreadcrumbItemType } from '@/types'; - -export function AppSidebarHeader({ breadcrumbs = [] }: { breadcrumbs?: BreadcrumbItemType[] }) { - return ( -
-
- - -
-
- ); -} diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 628f5b73..46f60e91 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -1,11 +1,20 @@ -import { NavFooter } from '@/components/nav-footer'; -import { NavMain } from '@/components/nav-main'; import { NavUser } from '@/components/nav-user'; -import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; +import { + Sidebar, + SidebarContent, + SidebarFooter, + SidebarGroup, + SidebarGroupContent, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from '@/components/ui/sidebar'; import { type NavItem } from '@/types'; import { Link } from '@inertiajs/react'; -import { BookOpen, Folder, ServerIcon } from 'lucide-react'; +import { BookOpen, CogIcon, Folder, ServerIcon } from 'lucide-react'; import AppLogo from './app-logo'; +import { Icon } from '@/components/icon'; const mainNavItems: NavItem[] = [ { @@ -13,6 +22,11 @@ const mainNavItems: NavItem[] = [ href: route('servers'), icon: ServerIcon, }, + { + title: 'Settings', + href: route('profile'), + icon: CogIcon, + }, ]; const footerNavItems: NavItem[] = [ @@ -28,29 +42,93 @@ const footerNavItems: NavItem[] = [ }, ]; -export function AppSidebar() { +export function AppSidebar({ secondNavItems, secondNavTitle }: { secondNavItems?: NavItem[]; secondNavTitle?: string }) { return ( - - - - - - - - - - - - + + {/* This is the first sidebar */} + {/* We disable collapsible and adjust width to icon. */} + {/* This will make the sidebar appear as icons. */} + + + + + + + + + + + + + + + + + {mainNavItems.map((item) => ( + + + + {item.icon && } + {item.title} + + + + ))} + + + + + + + {footerNavItems.map((item) => ( + + + + {item.icon && } + {item.title} + + + + ))} + + + + - - - - - - - - + {/* This is the second sidebar */} + {/* We enable collapsible and adjust width to icon. */} + {/* This will make the sidebar appear as icons. */} + {secondNavItems && secondNavItems.length > 0 && ( + + +
+ {secondNavTitle} +
+
+ + + + + {secondNavItems.map((item) => ( + + + + {item.icon && } + {item.title} + + + + ))} + + + + +
+ )}
); } diff --git a/resources/js/components/breadcrumbs.tsx b/resources/js/components/breadcrumbs.tsx deleted file mode 100644 index 675c7936..00000000 --- a/resources/js/components/breadcrumbs.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from '@/components/ui/breadcrumb'; -import { type BreadcrumbItem as BreadcrumbItemType } from '@/types'; -import { Link } from '@inertiajs/react'; -import { Fragment } from 'react'; - -export function Breadcrumbs({ breadcrumbs }: { breadcrumbs: BreadcrumbItemType[] }) { - return ( - <> - {breadcrumbs.length > 0 && ( - - - {breadcrumbs.map((item, index) => { - const isLast = index === breadcrumbs.length - 1; - return ( - - - {isLast ? ( - {item.title} - ) : ( - - {item.title} - - )} - - {!isLast && } - - ); - })} - - - )} - - ); -} diff --git a/resources/js/components/container.tsx b/resources/js/components/container.tsx index 1675a4f1..792266ce 100644 --- a/resources/js/components/container.tsx +++ b/resources/js/components/container.tsx @@ -1,5 +1,6 @@ import { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; -export default function Container({ children }: { children?: ReactNode }) { - return
{children}
; +export default function Container({ className, children }: { className?: string; children?: ReactNode }) { + return
{children}
; } diff --git a/resources/js/components/heading.tsx b/resources/js/components/heading.tsx index 6a4586be..5cb497e1 100644 --- a/resources/js/components/heading.tsx +++ b/resources/js/components/heading.tsx @@ -1,6 +1,6 @@ export default function Heading({ title, description }: { title: string; description?: string }) { return ( -
+

{title}

{description &&

{description}

}
diff --git a/resources/js/components/nav-footer.tsx b/resources/js/components/nav-footer.tsx deleted file mode 100644 index 33fed147..00000000 --- a/resources/js/components/nav-footer.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Icon } from '@/components/icon'; -import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; -import { type NavItem } from '@/types'; -import { type ComponentPropsWithoutRef } from 'react'; - -export function NavFooter({ - items, - className, - ...props -}: ComponentPropsWithoutRef & { - items: NavItem[]; -}) { - return ( - - - - {items.map((item) => ( - - - - {item.icon && } - {item.title} - - - - ))} - - - - ); -} diff --git a/resources/js/components/nav-main.tsx b/resources/js/components/nav-main.tsx deleted file mode 100644 index 092d8bd3..00000000 --- a/resources/js/components/nav-main.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; -import { type NavItem } from '@/types'; -import { Link, usePage } from '@inertiajs/react'; - -export function NavMain({ items = [] }: { items: NavItem[] }) { - const page = usePage(); - return ( - - Platform - - {items.map((item) => ( - - - - {item.icon && } - {item.title} - - - - ))} - - - ); -} diff --git a/resources/js/components/nav-user.tsx b/resources/js/components/nav-user.tsx index d4641f41..18eb3059 100644 --- a/resources/js/components/nav-user.tsx +++ b/resources/js/components/nav-user.tsx @@ -1,5 +1,5 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; -import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'; +import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; import { UserInfo } from '@/components/user-info'; import { UserMenuContent } from '@/components/user-menu-content'; import { useIsMobile } from '@/hooks/use-mobile'; @@ -9,7 +9,6 @@ import { ChevronsUpDown } from 'lucide-react'; export function NavUser() { const { auth } = usePage().props; - const { state } = useSidebar(); const isMobile = useIsMobile(); return ( @@ -17,15 +16,19 @@ export function NavUser() { - + diff --git a/resources/js/components/project-switch.tsx b/resources/js/components/project-switch.tsx index 6af2b86c..4b3cc9a5 100644 --- a/resources/js/components/project-switch.tsx +++ b/resources/js/components/project-switch.tsx @@ -33,8 +33,8 @@ export function ProjectSwitch() {
diff --git a/resources/js/components/server-switch.tsx b/resources/js/components/server-switch.tsx index a4886406..770381eb 100644 --- a/resources/js/components/server-switch.tsx +++ b/resources/js/components/server-switch.tsx @@ -31,8 +31,8 @@ export function ServerSwitch() { {selectedServer && ( @@ -41,8 +41,8 @@ export function ServerSwitch() { {!selectedServer && ( diff --git a/resources/js/components/ui/button.tsx b/resources/js/components/ui/button.tsx index 3c605828..29bc82f1 100644 --- a/resources/js/components/ui/button.tsx +++ b/resources/js/components/ui/button.tsx @@ -5,21 +5,22 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils'; const buttonVariants = cva( - "cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', destructive: - 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', - outline: 'border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground', + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', + ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', link: 'text-primary underline-offset-4 hover:underline', }, size: { default: 'h-9 px-4 py-2 has-[>svg]:px-3', - sm: 'h-8 rounded-md px-3 has-[>svg]:px-2.5', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', icon: 'size-9', }, diff --git a/resources/js/components/input-error.tsx b/resources/js/components/ui/input-error.tsx similarity index 100% rename from resources/js/components/input-error.tsx rename to resources/js/components/ui/input-error.tsx diff --git a/resources/js/components/ui/input.tsx b/resources/js/components/ui/input.tsx index 724de857..3c1cfcaf 100644 --- a/resources/js/components/ui/input.tsx +++ b/resources/js/components/ui/input.tsx @@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) { type={type} data-slot="input" className={cn( - 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-background border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', + 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]', 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', className, diff --git a/resources/js/components/ui/separator.tsx b/resources/js/components/ui/separator.tsx index fb8eb4fc..2d953010 100644 --- a/resources/js/components/ui/separator.tsx +++ b/resources/js/components/ui/separator.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as React from 'react'; import * as SeparatorPrimitive from '@radix-ui/react-separator'; diff --git a/resources/js/components/ui/sheet.tsx b/resources/js/components/ui/sheet.tsx index a02f879c..46fbc062 100644 --- a/resources/js/components/ui/sheet.tsx +++ b/resources/js/components/ui/sheet.tsx @@ -25,7 +25,7 @@ function SheetOverlay({ className, ...props }: React.ComponentProps) { - return
; + return
; } export { Skeleton }; diff --git a/resources/js/components/ui/tooltip.tsx b/resources/js/components/ui/tooltip.tsx index e72aca08..942fa16c 100644 --- a/resources/js/components/ui/tooltip.tsx +++ b/resources/js/components/ui/tooltip.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as React from 'react'; import * as TooltipPrimitive from '@radix-ui/react-tooltip'; diff --git a/resources/js/components/user-info.tsx b/resources/js/components/user-info.tsx index 7b1947bc..f3645461 100644 --- a/resources/js/components/user-info.tsx +++ b/resources/js/components/user-info.tsx @@ -7,7 +7,7 @@ export function UserInfo({ user, showEmail = false }: { user: User; showEmail?: return ( <> - + {getInitials(user.name)} diff --git a/resources/js/components/user-menu-content.tsx b/resources/js/components/user-menu-content.tsx index 78dce392..d7db0476 100644 --- a/resources/js/components/user-menu-content.tsx +++ b/resources/js/components/user-menu-content.tsx @@ -30,7 +30,7 @@ export function UserMenuContent({ user }: UserMenuContentProps) { - + Settings diff --git a/resources/js/hooks/use-mobile.ts b/resources/js/hooks/use-mobile.ts new file mode 100644 index 00000000..ba553c6f --- /dev/null +++ b/resources/js/hooks/use-mobile.ts @@ -0,0 +1,19 @@ +import * as React from 'react'; + +const MOBILE_BREAKPOINT = 768; + +export function useIsMobile() { + const [isMobile, setIsMobile] = React.useState(undefined); + + React.useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const onChange = () => { + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + }; + mql.addEventListener('change', onChange); + setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); + return () => mql.removeEventListener('change', onChange); + }, []); + + return !!isMobile; +} diff --git a/resources/js/layouts/app-layout.tsx b/resources/js/layouts/app-layout.tsx index ef24f5df..d1940576 100644 --- a/resources/js/layouts/app-layout.tsx +++ b/resources/js/layouts/app-layout.tsx @@ -1,4 +1,4 @@ -import AppLayoutTemplate from '@/layouts/app/app-header-layout'; +import AppLayout from '@/layouts/app/layout'; import { type BreadcrumbItem } from '@/types'; import { type ReactNode } from 'react'; @@ -7,4 +7,4 @@ interface AppLayoutProps { breadcrumbs?: BreadcrumbItem[]; } -export default ({ children, ...props }: AppLayoutProps) => {children}; +export default ({ children, ...props }: AppLayoutProps) => {children}; diff --git a/resources/js/layouts/app/app-header-layout.tsx b/resources/js/layouts/app/app-header-layout.tsx deleted file mode 100644 index 05644883..00000000 --- a/resources/js/layouts/app/app-header-layout.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { AppContent } from '@/components/app-content'; -import { AppHeader } from '@/components/app-header'; -import { AppShell } from '@/components/app-shell'; -import { type BreadcrumbItem } from '@/types'; -import type { PropsWithChildren } from 'react'; -import { usePoll } from '@inertiajs/react'; -import { Toaster } from '@/components/ui/sonner'; - -export default function AppHeaderLayout({ children }: PropsWithChildren<{ breadcrumbs?: BreadcrumbItem[] }>) { - usePoll(10000); - - return ( - - - {children} - - - ); -} diff --git a/resources/js/layouts/app/app-sidebar-layout.tsx b/resources/js/layouts/app/app-sidebar-layout.tsx deleted file mode 100644 index f2df3540..00000000 --- a/resources/js/layouts/app/app-sidebar-layout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { AppContent } from '@/components/app-content'; -import { AppShell } from '@/components/app-shell'; -import { AppSidebar } from '@/components/app-sidebar'; -import { AppSidebarHeader } from '@/components/app-sidebar-header'; -import { type BreadcrumbItem } from '@/types'; -import { type PropsWithChildren } from 'react'; - -export default function AppSidebarLayout({ children, breadcrumbs = [] }: PropsWithChildren<{ breadcrumbs?: BreadcrumbItem[] }>) { - return ( - - - - - {children} - - - ); -} diff --git a/resources/js/layouts/app/layout.tsx b/resources/js/layouts/app/layout.tsx new file mode 100644 index 00000000..e5d11a1b --- /dev/null +++ b/resources/js/layouts/app/layout.tsx @@ -0,0 +1,35 @@ +import { AppSidebar } from '@/components/app-sidebar'; +import { AppHeader } from '@/components/app-header'; +import { type BreadcrumbItem, NavItem } from '@/types'; +import { CSSProperties, type PropsWithChildren } from 'react'; +import { SidebarInset, SidebarProvider } from '@/components/ui/sidebar'; +import { usePoll } from '@inertiajs/react'; + +export default function Layout({ + children, + secondNavItems, + secondNavTitle, +}: PropsWithChildren<{ + breadcrumbs?: BreadcrumbItem[]; + secondNavItems?: NavItem[]; + secondNavTitle?: string; +}>) { + usePoll(10000); + + return ( + 0)} + > + + + +
{children}
+
+
+ ); +} diff --git a/resources/js/layouts/server/layout.tsx b/resources/js/layouts/server/layout.tsx new file mode 100644 index 00000000..3a9d6842 --- /dev/null +++ b/resources/js/layouts/server/layout.tsx @@ -0,0 +1,102 @@ +import { type NavItem } from '@/types'; +import { + ChartPieIcon, + ClockIcon, + CogIcon, + DatabaseIcon, + FlameIcon, + FolderOpenIcon, + HomeIcon, + KeyIcon, + ListEndIcon, + LogsIcon, + MousePointerClickIcon, + Settings2Icon, + TerminalSquareIcon, +} from 'lucide-react'; +import { ReactNode } from 'react'; +import { Server } from '@/types/server'; +import ServerHeader from '@/pages/servers/partials/header'; +import Layout from '@/layouts/app/layout'; + +export default function ServerLayout({ server, children }: { server: Server; children: ReactNode }) { + // When server-side rendering, we only render the layout on the client... + if (typeof window === 'undefined') { + return null; + } + const sidebarNavItems: NavItem[] = [ + { + title: 'Overview', + href: route('servers.show', { server: server.id }), + icon: HomeIcon, + }, + { + title: 'Databases', + href: '#', + icon: DatabaseIcon, + }, + { + title: 'Sites', + href: '#', + icon: MousePointerClickIcon, + }, + { + title: 'File Manager', + href: '#', + icon: FolderOpenIcon, + }, + { + title: 'Firewall', + href: '#', + icon: FlameIcon, + }, + { + title: 'CronJobs', + href: '#', + icon: ClockIcon, + }, + { + title: 'Workers', + href: '#', + icon: ListEndIcon, + }, + { + title: 'SSH Keys', + href: '#', + icon: KeyIcon, + }, + { + title: 'Services', + href: '#', + icon: CogIcon, + }, + { + title: 'Metrics', + href: '#', + icon: ChartPieIcon, + }, + { + title: 'Console', + href: '#', + icon: TerminalSquareIcon, + }, + { + title: 'Logs', + href: '#', + icon: LogsIcon, + }, + { + title: 'Settings', + href: '#', + icon: Settings2Icon, + }, + ]; + + return ( + + + +
{children}
+
+ ); +} diff --git a/resources/js/layouts/settings/layout.tsx b/resources/js/layouts/settings/layout.tsx index f568cdb1..f61928dd 100644 --- a/resources/js/layouts/settings/layout.tsx +++ b/resources/js/layouts/settings/layout.tsx @@ -1,16 +1,12 @@ -import Container from '@/components/container'; -import { Button } from '@/components/ui/button'; -import { Separator } from '@/components/ui/separator'; -import { cn } from '@/lib/utils'; -import { type NavItem } from '@/types'; -import { Link } from '@inertiajs/react'; -import { ListIcon, LockIcon, UserIcon } from 'lucide-react'; -import { type PropsWithChildren } from 'react'; +import { type BreadcrumbItem, type NavItem } from '@/types'; +import { ListIcon, UserIcon } from 'lucide-react'; +import { ReactNode } from 'react'; +import Layout from '@/layouts/app/layout'; const sidebarNavItems: NavItem[] = [ { title: 'Profile', - href: '/settings/profile', + href: route('profile'), icon: UserIcon, }, { @@ -18,51 +14,17 @@ const sidebarNavItems: NavItem[] = [ href: '/', icon: ListIcon, }, - { - title: 'Password', - href: '/settings/password', - icon: LockIcon, - }, ]; -export default function SettingsLayout({ children }: PropsWithChildren) { +export default function SettingsLayout({ children, breadcrumbs }: { children: ReactNode; breadcrumbs?: BreadcrumbItem[] }) { // When server-side rendering, we only render the layout on the client... if (typeof window === 'undefined') { return null; } - const currentPath = window.location.pathname; - return ( - -
- - - - -
-
{children}
-
-
-
+ + {children} + ); } diff --git a/resources/js/pages/auth/confirm-password.tsx b/resources/js/pages/auth/confirm-password.tsx index f55e1f28..62fbe0f1 100644 --- a/resources/js/pages/auth/confirm-password.tsx +++ b/resources/js/pages/auth/confirm-password.tsx @@ -3,7 +3,7 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; +import InputError from '@/components/ui/input-error'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; diff --git a/resources/js/pages/auth/forgot-password.tsx b/resources/js/pages/auth/forgot-password.tsx index 2adae3fe..f7d4d8d0 100644 --- a/resources/js/pages/auth/forgot-password.tsx +++ b/resources/js/pages/auth/forgot-password.tsx @@ -3,7 +3,7 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; +import InputError from '@/components/ui/input-error'; import TextLink from '@/components/text-link'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx index f1da1e46..e7a22e70 100644 --- a/resources/js/pages/auth/login.tsx +++ b/resources/js/pages/auth/login.tsx @@ -2,7 +2,7 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; +import InputError from '@/components/ui/input-error'; import TextLink from '@/components/text-link'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; diff --git a/resources/js/pages/auth/reset-password.tsx b/resources/js/pages/auth/reset-password.tsx index a4d4d4b4..54e0ae80 100644 --- a/resources/js/pages/auth/reset-password.tsx +++ b/resources/js/pages/auth/reset-password.tsx @@ -2,7 +2,7 @@ import { Head, useForm } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; import { FormEventHandler } from 'react'; -import InputError from '@/components/input-error'; +import InputError from '@/components/ui/input-error'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; diff --git a/resources/js/pages/server-logs/columns.tsx b/resources/js/pages/server-logs/partials/columns.tsx similarity index 93% rename from resources/js/pages/server-logs/columns.tsx rename to resources/js/pages/server-logs/partials/columns.tsx index 2ffc1e3f..3be139fe 100644 --- a/resources/js/pages/server-logs/columns.tsx +++ b/resources/js/pages/server-logs/partials/columns.tsx @@ -45,7 +45,7 @@ const LogActionCell = ({ row }: { row: Row }) => { View Log This is all content of the log - + {content} diff --git a/resources/js/pages/server-providers/create-server-provider.tsx b/resources/js/pages/server-providers/partials/create-server-provider.tsx similarity index 98% rename from resources/js/pages/server-providers/create-server-provider.tsx rename to resources/js/pages/server-providers/partials/create-server-provider.tsx index 609e2604..cc9ecdd1 100644 --- a/resources/js/pages/server-providers/create-server-provider.tsx +++ b/resources/js/pages/server-providers/partials/create-server-provider.tsx @@ -14,7 +14,7 @@ import { useForm, usePage } from '@inertiajs/react'; import { FormEventHandler, useEffect, useState } from 'react'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import InputError from '@/components/input-error'; +import InputError from '@/components/ui/input-error'; import { Form, FormField, FormFields } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { SharedData } from '@/types'; diff --git a/resources/js/pages/servers/index.tsx b/resources/js/pages/servers/index.tsx index c5fbba6e..53ceccf8 100644 --- a/resources/js/pages/servers/index.tsx +++ b/resources/js/pages/servers/index.tsx @@ -2,16 +2,16 @@ import { Head, usePage } from '@inertiajs/react'; import { type Configs } from '@/types'; -import AppLayout from '@/layouts/app-layout'; import { DataTable } from '@/components/data-table'; -import { columns } from '@/pages/servers/columns'; +import { columns } from '@/pages/servers/partials/columns'; import { Server } from '@/types/server'; import Heading from '@/components/heading'; -import CreateServer from '@/pages/servers/create-server'; +import CreateServer from '@/pages/servers/partials/create-server'; import Container from '@/components/container'; import { PlusIcon } from 'lucide-react'; import { Button } from '@/components/ui/button'; import React from 'react'; +import Layout from '@/layouts/app/layout'; type Response = { servers: { @@ -24,12 +24,12 @@ type Response = { export default function Servers() { const page = usePage(); return ( - + - +
- +
- + ); } diff --git a/resources/js/pages/servers/installing.tsx b/resources/js/pages/servers/installing.tsx index a2211eab..3407123d 100644 --- a/resources/js/pages/servers/installing.tsx +++ b/resources/js/pages/servers/installing.tsx @@ -1,12 +1,9 @@ import type { Server } from '@/types/server'; import type { ServerLog } from '@/types/server-log'; import Container from '@/components/container'; -import Heading from '@/components/heading'; -import { Progress } from '@/components/ui/progress'; import { DataTable } from '@/components/data-table'; -import { columns } from '@/pages/server-logs/columns'; +import { columns } from '@/pages/server-logs/partials/columns'; import { usePage } from '@inertiajs/react'; -import { Button } from '@/components/ui/button'; export default function InstallingServer() { const page = usePage<{ @@ -17,14 +14,8 @@ export default function InstallingServer() { }>(); return ( - -
- - {page.props.server.status === 'installation_failed' && } -
- -
{page.props.server.progress}%
- + + {' '} ); } diff --git a/resources/js/pages/servers/overview.tsx b/resources/js/pages/servers/overview.tsx new file mode 100644 index 00000000..47e238f9 --- /dev/null +++ b/resources/js/pages/servers/overview.tsx @@ -0,0 +1,21 @@ +import type { Server } from '@/types/server'; +import type { ServerLog } from '@/types/server-log'; +import { DataTable } from '@/components/data-table'; +import { columns } from '@/pages/server-logs/partials/columns'; +import { usePage } from '@inertiajs/react'; +import Container from '@/components/container'; + +export default function ServerOverview() { + const page = usePage<{ + server: Server; + logs: { + data: ServerLog[]; + }; + }>(); + + return ( + + + + ); +} diff --git a/resources/js/pages/servers/partials/actions.tsx b/resources/js/pages/servers/partials/actions.tsx new file mode 100644 index 00000000..8d4e4bef --- /dev/null +++ b/resources/js/pages/servers/partials/actions.tsx @@ -0,0 +1,27 @@ +import { Server } from '@/types/server'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { MoreVerticalIcon } from 'lucide-react'; +import DeleteServer from '@/pages/servers/partials/delete-server'; + +export default function ServerActions({ server }: { server: Server }) { + return ( + + + + + + Copy payment ID + + + e.preventDefault()} variant="destructive"> + Delete Server + + + + + ); +} diff --git a/resources/js/pages/servers/columns.tsx b/resources/js/pages/servers/partials/columns.tsx similarity index 89% rename from resources/js/pages/servers/columns.tsx rename to resources/js/pages/servers/partials/columns.tsx index 24b19ea3..d11643cc 100644 --- a/resources/js/pages/servers/columns.tsx +++ b/resources/js/pages/servers/partials/columns.tsx @@ -2,10 +2,10 @@ import { ColumnDef } from '@tanstack/react-table'; import { Server } from '@/types/server'; -import { Badge } from '@/components/ui/badge'; import { Link } from '@inertiajs/react'; import { Button } from '@/components/ui/button'; import { EyeIcon } from 'lucide-react'; +import ServerStatus from '@/pages/servers/partials/status'; export const columns: ColumnDef[] = [ { @@ -33,7 +33,7 @@ export const columns: ColumnDef[] = [ enableColumnFilter: true, enableSorting: true, cell: ({ row }) => { - return {row.original.status}; + return ; }, }, { diff --git a/resources/js/pages/servers/create-server.tsx b/resources/js/pages/servers/partials/create-server.tsx similarity index 98% rename from resources/js/pages/servers/create-server.tsx rename to resources/js/pages/servers/partials/create-server.tsx index 4d056206..910d99e3 100644 --- a/resources/js/pages/servers/create-server.tsx +++ b/resources/js/pages/servers/partials/create-server.tsx @@ -5,11 +5,11 @@ import { useForm, usePage } from '@inertiajs/react'; import React, { FormEventHandler, useState } from 'react'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import InputError from '@/components/input-error'; +import InputError from '@/components/ui/input-error'; import { Input } from '@/components/ui/input'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { ServerProvider } from '@/types/server-provider'; -import CreateServerProvider from '@/pages/server-providers/create-server-provider'; +import CreateServerProvider from '@/pages/server-providers/partials/create-server-provider'; import axios from 'axios'; import { Form, FormField, FormFields } from '@/components/ui/form'; import type { SharedData } from '@/types'; diff --git a/resources/js/pages/servers/partials/delete-server.tsx b/resources/js/pages/servers/partials/delete-server.tsx new file mode 100644 index 00000000..cee23362 --- /dev/null +++ b/resources/js/pages/servers/partials/delete-server.tsx @@ -0,0 +1,63 @@ +import { Server } from '@/types/server'; +import { FormEvent, ReactNode } from 'react'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { useForm } from '@inertiajs/react'; +import { Form, FormField, FormFields } from '@/components/ui/form'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import InputError from '@/components/ui/input-error'; +import { LoaderCircleIcon } from 'lucide-react'; + +export default function DeleteServer({ server, children }: { server: Server; children: ReactNode }) { + const form = useForm({ + name: '', + }); + + const submit = (e: FormEvent) => { + e.preventDefault(); + form.delete(route('servers.destroy', server.id)); + }; + + return ( + + {children} + + + Delete {server.name} + Delete server and its resources. + + +
+ + + + form.setData('name', e.target.value)} /> + + + +
+ + + + + + + + +
+
+ ); +} diff --git a/resources/js/pages/servers/partials/header.tsx b/resources/js/pages/servers/partials/header.tsx new file mode 100644 index 00000000..714327d3 --- /dev/null +++ b/resources/js/pages/servers/partials/header.tsx @@ -0,0 +1,73 @@ +import { Server } from '@/types/server'; +import { CloudIcon, IdCardIcon, LoaderCircleIcon, MapPinIcon, SlashIcon } from 'lucide-react'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import ServerStatus from '@/pages/servers/partials/status'; +import ServerActions from '@/pages/servers/partials/actions'; +import { cn } from '@/lib/utils'; + +export default function ServerHeader({ server }: { server: Server }) { + return ( +
+
+
+ + +
+ +
{server.name}
+
+
+ + {server.name} + Server Name + +
+ + + +
+ +
{server.provider}
+
+
+ + {server.provider} + Server Provider + +
+ + + +
+ +
{server.ip}
+
+
+ + {server.ip} + Server IP + +
+ {['installing', 'installation_failed'].includes(server.status) && ( + <> + + + +
+ +
%{server.progress}
+
+
+ Installation Progress +
+ + )} +
+
+
+ + +
+
+ ); +} diff --git a/resources/js/pages/servers/partials/status.tsx b/resources/js/pages/servers/partials/status.tsx new file mode 100644 index 00000000..2824ad29 --- /dev/null +++ b/resources/js/pages/servers/partials/status.tsx @@ -0,0 +1,6 @@ +import { Server } from '@/types/server'; +import { Badge } from '@/components/ui/badge'; + +export default function ServerStatus({ server }: { server: Server }) { + return {server.status}; +} diff --git a/resources/js/pages/servers/show.tsx b/resources/js/pages/servers/show.tsx index 585c6c52..c97bf967 100644 --- a/resources/js/pages/servers/show.tsx +++ b/resources/js/pages/servers/show.tsx @@ -2,10 +2,11 @@ import { Head, usePage } from '@inertiajs/react'; import { type Configs } from '@/types'; -import AppLayout from '@/layouts/app-layout'; import { type Server } from '@/types/server'; import InstallingServer from '@/pages/servers/installing'; import type { ServerLog } from '@/types/server-log'; +import ServerOverview from '@/pages/servers/overview'; +import ServerLayout from '@/layouts/server/layout'; type Response = { servers: { @@ -22,10 +23,10 @@ type Response = { export default function ShowServer() { const page = usePage(); return ( - + - {['installing', 'installation_failed'].includes(page.props.server.status) && } - + {['installing', 'installation_failed'].includes(page.props.server.status) ? : } + ); } diff --git a/resources/js/pages/settings/password.tsx b/resources/js/pages/settings/password.tsx deleted file mode 100644 index ae2e2287..00000000 --- a/resources/js/pages/settings/password.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import InputError from '@/components/input-error'; -import AppLayout from '@/layouts/app-layout'; -import SettingsLayout from '@/layouts/settings/layout'; -import { type BreadcrumbItem } from '@/types'; -import { Transition } from '@headlessui/react'; -import { Head, useForm } from '@inertiajs/react'; -import { FormEventHandler, useRef } from 'react'; - -import HeadingSmall from '@/components/heading-small'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; - -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Password settings', - href: '/settings/password', - }, -]; - -export default function Password() { - const passwordInput = useRef(null); - const currentPasswordInput = useRef(null); - - const { data, setData, errors, put, reset, processing, recentlySuccessful } = useForm({ - current_password: '', - password: '', - password_confirmation: '', - }); - - const updatePassword: FormEventHandler = (e) => { - e.preventDefault(); - - put(route('password.update'), { - preserveScroll: true, - onSuccess: () => reset(), - onError: (errors) => { - if (errors.password) { - reset('password', 'password_confirmation'); - passwordInput.current?.focus(); - } - - if (errors.current_password) { - reset('current_password'); - currentPasswordInput.current?.focus(); - } - }, - }); - }; - - return ( - - - - -
- - -
-
- - - setData('current_password', e.target.value)} - type="password" - className="mt-1 block w-full" - autoComplete="current-password" - placeholder="Current password" - /> - - -
- -
- - - setData('password', e.target.value)} - type="password" - className="mt-1 block w-full" - autoComplete="new-password" - placeholder="New password" - /> - - -
- -
- - - setData('password_confirmation', e.target.value)} - type="password" - className="mt-1 block w-full" - autoComplete="new-password" - placeholder="Confirm password" - /> - - -
- -
- - - -

Saved

-
-
-
-
-
-
- ); -} diff --git a/resources/js/pages/settings/profile.tsx b/resources/js/pages/settings/profile.tsx deleted file mode 100644 index 81ef3bb7..00000000 --- a/resources/js/pages/settings/profile.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { type BreadcrumbItem, type SharedData } from '@/types'; -import { Transition } from '@headlessui/react'; -import { Head, Link, useForm, usePage } from '@inertiajs/react'; -import { FormEventHandler } from 'react'; - -import DeleteUser from '@/components/delete-user'; -import HeadingSmall from '@/components/heading-small'; -import InputError from '@/components/input-error'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import AppLayout from '@/layouts/app-layout'; -import SettingsLayout from '@/layouts/settings/layout'; - -const breadcrumbs: BreadcrumbItem[] = [ - { - title: 'Home', - href: '/', - }, - { - title: 'Profile settings', - href: '/settings/profile', - }, -]; - -type ProfileForm = { - name: string; - email: string; -}; - -export default function Profile({ mustVerifyEmail, status }: { mustVerifyEmail: boolean; status?: string }) { - const { auth } = usePage().props; - - const { data, setData, patch, errors, processing, recentlySuccessful } = useForm>({ - name: auth.user.name, - email: auth.user.email, - }); - - const submit: FormEventHandler = (e) => { - e.preventDefault(); - - patch(route('profile.update'), { - preserveScroll: true, - }); - }; - - return ( - - - - -
- - -
-
- - - setData('name', e.target.value)} - required - autoComplete="name" - placeholder="Full name" - /> - - -
- -
- - - setData('email', e.target.value)} - required - autoComplete="username" - placeholder="Email address" - /> - - -
- - {mustVerifyEmail && auth.user.email_verified_at === null && ( -
-

- Your email address is unverified.{' '} - - Click here to resend the verification email. - -

- - {status === 'verification-link-sent' && ( -
A new verification link has been sent to your email address.
- )} -
- )} - -
- - - -

Saved

-
-
-
-
- - -
-
- ); -} diff --git a/resources/js/pages/settings/profile/index.tsx b/resources/js/pages/settings/profile/index.tsx new file mode 100644 index 00000000..2a8d470a --- /dev/null +++ b/resources/js/pages/settings/profile/index.tsx @@ -0,0 +1,19 @@ +import { Head } from '@inertiajs/react'; +import DeleteUser from '@/pages/settings/profile/partials/delete-user'; +import SettingsLayout from '@/layouts/settings/layout'; +import Container from '@/components/container'; +import UpdatePassword from '@/pages/settings/profile/partials/update-password'; +import UpdateUser from '@/pages/settings/profile/partials/update-user'; + +export default function Profile({ mustVerifyEmail, status }: { mustVerifyEmail: boolean; status?: string }) { + return ( + + + + + + + + + ); +} diff --git a/resources/js/components/delete-user.tsx b/resources/js/pages/settings/profile/partials/delete-user.tsx similarity index 72% rename from resources/js/components/delete-user.tsx rename to resources/js/pages/settings/profile/partials/delete-user.tsx index c8205304..58e08472 100644 --- a/resources/js/components/delete-user.tsx +++ b/resources/js/pages/settings/profile/partials/delete-user.tsx @@ -1,18 +1,29 @@ import { useForm } from '@inertiajs/react'; import { FormEventHandler, useRef } from 'react'; -import InputError from '@/components/input-error'; +import InputError from '@/components/ui/input-error'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; -import HeadingSmall from '@/components/heading-small'; - import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; export default function DeleteUser() { const passwordInput = useRef(null); - const { data, setData, delete: destroy, processing, reset, errors, clearErrors } = useForm>({ password: '' }); + const { + data, + setData, + delete: destroy, + processing, + reset, + errors, + clearErrors, + } = useForm< + Required<{ + password: string; + }> + >({ password: '' }); const deleteUser: FormEventHandler = (e) => { e.preventDefault(); @@ -31,14 +42,19 @@ export default function DeleteUser() { }; return ( -
- -
-
-

Warning

-

Please proceed with caution, this cannot be undone.

-
+ + + Delete account + Delete your account and all of its resources + + +
+
+

Warning

+

Please proceed with caution, this cannot be undone.

+
+
@@ -83,7 +99,7 @@ export default function DeleteUser() { -
-
+ + ); } diff --git a/resources/js/pages/settings/profile/partials/update-password.tsx b/resources/js/pages/settings/profile/partials/update-password.tsx new file mode 100644 index 00000000..f6dc7b16 --- /dev/null +++ b/resources/js/pages/settings/profile/partials/update-password.tsx @@ -0,0 +1,116 @@ +import InputError from '@/components/ui/input-error'; +import { Transition } from '@headlessui/react'; +import { useForm } from '@inertiajs/react'; +import { FormEventHandler, useRef } from 'react'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; + +export default function UpdatePassword() { + const passwordInput = useRef(null); + const currentPasswordInput = useRef(null); + + const { data, setData, errors, put, reset, processing, recentlySuccessful } = useForm({ + current_password: '', + password: '', + password_confirmation: '', + }); + + const updatePassword: FormEventHandler = (e) => { + e.preventDefault(); + + put(route('profile.password'), { + preserveScroll: true, + onSuccess: () => reset(), + onError: (errors) => { + if (errors.password) { + reset('password', 'password_confirmation'); + passwordInput.current?.focus(); + } + + if (errors.current_password) { + reset('current_password'); + currentPasswordInput.current?.focus(); + } + }, + }); + }; + + return ( + + + Update password + Ensure your account is using a long, random password to stay secure. + + +
+
+ + + setData('current_password', e.target.value)} + type="password" + className="mt-1 block w-full" + autoComplete="current-password" + placeholder="Current password" + /> + + +
+ +
+ + + setData('password', e.target.value)} + type="password" + className="mt-1 block w-full" + autoComplete="new-password" + placeholder="New password" + /> + + +
+ +
+ + + setData('password_confirmation', e.target.value)} + type="password" + className="mt-1 block w-full" + autoComplete="new-password" + placeholder="Confirm password" + /> + + +
+ +
+ + + +

Saved

+
+
+
+
+
+ ); +} diff --git a/resources/js/pages/settings/profile/partials/update-user.tsx b/resources/js/pages/settings/profile/partials/update-user.tsx new file mode 100644 index 00000000..f20eb1bf --- /dev/null +++ b/resources/js/pages/settings/profile/partials/update-user.tsx @@ -0,0 +1,110 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import InputError from '@/components/ui/input-error'; +import { Link, useForm, usePage } from '@inertiajs/react'; +import { Button } from '@/components/ui/button'; +import { Transition } from '@headlessui/react'; +import type { SharedData } from '@/types'; +import { FormEventHandler } from 'react'; + +type ProfileForm = { + name: string; + email: string; +}; + +export default function UpdateUser({ mustVerifyEmail, status }: { mustVerifyEmail: boolean; status?: string }) { + const { auth } = usePage().props; + + const { data, setData, patch, errors, processing, recentlySuccessful } = useForm>({ + name: auth.user.name, + email: auth.user.email, + }); + + const submit: FormEventHandler = (e) => { + e.preventDefault(); + + patch(route('profile.update'), { + preserveScroll: true, + }); + }; + + return ( + + + Profile information + Update your profile information and email address. + + +
+
+ + + setData('name', e.target.value)} + required + autoComplete="name" + placeholder="Full name" + /> + + +
+ +
+ + + setData('email', e.target.value)} + required + autoComplete="username" + placeholder="Email address" + /> + + +
+ + {mustVerifyEmail && auth.user.email_verified_at === null && ( +
+

+ Your email address is unverified.{' '} + + Click here to resend the verification email. + +

+ + {status === 'verification-link-sent' && ( +
A new verification link has been sent to your email address.
+ )} +
+ )} + +
+ + + +

Saved

+
+
+
+
+
+ ); +}