diff --git a/app/Actions/User/CreateUser.php b/app/Actions/User/CreateUser.php index 55569d87..13c178f0 100644 --- a/app/Actions/User/CreateUser.php +++ b/app/Actions/User/CreateUser.php @@ -14,7 +14,7 @@ class CreateUser */ public function create(array $input): User { - $this->validate($input); + Validator::make($input, self::rules())->validate(); /** @var User $user */ $user = User::query()->create([ @@ -28,14 +28,6 @@ public function create(array $input): User return $user; } - /** - * @param array $input - */ - private function validate(array $input): void - { - Validator::make($input, self::rules())->validate(); - } - /** * @return array */ diff --git a/app/Actions/User/UpdateUser.php b/app/Actions/User/UpdateUser.php index dbd268ba..9da9379a 100644 --- a/app/Actions/User/UpdateUser.php +++ b/app/Actions/User/UpdateUser.php @@ -18,7 +18,6 @@ public function update(User $user, array $input): User $user->name = $input['name']; $user->email = $input['email']; - $user->timezone = $input['timezone']; $user->role = $input['role']; if (isset($input['password'])) { @@ -50,10 +49,6 @@ public static function rules(User $user): array 'email', 'max:255', Rule::unique('users', 'email')->ignore($user->id), ], - 'timezone' => [ - 'required', - Rule::in(timezone_identifiers_list()), - ], 'role' => [ 'required', Rule::in([UserRole::ADMIN, UserRole::USER]), diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 21f33cfb..15f9c260 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -6,13 +6,11 @@ use Illuminate\Contracts\Auth\MustVerifyEmail; 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; use Spatie\RouteAttributes\Attributes\Get; use Spatie\RouteAttributes\Attributes\Middleware; use Spatie\RouteAttributes\Attributes\Patch; @@ -37,7 +35,6 @@ public function update(Request $request): RedirectResponse { $this->validate($request, [ 'name' => ['required', 'string', 'max:255'], - 'email' => [ 'required', 'string', @@ -66,25 +63,6 @@ public function password(Request $request): RedirectResponse 'password' => Hash::make($validated['password']), ]); - return back(); - } - - #[Delete('/', name: 'profile.destroy')] - public function destroy(Request $request): RedirectResponse - { - $request->validate([ - 'password' => ['required', 'current_password'], - ]); - - $user = $request->user(); - - Auth::logout(); - - $user->delete(); - - $request->session()->invalidate(); - $request->session()->regenerateToken(); - - return redirect('/'); + return to_route('profile'); } } diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 5464098c..5995b1a4 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -5,6 +5,7 @@ use App\Actions\Projects\AddUser; use App\Actions\Projects\CreateProject; use App\Actions\Projects\DeleteProject; +use App\Actions\Projects\UpdateProject; use App\Http\Resources\ProjectResource; use App\Models\Project; use App\Models\User; @@ -16,6 +17,7 @@ use Spatie\RouteAttributes\Attributes\Delete; use Spatie\RouteAttributes\Attributes\Get; use Spatie\RouteAttributes\Attributes\Middleware; +use Spatie\RouteAttributes\Attributes\Patch; use Spatie\RouteAttributes\Attributes\Post; use Spatie\RouteAttributes\Attributes\Prefix; @@ -30,7 +32,7 @@ public function index(): Response return Inertia::render('projects/index', [ 'projects' => ProjectResource::collection( - Project::query()->simplePaginate(config('web.pagination_size')) + Project::query()->with('users')->simplePaginate(config('web.pagination_size')) ), ]); } @@ -50,7 +52,18 @@ public function store(Request $request): RedirectResponse ->with('success', 'Project created successfully.'); } - #[Post('switch/{project}', name: 'projects.switch')] + #[Patch('/{project}', name: 'projects.update')] + public function update(Request $request, Project $project): RedirectResponse + { + $this->authorize('update', $project); + + app(UpdateProject::class)->update($project, $request->all()); + + return redirect()->route('projects') + ->with('success', 'Project updated successfully.'); + } + + #[Patch('switch/{project}', name: 'projects.switch')] public function switch(Project $project): RedirectResponse { $this->authorize('view', $project); @@ -70,7 +83,7 @@ public function switch(Project $project): RedirectResponse return redirect()->route($previousRoute->getName()); } - #[Post('/{project}/users', name: 'projects.users')] + #[Post('/{project}/users', name: 'projects.users.store')] public function storeUser(Request $request, Project $project): RedirectResponse { $this->authorize('update', $project); @@ -81,20 +94,11 @@ public function storeUser(Request $request, Project $project): RedirectResponse ->with('success', 'User added to project successfully.'); } - #[Delete('{project}/users', name: 'projects.users')] - public function destroyUser(Request $request, Project $project): RedirectResponse + #[Delete('{project}/users/{user}', name: 'projects.users.destroy')] + public function destroyUser(Project $project, User $user): RedirectResponse { $this->authorize('update', $project); - $this->validate($request, [ - 'user' => [ - 'required', - 'exists:users,id', - ], - ]); - - $user = User::query()->find($request->input('user')); - $project->users()->detach($user); return redirect()->route('projects') diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 52de6220..7839a408 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -2,12 +2,22 @@ namespace App\Http\Controllers; +use App\Actions\User\CreateUser; +use App\Actions\User\UpdateUser; use App\Http\Resources\UserResource; +use App\Models\Project; use App\Models\User; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\ResourceCollection; +use Illuminate\Validation\Rule; +use Inertia\Inertia; +use Inertia\Response; +use Spatie\RouteAttributes\Attributes\Delete; use Spatie\RouteAttributes\Attributes\Get; use Spatie\RouteAttributes\Attributes\Middleware; +use Spatie\RouteAttributes\Attributes\Patch; +use Spatie\RouteAttributes\Attributes\Post; use Spatie\RouteAttributes\Attributes\Prefix; #[Prefix('users')] @@ -15,7 +25,19 @@ class UserController extends Controller { #[Get('/', name: 'users')] - public function index(Request $request): ResourceCollection + public function index(): Response + { + $this->authorize('viewAny', User::class); + + return Inertia::render('users/index', [ + 'users' => UserResource::collection( + User::query()->with('projects')->simplePaginate(config('web.pagination_size')) + ), + ]); + } + + #[Get('/json', name: 'users.json')] + public function json(Request $request): ResourceCollection { $this->authorize('viewAny', User::class); @@ -33,4 +55,64 @@ public function index(Request $request): ResourceCollection return UserResource::collection($users); } + + #[Post('/', name: 'users.store')] + public function store(Request $request): RedirectResponse + { + $this->authorize('create', User::class); + + app(CreateUser::class)->create($request->all()); + + return to_route('users')->with('success', 'User created successfully.'); + } + + #[Patch('/{user}', name: 'users.update')] + public function update(Request $request, User $user): RedirectResponse + { + $this->authorize('update', $user); + + app(UpdateUser::class)->update($user, $request->all()); + + return to_route('users')->with('success', 'User updated successfully.'); + } + + #[Post('/{user}/projects', name: 'users.projects.store')] + public function addToProject(Request $request, User $user): RedirectResponse + { + $this->authorize('update', $user); + + $this->validate($request, [ + 'project' => [ + 'required', + Rule::exists('projects', 'id'), + ], + ]); + + $project = Project::query()->findOrFail($request->input('project')); + + $user->projects()->detach($project); + $user->projects()->attach($project); + + return to_route('users')->with('success', 'Project was successfully added to user.'); + } + + #[Delete('/{user}/projects/{project}', name: 'users.projects.destroy')] + public function removeProject(User $user, Project $project): RedirectResponse + { + $this->authorize('update', $user); + + $user->projects()->detach($project); + + return to_route('users')->with('success', 'Project was successfully removed from user.'); + } + + #[Delete('/{user}', 'users.destroy')] + public function destroy(User $user): RedirectResponse + { + $this->authorize('delete', $user); + + $user->delete(); + + return to_route('users')->with('success', 'User was successfully deleted.'); + } } diff --git a/app/Http/Resources/ProjectResource.php b/app/Http/Resources/ProjectResource.php index 8e427cea..cc3b2273 100644 --- a/app/Http/Resources/ProjectResource.php +++ b/app/Http/Resources/ProjectResource.php @@ -17,11 +17,11 @@ public function toArray(Request $request): array return [ 'id' => $this->id, 'name' => $this->name, - 'users' => UserResource::collection($this->users), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, - 'created_at_by_timezone' => $this->created_at_by_timezone, - 'updated_at_by_timezone' => $this->updated_at_by_timezone, + 'users' => UserResource::collection( + $this->whenLoaded('users') + ), ]; } } diff --git a/app/Http/Resources/ServerLogResource.php b/app/Http/Resources/ServerLogResource.php index 742bd31f..b813464b 100644 --- a/app/Http/Resources/ServerLogResource.php +++ b/app/Http/Resources/ServerLogResource.php @@ -24,8 +24,6 @@ public function toArray(Request $request): array 'is_remote' => $this->is_remote, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, - 'created_at_by_timezone' => $this->created_at_by_timezone, - 'updated_at_by_timezone' => $this->updated_at_by_timezone, ]; } } diff --git a/app/Http/Resources/ServerProviderResource.php b/app/Http/Resources/ServerProviderResource.php index d85f58bc..22e25094 100644 --- a/app/Http/Resources/ServerProviderResource.php +++ b/app/Http/Resources/ServerProviderResource.php @@ -22,8 +22,6 @@ public function toArray(Request $request): array 'provider' => $this->provider, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, - 'created_at_by_timezone' => $this->created_at_by_timezone, - 'updated_at_by_timezone' => $this->updated_at_by_timezone, ]; } } diff --git a/app/Http/Resources/ServerResource.php b/app/Http/Resources/ServerResource.php index 5983b68f..9bd6f9fc 100644 --- a/app/Http/Resources/ServerResource.php +++ b/app/Http/Resources/ServerResource.php @@ -41,8 +41,6 @@ public function toArray(Request $request): array 'status_color' => $this->getStatusColor(), 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, - 'created_at_by_timezone' => $this->created_at_by_timezone, - 'updated_at_by_timezone' => $this->updated_at_by_timezone, ]; } } diff --git a/app/Http/Resources/UserResource.php b/app/Http/Resources/UserResource.php index 190fb29b..84ca78ba 100644 --- a/app/Http/Resources/UserResource.php +++ b/app/Http/Resources/UserResource.php @@ -18,8 +18,10 @@ public function toArray(Request $request): array 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, + 'role' => $this->role, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, + 'projects' => ProjectResource::collection($this->whenLoaded('projects')), ]; } } diff --git a/package-lock.json b/package-lock.json index a985311d..64eec195 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "globals": "^15.14.0", "laravel-vite-plugin": "^1.0", "lucide-react": "^0.475.0", + "moment": "^2.30.1", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -6356,6 +6357,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/package.json b/package.json index 601c3b67..d3ef5730 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "globals": "^15.14.0", "laravel-vite-plugin": "^1.0", "lucide-react": "^0.475.0", + "moment": "^2.30.1", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/resources/css/app.css b/resources/css/app.css index 0ae51a02..d16d3736 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -92,21 +92,21 @@ @layer base { } :root { - --brand: var(--color-indigo-500); + --brand: oklch(58.5% 0.233 277.117); --background: oklch(1 0 0); --foreground: oklch(0.145 0 0); --card: oklch(1 0 0); --card-foreground: oklch(0.145 0 0); --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); + --primary: oklch(51.1% 0.262 276.966); + --primary-foreground: oklch(1 0 0); --secondary: oklch(0.97 0 0); --secondary-foreground: oklch(0.205 0 0); --muted: oklch(0.97 0 0); --muted-foreground: oklch(0.556 0 0); --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); + --accent-foreground: oklch(0.145 0 0); --destructive: oklch(0.577 0.245 27.325); --destructive-foreground: oklch(0.577 0.245 27.325); --success: var(--color-green-500); @@ -148,8 +148,8 @@ .dark { --card-foreground: oklch(0.985 0 0); --popover: oklch(0.145 0 0); --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); + --primary: oklch(51.1% 0.262 276.966); + --primary-foreground: oklch(0.985 0 0); --secondary: oklch(0.269 0 0); --secondary-foreground: oklch(0.985 0 0); --muted: oklch(0.269 0 0); @@ -199,10 +199,6 @@ @layer base { } } -/* - ---break--- -*/ - @theme inline { --color-sidebar: var(--sidebar); --color-sidebar-foreground: var(--sidebar-foreground); @@ -222,6 +218,7 @@ @layer base { * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; } diff --git a/resources/js/components/app-command.tsx b/resources/js/components/app-command.tsx index 2acf1b76..b6678063 100644 --- a/resources/js/components/app-command.tsx +++ b/resources/js/components/app-command.tsx @@ -1,4 +1,4 @@ -import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from '@/components/ui/command'; +import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; import { useEffect, useState } from 'react'; import { Button } from '@/components/ui/button'; import { CommandIcon, SearchIcon } from 'lucide-react'; diff --git a/resources/js/components/app-header.tsx b/resources/js/components/app-header.tsx index f32a1ba8..33778c06 100644 --- a/resources/js/components/app-header.tsx +++ b/resources/js/components/app-header.tsx @@ -1,5 +1,4 @@ import { SidebarTrigger } from '@/components/ui/sidebar'; -import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbSeparator } from '@/components/ui/breadcrumb'; import { ProjectSwitch } from '@/components/project-switch'; import { SlashIcon } from 'lucide-react'; import { ServerSwitch } from '@/components/server-switch'; @@ -10,19 +9,11 @@ export function AppHeader() {
- - - - - - - - - - - - - +
+ + + +
diff --git a/resources/js/components/app-logo-icon-html.tsx b/resources/js/components/app-logo-icon-html.tsx new file mode 100644 index 00000000..f9edde75 --- /dev/null +++ b/resources/js/components/app-logo-icon-html.tsx @@ -0,0 +1,14 @@ +import { cn } from '@/lib/utils'; + +export default function AppLogoIconHtml({ className }: { className?: string }) { + return ( +
+ V +
+ ); +} diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/app-logo.tsx index dd7b3850..b7930f9a 100644 --- a/resources/js/components/app-logo.tsx +++ b/resources/js/components/app-logo.tsx @@ -4,7 +4,7 @@ export default function AppLogo() { return ( <>
- +
); diff --git a/resources/js/components/data-table.tsx b/resources/js/components/data-table.tsx index ad079233..18b4689c 100644 --- a/resources/js/components/data-table.tsx +++ b/resources/js/components/data-table.tsx @@ -1,23 +1,26 @@ -'use client'; - import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { cn } from '@/lib/utils'; interface DataTableProps { columns: ColumnDef[]; data: TData[]; + className?: string; + modal?: boolean; } -export function DataTable({ columns, data }: DataTableProps) { +export function DataTable({ columns, data, className, modal }: DataTableProps) { const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), }); + const extraClasses = modal && 'border-none shadow-none'; + return ( -
+
{table.getHeaderGroups().map((headerGroup) => ( diff --git a/resources/js/components/date-time.tsx b/resources/js/components/date-time.tsx new file mode 100644 index 00000000..fcb142cd --- /dev/null +++ b/resources/js/components/date-time.tsx @@ -0,0 +1,9 @@ +import moment from 'moment'; + +export default function DateTime({ date, format = 'YYYY-MM-DD hh:mm:ss', className }: { date: string; format?: string; className?: string }) { + return ( + + ); +} diff --git a/resources/js/components/project-switch.tsx b/resources/js/components/project-switch.tsx index 2e406d4d..90669a4e 100644 --- a/resources/js/components/project-switch.tsx +++ b/resources/js/components/project-switch.tsx @@ -13,7 +13,7 @@ import { Button } from '@/components/ui/button'; import { ChevronsUpDownIcon, PlusIcon } from 'lucide-react'; import { useInitials } from '@/hooks/use-initials'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import CreateProject from '@/pages/projects/components/create-project'; +import ProjectForm from '@/pages/projects/components/project-form'; export function ProjectSwitch() { const page = usePage(); @@ -26,7 +26,7 @@ export function ProjectSwitch() { const selectedProject = auth.projects.find((project) => project.id.toString() === projectId); if (selectedProject) { setSelectedProject(selectedProject.id.toString()); - form.post(route('projects.switch', { project: projectId, currentPath: window.location.pathname })); + form.patch(route('projects.switch', { project: projectId, currentPath: window.location.pathname })); } }; @@ -57,14 +57,14 @@ export function ProjectSwitch() { ))} - + e.preventDefault()}>
Create new project
-
+ diff --git a/resources/js/components/ui/alert-dialog.tsx b/resources/js/components/ui/alert-dialog.tsx deleted file mode 100644 index 935eecf3..00000000 --- a/resources/js/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -function AlertDialog({ - ...props -}: React.ComponentProps) { - return -} - -function AlertDialogTrigger({ - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogOverlay({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogContent({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - - ) -} - -function AlertDialogHeader({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( -
- ) -} - -function AlertDialogTitle({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogDescription({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogAction({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -function AlertDialogCancel({ - className, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/resources/js/components/ui/button.tsx b/resources/js/components/ui/button.tsx index 895aaec3..c30aca7d 100644 --- a/resources/js/components/ui/button.tsx +++ b/resources/js/components/ui/button.tsx @@ -9,9 +9,10 @@ const buttonVariants = cva( { variants: { variant: { - default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + default: + 'shadow-lg dark:border dark:border-primary bg-primary/90 dark:bg-primary/60 text-primary-foreground dark:text-foreground/90 shadow-xs hover:bg-primary/90 dark:hover:bg-primary/80 focus-visible:ring-primary/20 dark:focus-visible:ring-primary/40', destructive: - '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', + 'border border-destructive dark:bg-destructive/60 bg-destructive/70 text-white shadow-xs hover:bg-destructive/90 dark:hover:bg-destructive/80 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40', 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', diff --git a/resources/js/components/ui/card.tsx b/resources/js/components/ui/card.tsx index 72ccb96d..c8f99644 100644 --- a/resources/js/components/ui/card.tsx +++ b/resources/js/components/ui/card.tsx @@ -3,13 +3,11 @@ import * as React from 'react'; import { cn } from '@/lib/utils'; function Card({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ); + return
; } function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { - return
; + return
; } function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { @@ -21,11 +19,11 @@ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { } function CardContent({ className, ...props }: React.ComponentProps<'div'>) { - return
; + return
; } function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { - return
; + return
; } export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/resources/js/components/ui/combobox.tsx b/resources/js/components/ui/combobox.tsx new file mode 100644 index 00000000..a4f3dea9 --- /dev/null +++ b/resources/js/components/ui/combobox.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { Check, ChevronsUpDown } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; + +export function Combobox({ + items, + value, + onValueChange, +}: { + items: { value: string; label: string }[]; + value: string; + onValueChange: (value: string) => void; +}) { + const [open, setOpen] = React.useState(false); + + return ( + + + + + + + + + No item found. + + {open && + items.map((item) => ( + { + value = currentValue; + onValueChange(value); + setOpen(false); + }} + > + {item.label} + + + ))} + + + + + + ); +} diff --git a/resources/js/components/ui/dialog.tsx b/resources/js/components/ui/dialog.tsx index 251de46a..8aca62fa 100644 --- a/resources/js/components/ui/dialog.tsx +++ b/resources/js/components/ui/dialog.tsx @@ -25,7 +25,7 @@ function DialogOverlay({ className, ...props }: React.ComponentProps) { - return
; + return
; } function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) { - return
; + return ( +
+ ); } function DialogTitle({ className, ...props }: React.ComponentProps) { diff --git a/resources/js/components/ui/input.tsx b/resources/js/components/ui/input.tsx index 3c1cfcaf..2578a301 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 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', + 'file:text-foreground placeholder:text-muted-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/select.tsx b/resources/js/components/ui/select.tsx index d6d1a18b..d76b78fe 100644 --- a/resources/js/components/ui/select.tsx +++ b/resources/js/components/ui/select.tsx @@ -21,7 +21,7 @@ function SelectTrigger({ className, children, ...props }: React.ComponentPropsspan]:line-clamp-1", + "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex h-9 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&>span]:line-clamp-1", className, )} {...props} diff --git a/resources/js/components/ui/separator.tsx b/resources/js/components/ui/separator.tsx index 2d953010..fb8eb4fc 100644 --- a/resources/js/components/ui/separator.tsx +++ b/resources/js/components/ui/separator.tsx @@ -1,5 +1,3 @@ -'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 46fbc062..e0c8083b 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
; } function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) { - return
; + return
; } function SheetTitle({ className, ...props }: React.ComponentProps) { diff --git a/resources/js/components/ui/sidebar.tsx b/resources/js/components/ui/sidebar.tsx index 626c50fb..51da37e5 100644 --- a/resources/js/components/ui/sidebar.tsx +++ b/resources/js/components/ui/sidebar.tsx @@ -365,7 +365,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) { } const sidebarMenuButtonVariants = cva( - 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', + 'cursor-pointer peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', { variants: { variant: { diff --git a/resources/js/components/ui/table.tsx b/resources/js/components/ui/table.tsx index 640f7e77..1553673e 100644 --- a/resources/js/components/ui/table.tsx +++ b/resources/js/components/ui/table.tsx @@ -33,7 +33,7 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
[role=checkbox]]:translate-y-[2px]', + 'text-foreground h-10 px-4 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className, )} {...props} @@ -45,7 +45,7 @@ function TableCell({ className, ...props }: React.ComponentProps<'td'>) { return ( [role=checkbox]]:translate-y-[2px]', className)} + className={cn('px-4 py-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)} {...props} /> ); diff --git a/resources/js/components/ui/tooltip.tsx b/resources/js/components/ui/tooltip.tsx index 942fa16c..41d8ec2a 100644 --- a/resources/js/components/ui/tooltip.tsx +++ b/resources/js/components/ui/tooltip.tsx @@ -1,43 +1,28 @@ -'use client'; - import * as React from 'react'; import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import { cn } from '@/lib/utils'; -function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps) { - return ; -} +const TooltipProvider = TooltipPrimitive.Provider; -function Tooltip({ ...props }: React.ComponentProps) { - return ( - - - - ); -} +const Tooltip = TooltipPrimitive.Root; -function TooltipTrigger({ ...props }: React.ComponentProps) { - return ; -} +const TooltipTrigger = TooltipPrimitive.Trigger; -function TooltipContent({ className, sideOffset = 0, children, ...props }: React.ComponentProps) { - return ( - - - {children} - - - - ); -} +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/resources/js/components/user-select.tsx b/resources/js/components/user-select.tsx index e61a10ff..b9c59bd7 100644 --- a/resources/js/components/user-select.tsx +++ b/resources/js/components/user-select.tsx @@ -14,7 +14,7 @@ export default function UserSelect({ onChange }: { onChange: (selectedUser: User const [value, setValue] = useState(); const fetchUsers = async () => { - const response = await axios.get(route('users', { query: query })); + const response = await axios.get(route('users.json', { query: query })); if (response.status === 200) { setUsers(response.data as User[]); diff --git a/resources/js/layouts/settings/layout.tsx b/resources/js/layouts/settings/layout.tsx index 9a296d87..94b47ea3 100644 --- a/resources/js/layouts/settings/layout.tsx +++ b/resources/js/layouts/settings/layout.tsx @@ -1,5 +1,5 @@ import { type BreadcrumbItem, type NavItem } from '@/types'; -import { ListIcon, UserIcon } from 'lucide-react'; +import { ListIcon, UserIcon, UsersIcon } from 'lucide-react'; import { ReactNode } from 'react'; import Layout from '@/layouts/app/layout'; @@ -9,6 +9,11 @@ const sidebarNavItems: NavItem[] = [ href: route('profile'), icon: UserIcon, }, + { + title: 'Users', + href: route('users'), + icon: UsersIcon, + }, { title: 'Projects', href: route('projects'), diff --git a/resources/js/lib/timezones.ts b/resources/js/lib/timezones.ts new file mode 100644 index 00000000..ee50ff90 --- /dev/null +++ b/resources/js/lib/timezones.ts @@ -0,0 +1,420 @@ +export const timezones = [ + { value: 'Africa/Abidjan', label: 'Africa/Abidjan' }, + { value: 'Africa/Accra', label: 'Africa/Accra' }, + { value: 'Africa/Addis_Ababa', label: 'Africa/Addis_Ababa' }, + { value: 'Africa/Algiers', label: 'Africa/Algiers' }, + { value: 'Africa/Asmara', label: 'Africa/Asmara' }, + { value: 'Africa/Bamako', label: 'Africa/Bamako' }, + { value: 'Africa/Bangui', label: 'Africa/Bangui' }, + { value: 'Africa/Banjul', label: 'Africa/Banjul' }, + { value: 'Africa/Bissau', label: 'Africa/Bissau' }, + { value: 'Africa/Blantyre', label: 'Africa/Blantyre' }, + { value: 'Africa/Brazzaville', label: 'Africa/Brazzaville' }, + { value: 'Africa/Bujumbura', label: 'Africa/Bujumbura' }, + { value: 'Africa/Cairo', label: 'Africa/Cairo' }, + { value: 'Africa/Casablanca', label: 'Africa/Casablanca' }, + { value: 'Africa/Ceuta', label: 'Africa/Ceuta' }, + { value: 'Africa/Conakry', label: 'Africa/Conakry' }, + { value: 'Africa/Dakar', label: 'Africa/Dakar' }, + { value: 'Africa/Dar_es_Salaam', label: 'Africa/Dar_es_Salaam' }, + { value: 'Africa/Djibouti', label: 'Africa/Djibouti' }, + { value: 'Africa/Douala', label: 'Africa/Douala' }, + { value: 'Africa/El_Aaiun', label: 'Africa/El_Aaiun' }, + { value: 'Africa/Freetown', label: 'Africa/Freetown' }, + { value: 'Africa/Gaborone', label: 'Africa/Gaborone' }, + { value: 'Africa/Harare', label: 'Africa/Harare' }, + { value: 'Africa/Johannesburg', label: 'Africa/Johannesburg' }, + { value: 'Africa/Juba', label: 'Africa/Juba' }, + { value: 'Africa/Kampala', label: 'Africa/Kampala' }, + { value: 'Africa/Khartoum', label: 'Africa/Khartoum' }, + { value: 'Africa/Kigali', label: 'Africa/Kigali' }, + { value: 'Africa/Kinshasa', label: 'Africa/Kinshasa' }, + { value: 'Africa/Lagos', label: 'Africa/Lagos' }, + { value: 'Africa/Libreville', label: 'Africa/Libreville' }, + { value: 'Africa/Lome', label: 'Africa/Lome' }, + { value: 'Africa/Luanda', label: 'Africa/Luanda' }, + { value: 'Africa/Lubumbashi', label: 'Africa/Lubumbashi' }, + { value: 'Africa/Lusaka', label: 'Africa/Lusaka' }, + { value: 'Africa/Malabo', label: 'Africa/Malabo' }, + { value: 'Africa/Maputo', label: 'Africa/Maputo' }, + { value: 'Africa/Maseru', label: 'Africa/Maseru' }, + { value: 'Africa/Mbabane', label: 'Africa/Mbabane' }, + { value: 'Africa/Mogadishu', label: 'Africa/Mogadishu' }, + { value: 'Africa/Monrovia', label: 'Africa/Monrovia' }, + { value: 'Africa/Nairobi', label: 'Africa/Nairobi' }, + { value: 'Africa/Ndjamena', label: 'Africa/Ndjamena' }, + { value: 'Africa/Niamey', label: 'Africa/Niamey' }, + { value: 'Africa/Nouakchott', label: 'Africa/Nouakchott' }, + { value: 'Africa/Ouagadougou', label: 'Africa/Ouagadougou' }, + { value: 'Africa/Porto-Novo', label: 'Africa/Porto-Novo' }, + { value: 'Africa/Sao_Tome', label: 'Africa/Sao_Tome' }, + { value: 'Africa/Tripoli', label: 'Africa/Tripoli' }, + { value: 'Africa/Tunis', label: 'Africa/Tunis' }, + { value: 'Africa/Windhoek', label: 'Africa/Windhoek' }, + { value: 'America/Adak', label: 'America/Adak' }, + { value: 'America/Anchorage', label: 'America/Anchorage' }, + { value: 'America/Anguilla', label: 'America/Anguilla' }, + { value: 'America/Antigua', label: 'America/Antigua' }, + { value: 'America/Araguaina', label: 'America/Araguaina' }, + { value: 'America/Argentina/Buenos_Aires', label: 'America/Argentina/Buenos_Aires' }, + { value: 'America/Argentina/Catamarca', label: 'America/Argentina/Catamarca' }, + { value: 'America/Argentina/Cordoba', label: 'America/Argentina/Cordoba' }, + { value: 'America/Argentina/Jujuy', label: 'America/Argentina/Jujuy' }, + { value: 'America/Argentina/La_Rioja', label: 'America/Argentina/La_Rioja' }, + { value: 'America/Argentina/Mendoza', label: 'America/Argentina/Mendoza' }, + { value: 'America/Argentina/Rio_Gallegos', label: 'America/Argentina/Rio_Gallegos' }, + { value: 'America/Argentina/Salta', label: 'America/Argentina/Salta' }, + { value: 'America/Argentina/San_Juan', label: 'America/Argentina/San_Juan' }, + { value: 'America/Argentina/San_Luis', label: 'America/Argentina/San_Luis' }, + { value: 'America/Argentina/Tucuman', label: 'America/Argentina/Tucuman' }, + { value: 'America/Argentina/Ushuaia', label: 'America/Argentina/Ushuaia' }, + { value: 'America/Aruba', label: 'America/Aruba' }, + { value: 'America/Asuncion', label: 'America/Asuncion' }, + { value: 'America/Atikokan', label: 'America/Atikokan' }, + { value: 'America/Bahia', label: 'America/Bahia' }, + { value: 'America/Bahia_Banderas', label: 'America/Bahia_Banderas' }, + { value: 'America/Barbados', label: 'America/Barbados' }, + { value: 'America/Belem', label: 'America/Belem' }, + { value: 'America/Belize', label: 'America/Belize' }, + { value: 'America/Blanc-Sablon', label: 'America/Blanc-Sablon' }, + { value: 'America/Boa_Vista', label: 'America/Boa_Vista' }, + { value: 'America/Bogota', label: 'America/Bogota' }, + { value: 'America/Boise', label: 'America/Boise' }, + { value: 'America/Cambridge_Bay', label: 'America/Cambridge_Bay' }, + { value: 'America/Campo_Grande', label: 'America/Campo_Grande' }, + { value: 'America/Cancun', label: 'America/Cancun' }, + { value: 'America/Caracas', label: 'America/Caracas' }, + { value: 'America/Cayenne', label: 'America/Cayenne' }, + { value: 'America/Cayman', label: 'America/Cayman' }, + { value: 'America/Chicago', label: 'America/Chicago' }, + { value: 'America/Chihuahua', label: 'America/Chihuahua' }, + { value: 'America/Ciudad_Juarez', label: 'America/Ciudad_Juarez' }, + { value: 'America/Costa_Rica', label: 'America/Costa_Rica' }, + { value: 'America/Creston', label: 'America/Creston' }, + { value: 'America/Cuiaba', label: 'America/Cuiaba' }, + { value: 'America/Curacao', label: 'America/Curacao' }, + { value: 'America/Danmarkshavn', label: 'America/Danmarkshavn' }, + { value: 'America/Dawson', label: 'America/Dawson' }, + { value: 'America/Dawson_Creek', label: 'America/Dawson_Creek' }, + { value: 'America/Denver', label: 'America/Denver' }, + { value: 'America/Detroit', label: 'America/Detroit' }, + { value: 'America/Dominica', label: 'America/Dominica' }, + { value: 'America/Edmonton', label: 'America/Edmonton' }, + { value: 'America/Eirunepe', label: 'America/Eirunepe' }, + { value: 'America/El_Salvador', label: 'America/El_Salvador' }, + { value: 'America/Fort_Nelson', label: 'America/Fort_Nelson' }, + { value: 'America/Fortaleza', label: 'America/Fortaleza' }, + { value: 'America/Glace_Bay', label: 'America/Glace_Bay' }, + { value: 'America/Goose_Bay', label: 'America/Goose_Bay' }, + { value: 'America/Grand_Turk', label: 'America/Grand_Turk' }, + { value: 'America/Grenada', label: 'America/Grenada' }, + { value: 'America/Guadeloupe', label: 'America/Guadeloupe' }, + { value: 'America/Guatemala', label: 'America/Guatemala' }, + { value: 'America/Guayaquil', label: 'America/Guayaquil' }, + { value: 'America/Guyana', label: 'America/Guyana' }, + { value: 'America/Halifax', label: 'America/Halifax' }, + { value: 'America/Havana', label: 'America/Havana' }, + { value: 'America/Hermosillo', label: 'America/Hermosillo' }, + { value: 'America/Indiana/Indianapolis', label: 'America/Indiana/Indianapolis' }, + { value: 'America/Indiana/Knox', label: 'America/Indiana/Knox' }, + { value: 'America/Indiana/Marengo', label: 'America/Indiana/Marengo' }, + { value: 'America/Indiana/Petersburg', label: 'America/Indiana/Petersburg' }, + { value: 'America/Indiana/Tell_City', label: 'America/Indiana/Tell_City' }, + { value: 'America/Indiana/Vevay', label: 'America/Indiana/Vevay' }, + { value: 'America/Indiana/Vincennes', label: 'America/Indiana/Vincennes' }, + { value: 'America/Indiana/Winamac', label: 'America/Indiana/Winamac' }, + { value: 'America/Inuvik', label: 'America/Inuvik' }, + { value: 'America/Iqaluit', label: 'America/Iqaluit' }, + { value: 'America/Jamaica', label: 'America/Jamaica' }, + { value: 'America/Juneau', label: 'America/Juneau' }, + { value: 'America/Kentucky/Louisville', label: 'America/Kentucky/Louisville' }, + { value: 'America/Kentucky/Monticello', label: 'America/Kentucky/Monticello' }, + { value: 'America/Kralendijk', label: 'America/Kralendijk' }, + { value: 'America/La_Paz', label: 'America/La_Paz' }, + { value: 'America/Lima', label: 'America/Lima' }, + { value: 'America/Los_Angeles', label: 'America/Los_Angeles' }, + { value: 'America/Lower_Princes', label: 'America/Lower_Princes' }, + { value: 'America/Maceio', label: 'America/Maceio' }, + { value: 'America/Managua', label: 'America/Managua' }, + { value: 'America/Manaus', label: 'America/Manaus' }, + { value: 'America/Marigot', label: 'America/Marigot' }, + { value: 'America/Martinique', label: 'America/Martinique' }, + { value: 'America/Matamoros', label: 'America/Matamoros' }, + { value: 'America/Mazatlan', label: 'America/Mazatlan' }, + { value: 'America/Menominee', label: 'America/Menominee' }, + { value: 'America/Merida', label: 'America/Merida' }, + { value: 'America/Metlakatla', label: 'America/Metlakatla' }, + { value: 'America/Mexico_City', label: 'America/Mexico_City' }, + { value: 'America/Miquelon', label: 'America/Miquelon' }, + { value: 'America/Moncton', label: 'America/Moncton' }, + { value: 'America/Monterrey', label: 'America/Monterrey' }, + { value: 'America/Montevideo', label: 'America/Montevideo' }, + { value: 'America/Montserrat', label: 'America/Montserrat' }, + { value: 'America/Nassau', label: 'America/Nassau' }, + { value: 'America/New_York', label: 'America/New_York' }, + { value: 'America/Nome', label: 'America/Nome' }, + { value: 'America/Noronha', label: 'America/Noronha' }, + { value: 'America/North_Dakota/Beulah', label: 'America/North_Dakota/Beulah' }, + { value: 'America/North_Dakota/Center', label: 'America/North_Dakota/Center' }, + { value: 'America/North_Dakota/New_Salem', label: 'America/North_Dakota/New_Salem' }, + { value: 'America/Nuuk', label: 'America/Nuuk' }, + { value: 'America/Ojinaga', label: 'America/Ojinaga' }, + { value: 'America/Panama', label: 'America/Panama' }, + { value: 'America/Paramaribo', label: 'America/Paramaribo' }, + { value: 'America/Phoenix', label: 'America/Phoenix' }, + { value: 'America/Port-au-Prince', label: 'America/Port-au-Prince' }, + { value: 'America/Port_of_Spain', label: 'America/Port_of_Spain' }, + { value: 'America/Porto_Velho', label: 'America/Porto_Velho' }, + { value: 'America/Puerto_Rico', label: 'America/Puerto_Rico' }, + { value: 'America/Punta_Arenas', label: 'America/Punta_Arenas' }, + { value: 'America/Rankin_Inlet', label: 'America/Rankin_Inlet' }, + { value: 'America/Recife', label: 'America/Recife' }, + { value: 'America/Regina', label: 'America/Regina' }, + { value: 'America/Resolute', label: 'America/Resolute' }, + { value: 'America/Rio_Branco', label: 'America/Rio_Branco' }, + { value: 'America/Santarem', label: 'America/Santarem' }, + { value: 'America/Santiago', label: 'America/Santiago' }, + { value: 'America/Santo_Domingo', label: 'America/Santo_Domingo' }, + { value: 'America/Sao_Paulo', label: 'America/Sao_Paulo' }, + { value: 'America/Scoresbysund', label: 'America/Scoresbysund' }, + { value: 'America/Sitka', label: 'America/Sitka' }, + { value: 'America/St_Barthelemy', label: 'America/St_Barthelemy' }, + { value: 'America/St_Johns', label: 'America/St_Johns' }, + { value: 'America/St_Kitts', label: 'America/St_Kitts' }, + { value: 'America/St_Lucia', label: 'America/St_Lucia' }, + { value: 'America/St_Thomas', label: 'America/St_Thomas' }, + { value: 'America/St_Vincent', label: 'America/St_Vincent' }, + { value: 'America/Swift_Current', label: 'America/Swift_Current' }, + { value: 'America/Tegucigalpa', label: 'America/Tegucigalpa' }, + { value: 'America/Thule', label: 'America/Thule' }, + { value: 'America/Tijuana', label: 'America/Tijuana' }, + { value: 'America/Toronto', label: 'America/Toronto' }, + { value: 'America/Tortola', label: 'America/Tortola' }, + { value: 'America/Vancouver', label: 'America/Vancouver' }, + { value: 'America/Whitehorse', label: 'America/Whitehorse' }, + { value: 'America/Winnipeg', label: 'America/Winnipeg' }, + { value: 'America/Yakutat', label: 'America/Yakutat' }, + { value: 'Antarctica/Casey', label: 'Antarctica/Casey' }, + { value: 'Antarctica/Davis', label: 'Antarctica/Davis' }, + { value: 'Antarctica/DumontDUrville', label: 'Antarctica/DumontDUrville' }, + { value: 'Antarctica/Macquarie', label: 'Antarctica/Macquarie' }, + { value: 'Antarctica/Mawson', label: 'Antarctica/Mawson' }, + { value: 'Antarctica/McMurdo', label: 'Antarctica/McMurdo' }, + { value: 'Antarctica/Palmer', label: 'Antarctica/Palmer' }, + { value: 'Antarctica/Rothera', label: 'Antarctica/Rothera' }, + { value: 'Antarctica/Syowa', label: 'Antarctica/Syowa' }, + { value: 'Antarctica/Troll', label: 'Antarctica/Troll' }, + { value: 'Antarctica/Vostok', label: 'Antarctica/Vostok' }, + { value: 'Arctic/Longyearbyen', label: 'Arctic/Longyearbyen' }, + { value: 'Asia/Aden', label: 'Asia/Aden' }, + { value: 'Asia/Almaty', label: 'Asia/Almaty' }, + { value: 'Asia/Amman', label: 'Asia/Amman' }, + { value: 'Asia/Anadyr', label: 'Asia/Anadyr' }, + { value: 'Asia/Aqtau', label: 'Asia/Aqtau' }, + { value: 'Asia/Aqtobe', label: 'Asia/Aqtobe' }, + { value: 'Asia/Ashgabat', label: 'Asia/Ashgabat' }, + { value: 'Asia/Atyrau', label: 'Asia/Atyrau' }, + { value: 'Asia/Baghdad', label: 'Asia/Baghdad' }, + { value: 'Asia/Bahrain', label: 'Asia/Bahrain' }, + { value: 'Asia/Baku', label: 'Asia/Baku' }, + { value: 'Asia/Bangkok', label: 'Asia/Bangkok' }, + { value: 'Asia/Barnaul', label: 'Asia/Barnaul' }, + { value: 'Asia/Beirut', label: 'Asia/Beirut' }, + { value: 'Asia/Bishkek', label: 'Asia/Bishkek' }, + { value: 'Asia/Brunei', label: 'Asia/Brunei' }, + { value: 'Asia/Chita', label: 'Asia/Chita' }, + { value: 'Asia/Colombo', label: 'Asia/Colombo' }, + { value: 'Asia/Damascus', label: 'Asia/Damascus' }, + { value: 'Asia/Dhaka', label: 'Asia/Dhaka' }, + { value: 'Asia/Dili', label: 'Asia/Dili' }, + { value: 'Asia/Dubai', label: 'Asia/Dubai' }, + { value: 'Asia/Dushanbe', label: 'Asia/Dushanbe' }, + { value: 'Asia/Famagusta', label: 'Asia/Famagusta' }, + { value: 'Asia/Gaza', label: 'Asia/Gaza' }, + { value: 'Asia/Hebron', label: 'Asia/Hebron' }, + { value: 'Asia/Ho_Chi_Minh', label: 'Asia/Ho_Chi_Minh' }, + { value: 'Asia/Hong_Kong', label: 'Asia/Hong_Kong' }, + { value: 'Asia/Hovd', label: 'Asia/Hovd' }, + { value: 'Asia/Irkutsk', label: 'Asia/Irkutsk' }, + { value: 'Asia/Jakarta', label: 'Asia/Jakarta' }, + { value: 'Asia/Jayapura', label: 'Asia/Jayapura' }, + { value: 'Asia/Jerusalem', label: 'Asia/Jerusalem' }, + { value: 'Asia/Kabul', label: 'Asia/Kabul' }, + { value: 'Asia/Kamchatka', label: 'Asia/Kamchatka' }, + { value: 'Asia/Karachi', label: 'Asia/Karachi' }, + { value: 'Asia/Kathmandu', label: 'Asia/Kathmandu' }, + { value: 'Asia/Khandyga', label: 'Asia/Khandyga' }, + { value: 'Asia/Kolkata', label: 'Asia/Kolkata' }, + { value: 'Asia/Krasnoyarsk', label: 'Asia/Krasnoyarsk' }, + { value: 'Asia/Kuala_Lumpur', label: 'Asia/Kuala_Lumpur' }, + { value: 'Asia/Kuching', label: 'Asia/Kuching' }, + { value: 'Asia/Kuwait', label: 'Asia/Kuwait' }, + { value: 'Asia/Macau', label: 'Asia/Macau' }, + { value: 'Asia/Magadan', label: 'Asia/Magadan' }, + { value: 'Asia/Makassar', label: 'Asia/Makassar' }, + { value: 'Asia/Manila', label: 'Asia/Manila' }, + { value: 'Asia/Muscat', label: 'Asia/Muscat' }, + { value: 'Asia/Nicosia', label: 'Asia/Nicosia' }, + { value: 'Asia/Novokuznetsk', label: 'Asia/Novokuznetsk' }, + { value: 'Asia/Novosibirsk', label: 'Asia/Novosibirsk' }, + { value: 'Asia/Omsk', label: 'Asia/Omsk' }, + { value: 'Asia/Oral', label: 'Asia/Oral' }, + { value: 'Asia/Phnom_Penh', label: 'Asia/Phnom_Penh' }, + { value: 'Asia/Pontianak', label: 'Asia/Pontianak' }, + { value: 'Asia/Pyongyang', label: 'Asia/Pyongyang' }, + { value: 'Asia/Qatar', label: 'Asia/Qatar' }, + { value: 'Asia/Qostanay', label: 'Asia/Qostanay' }, + { value: 'Asia/Qyzylorda', label: 'Asia/Qyzylorda' }, + { value: 'Asia/Riyadh', label: 'Asia/Riyadh' }, + { value: 'Asia/Sakhalin', label: 'Asia/Sakhalin' }, + { value: 'Asia/Samarkand', label: 'Asia/Samarkand' }, + { value: 'Asia/Seoul', label: 'Asia/Seoul' }, + { value: 'Asia/Shanghai', label: 'Asia/Shanghai' }, + { value: 'Asia/Singapore', label: 'Asia/Singapore' }, + { value: 'Asia/Srednekolymsk', label: 'Asia/Srednekolymsk' }, + { value: 'Asia/Taipei', label: 'Asia/Taipei' }, + { value: 'Asia/Tashkent', label: 'Asia/Tashkent' }, + { value: 'Asia/Tbilisi', label: 'Asia/Tbilisi' }, + { value: 'Asia/Tehran', label: 'Asia/Tehran' }, + { value: 'Asia/Thimphu', label: 'Asia/Thimphu' }, + { value: 'Asia/Tokyo', label: 'Asia/Tokyo' }, + { value: 'Asia/Tomsk', label: 'Asia/Tomsk' }, + { value: 'Asia/Ulaanbaatar', label: 'Asia/Ulaanbaatar' }, + { value: 'Asia/Urumqi', label: 'Asia/Urumqi' }, + { value: 'Asia/Ust-Nera', label: 'Asia/Ust-Nera' }, + { value: 'Asia/Vientiane', label: 'Asia/Vientiane' }, + { value: 'Asia/Vladivostok', label: 'Asia/Vladivostok' }, + { value: 'Asia/Yakutsk', label: 'Asia/Yakutsk' }, + { value: 'Asia/Yangon', label: 'Asia/Yangon' }, + { value: 'Asia/Yekaterinburg', label: 'Asia/Yekaterinburg' }, + { value: 'Asia/Yerevan', label: 'Asia/Yerevan' }, + { value: 'Atlantic/Azores', label: 'Atlantic/Azores' }, + { value: 'Atlantic/Bermuda', label: 'Atlantic/Bermuda' }, + { value: 'Atlantic/Canary', label: 'Atlantic/Canary' }, + { value: 'Atlantic/Cape_Verde', label: 'Atlantic/Cape_Verde' }, + { value: 'Atlantic/Faroe', label: 'Atlantic/Faroe' }, + { value: 'Atlantic/Madeira', label: 'Atlantic/Madeira' }, + { value: 'Atlantic/Reykjavik', label: 'Atlantic/Reykjavik' }, + { value: 'Atlantic/South_Georgia', label: 'Atlantic/South_Georgia' }, + { value: 'Atlantic/St_Helena', label: 'Atlantic/St_Helena' }, + { value: 'Atlantic/Stanley', label: 'Atlantic/Stanley' }, + { value: 'Australia/Adelaide', label: 'Australia/Adelaide' }, + { value: 'Australia/Brisbane', label: 'Australia/Brisbane' }, + { value: 'Australia/Broken_Hill', label: 'Australia/Broken_Hill' }, + { value: 'Australia/Darwin', label: 'Australia/Darwin' }, + { value: 'Australia/Eucla', label: 'Australia/Eucla' }, + { value: 'Australia/Hobart', label: 'Australia/Hobart' }, + { value: 'Australia/Lindeman', label: 'Australia/Lindeman' }, + { value: 'Australia/Lord_Howe', label: 'Australia/Lord_Howe' }, + { value: 'Australia/Melbourne', label: 'Australia/Melbourne' }, + { value: 'Australia/Perth', label: 'Australia/Perth' }, + { value: 'Australia/Sydney', label: 'Australia/Sydney' }, + { value: 'Europe/Amsterdam', label: 'Europe/Amsterdam' }, + { value: 'Europe/Andorra', label: 'Europe/Andorra' }, + { value: 'Europe/Astrakhan', label: 'Europe/Astrakhan' }, + { value: 'Europe/Athens', label: 'Europe/Athens' }, + { value: 'Europe/Belgrade', label: 'Europe/Belgrade' }, + { value: 'Europe/Berlin', label: 'Europe/Berlin' }, + { value: 'Europe/Bratislava', label: 'Europe/Bratislava' }, + { value: 'Europe/Brussels', label: 'Europe/Brussels' }, + { value: 'Europe/Bucharest', label: 'Europe/Bucharest' }, + { value: 'Europe/Budapest', label: 'Europe/Budapest' }, + { value: 'Europe/Busingen', label: 'Europe/Busingen' }, + { value: 'Europe/Chisinau', label: 'Europe/Chisinau' }, + { value: 'Europe/Copenhagen', label: 'Europe/Copenhagen' }, + { value: 'Europe/Dublin', label: 'Europe/Dublin' }, + { value: 'Europe/Gibraltar', label: 'Europe/Gibraltar' }, + { value: 'Europe/Guernsey', label: 'Europe/Guernsey' }, + { value: 'Europe/Helsinki', label: 'Europe/Helsinki' }, + { value: 'Europe/Isle_of_Man', label: 'Europe/Isle_of_Man' }, + { value: 'Europe/Istanbul', label: 'Europe/Istanbul' }, + { value: 'Europe/Jersey', label: 'Europe/Jersey' }, + { value: 'Europe/Kaliningrad', label: 'Europe/Kaliningrad' }, + { value: 'Europe/Kirov', label: 'Europe/Kirov' }, + { value: 'Europe/Kyiv', label: 'Europe/Kyiv' }, + { value: 'Europe/Lisbon', label: 'Europe/Lisbon' }, + { value: 'Europe/Ljubljana', label: 'Europe/Ljubljana' }, + { value: 'Europe/London', label: 'Europe/London' }, + { value: 'Europe/Luxembourg', label: 'Europe/Luxembourg' }, + { value: 'Europe/Madrid', label: 'Europe/Madrid' }, + { value: 'Europe/Malta', label: 'Europe/Malta' }, + { value: 'Europe/Mariehamn', label: 'Europe/Mariehamn' }, + { value: 'Europe/Minsk', label: 'Europe/Minsk' }, + { value: 'Europe/Monaco', label: 'Europe/Monaco' }, + { value: 'Europe/Moscow', label: 'Europe/Moscow' }, + { value: 'Europe/Oslo', label: 'Europe/Oslo' }, + { value: 'Europe/Paris', label: 'Europe/Paris' }, + { value: 'Europe/Podgorica', label: 'Europe/Podgorica' }, + { value: 'Europe/Prague', label: 'Europe/Prague' }, + { value: 'Europe/Riga', label: 'Europe/Riga' }, + { value: 'Europe/Rome', label: 'Europe/Rome' }, + { value: 'Europe/Samara', label: 'Europe/Samara' }, + { value: 'Europe/San_Marino', label: 'Europe/San_Marino' }, + { value: 'Europe/Sarajevo', label: 'Europe/Sarajevo' }, + { value: 'Europe/Saratov', label: 'Europe/Saratov' }, + { value: 'Europe/Simferopol', label: 'Europe/Simferopol' }, + { value: 'Europe/Skopje', label: 'Europe/Skopje' }, + { value: 'Europe/Sofia', label: 'Europe/Sofia' }, + { value: 'Europe/Stockholm', label: 'Europe/Stockholm' }, + { value: 'Europe/Tallinn', label: 'Europe/Tallinn' }, + { value: 'Europe/Tirane', label: 'Europe/Tirane' }, + { value: 'Europe/Ulyanovsk', label: 'Europe/Ulyanovsk' }, + { value: 'Europe/Vaduz', label: 'Europe/Vaduz' }, + { value: 'Europe/Vatican', label: 'Europe/Vatican' }, + { value: 'Europe/Vienna', label: 'Europe/Vienna' }, + { value: 'Europe/Vilnius', label: 'Europe/Vilnius' }, + { value: 'Europe/Volgograd', label: 'Europe/Volgograd' }, + { value: 'Europe/Warsaw', label: 'Europe/Warsaw' }, + { value: 'Europe/Zagreb', label: 'Europe/Zagreb' }, + { value: 'Europe/Zurich', label: 'Europe/Zurich' }, + { value: 'Indian/Antananarivo', label: 'Indian/Antananarivo' }, + { value: 'Indian/Chagos', label: 'Indian/Chagos' }, + { value: 'Indian/Christmas', label: 'Indian/Christmas' }, + { value: 'Indian/Cocos', label: 'Indian/Cocos' }, + { value: 'Indian/Comoro', label: 'Indian/Comoro' }, + { value: 'Indian/Kerguelen', label: 'Indian/Kerguelen' }, + { value: 'Indian/Mahe', label: 'Indian/Mahe' }, + { value: 'Indian/Maldives', label: 'Indian/Maldives' }, + { value: 'Indian/Mauritius', label: 'Indian/Mauritius' }, + { value: 'Indian/Mayotte', label: 'Indian/Mayotte' }, + { value: 'Indian/Reunion', label: 'Indian/Reunion' }, + { value: 'Pacific/Apia', label: 'Pacific/Apia' }, + { value: 'Pacific/Auckland', label: 'Pacific/Auckland' }, + { value: 'Pacific/Bougainville', label: 'Pacific/Bougainville' }, + { value: 'Pacific/Chatham', label: 'Pacific/Chatham' }, + { value: 'Pacific/Chuuk', label: 'Pacific/Chuuk' }, + { value: 'Pacific/Easter', label: 'Pacific/Easter' }, + { value: 'Pacific/Efate', label: 'Pacific/Efate' }, + { value: 'Pacific/Fakaofo', label: 'Pacific/Fakaofo' }, + { value: 'Pacific/Fiji', label: 'Pacific/Fiji' }, + { value: 'Pacific/Funafuti', label: 'Pacific/Funafuti' }, + { value: 'Pacific/Galapagos', label: 'Pacific/Galapagos' }, + { value: 'Pacific/Gambier', label: 'Pacific/Gambier' }, + { value: 'Pacific/Guadalcanal', label: 'Pacific/Guadalcanal' }, + { value: 'Pacific/Guam', label: 'Pacific/Guam' }, + { value: 'Pacific/Honolulu', label: 'Pacific/Honolulu' }, + { value: 'Pacific/Kanton', label: 'Pacific/Kanton' }, + { value: 'Pacific/Kiritimati', label: 'Pacific/Kiritimati' }, + { value: 'Pacific/Kosrae', label: 'Pacific/Kosrae' }, + { value: 'Pacific/Kwajalein', label: 'Pacific/Kwajalein' }, + { value: 'Pacific/Majuro', label: 'Pacific/Majuro' }, + { value: 'Pacific/Marquesas', label: 'Pacific/Marquesas' }, + { value: 'Pacific/Midway', label: 'Pacific/Midway' }, + { value: 'Pacific/Nauru', label: 'Pacific/Nauru' }, + { value: 'Pacific/Niue', label: 'Pacific/Niue' }, + { value: 'Pacific/Norfolk', label: 'Pacific/Norfolk' }, + { value: 'Pacific/Noumea', label: 'Pacific/Noumea' }, + { value: 'Pacific/Pago_Pago', label: 'Pacific/Pago_Pago' }, + { value: 'Pacific/Palau', label: 'Pacific/Palau' }, + { value: 'Pacific/Pitcairn', label: 'Pacific/Pitcairn' }, + { value: 'Pacific/Pohnpei', label: 'Pacific/Pohnpei' }, + { value: 'Pacific/Port_Moresby', label: 'Pacific/Port_Moresby' }, + { value: 'Pacific/Rarotonga', label: 'Pacific/Rarotonga' }, + { value: 'Pacific/Saipan', label: 'Pacific/Saipan' }, + { value: 'Pacific/Tahiti', label: 'Pacific/Tahiti' }, + { value: 'Pacific/Tarawa', label: 'Pacific/Tarawa' }, + { value: 'Pacific/Tongatapu', label: 'Pacific/Tongatapu' }, + { value: 'Pacific/Wake', label: 'Pacific/Wake' }, + { value: 'Pacific/Wallis', label: 'Pacific/Wallis' }, + { value: 'UTC', label: 'UTC' }, +]; diff --git a/resources/js/pages/profile/components/delete-user.tsx b/resources/js/pages/profile/components/delete-user.tsx deleted file mode 100644 index 58e08472..00000000 --- a/resources/js/pages/profile/components/delete-user.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useForm } from '@inertiajs/react'; -import { FormEventHandler, useRef } from 'react'; - -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 { 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< - Required<{ - password: string; - }> - >({ password: '' }); - - const deleteUser: FormEventHandler = (e) => { - e.preventDefault(); - - destroy(route('profile.destroy'), { - preserveScroll: true, - onSuccess: () => closeModal(), - onError: () => passwordInput.current?.focus(), - onFinish: () => reset(), - }); - }; - - const closeModal = () => { - clearErrors(); - reset(); - }; - - return ( - - - Delete account - Delete your account and all of its resources - - - -
-
-

Warning

-

Please proceed with caution, this cannot be undone.

-
-
- - - - - - Are you sure you want to delete your account? - - Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your password to confirm you - would like to permanently delete your account. - -
-
- - - setData('password', e.target.value)} - placeholder="Password" - autoComplete="current-password" - /> - - -
- - - - - - - - - -
-
-
-
-
- ); -} diff --git a/resources/js/pages/profile/components/update-password.tsx b/resources/js/pages/profile/components/update-password.tsx index f6dc7b16..b592f49f 100644 --- a/resources/js/pages/profile/components/update-password.tsx +++ b/resources/js/pages/profile/components/update-password.tsx @@ -6,7 +6,9 @@ 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'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Form, FormField, FormFields } from '@/components/ui/form'; +import { CheckIcon, LoaderCircleIcon } from 'lucide-react'; export default function UpdatePassword() { const passwordInput = useRef(null); @@ -44,73 +46,62 @@ export default function UpdatePassword() { 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

-
-
-
+ +
+ + + + 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" + /> + + + +
+ + + + + + ); } diff --git a/resources/js/pages/profile/components/update-user.tsx b/resources/js/pages/profile/components/update-user.tsx index f20eb1bf..3498fa35 100644 --- a/resources/js/pages/profile/components/update-user.tsx +++ b/resources/js/pages/profile/components/update-user.tsx @@ -1,19 +1,21 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Card, CardContent, CardDescription, CardFooter, 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 { 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'; +import { Form, FormField, FormFields } from '@/components/ui/form'; +import { CheckIcon, LoaderCircleIcon } from 'lucide-react'; type ProfileForm = { name: string; email: string; }; -export default function UpdateUser({ mustVerifyEmail, status }: { mustVerifyEmail: boolean; status?: string }) { +export default function UpdateUser() { const { auth } = usePage().props; const { data, setData, patch, errors, processing, recentlySuccessful } = useForm>({ @@ -35,76 +37,46 @@ export default function UpdateUser({ mustVerifyEmail, status }: { mustVerifyEmai 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

-
-
-
+ +
+ + + + setData('name', e.target.value)} + required + autoComplete="name" + placeholder="Full name" + /> + + + + + setData('email', e.target.value)} + required + autoComplete="username" + placeholder="Email address" + /> + + + +
+ + + + + + ); } diff --git a/resources/js/pages/profile/index.tsx b/resources/js/pages/profile/index.tsx index f383fff3..01cb9cea 100644 --- a/resources/js/pages/profile/index.tsx +++ b/resources/js/pages/profile/index.tsx @@ -1,20 +1,18 @@ import { Head } from '@inertiajs/react'; -import DeleteUser from '@/pages/profile/components/delete-user'; import SettingsLayout from '@/layouts/settings/layout'; import Container from '@/components/container'; import UpdatePassword from '@/pages/profile/components/update-password'; import UpdateUser from '@/pages/profile/components/update-user'; import Heading from '@/components/heading'; -export default function Profile({ mustVerifyEmail, status }: { mustVerifyEmail: boolean; status?: string }) { +export default function Profile() { return ( - + - + - ); diff --git a/resources/js/pages/projects/components/actions.tsx b/resources/js/pages/projects/components/actions.tsx index ebe2efc2..71df0d7e 100644 --- a/resources/js/pages/projects/components/actions.tsx +++ b/resources/js/pages/projects/components/actions.tsx @@ -4,6 +4,7 @@ import { MoreVerticalIcon } from 'lucide-react'; import { Project } from '@/types/project'; import DeleteProject from '@/pages/projects/components/delete-project'; import UsersAction from '@/pages/projects/components/users-action'; +import ProjectForm from '@/pages/projects/components/project-form'; export default function ProjectActions({ project }: { project: Project }) { return ( @@ -15,6 +16,9 @@ export default function ProjectActions({ project }: { project: Project }) { + + e.preventDefault()}>Edit + e.preventDefault()}>Users diff --git a/resources/js/pages/projects/components/columns.tsx b/resources/js/pages/projects/components/columns.tsx index 451dfba7..99ba3927 100644 --- a/resources/js/pages/projects/components/columns.tsx +++ b/resources/js/pages/projects/components/columns.tsx @@ -1,11 +1,10 @@ -'use client'; - import { ColumnDef } from '@tanstack/react-table'; import type { Project } from '@/types/project'; import ProjectActions from '@/pages/projects/components/actions'; import { usePage } from '@inertiajs/react'; import { SharedData } from '@/types'; import { Badge } from '@/components/ui/badge'; +import DateTime from '@/components/date-time'; const CurrentProject = ({ project }: { project: Project }) => { const page = usePage(); @@ -34,10 +33,13 @@ export const columns: ColumnDef[] = [ }, }, { - accessorKey: 'created_at_by_timezone', + accessorKey: 'created_at', header: 'Created at', enableColumnFilter: true, enableSorting: true, + cell: ({ row }) => { + return ; + }, }, { id: 'actions', diff --git a/resources/js/pages/projects/components/delete-project.tsx b/resources/js/pages/projects/components/delete-project.tsx index 402cfd34..472102ee 100644 --- a/resources/js/pages/projects/components/delete-project.tsx +++ b/resources/js/pages/projects/components/delete-project.tsx @@ -34,10 +34,11 @@ export default function DeleteProject({ project, children }: { project: Project; Delete {project.name} - Delete project and all its resources. + Delete project and all its resources. -
+ +

Are you sure you want to delete this project? This action cannot be undone.

@@ -49,7 +50,7 @@ export default function DeleteProject({ project, children }: { project: Project; - + -
diff --git a/resources/js/pages/projects/components/users-action.tsx b/resources/js/pages/projects/components/users-action.tsx index bf524d93..38a69383 100644 --- a/resources/js/pages/projects/components/users-action.tsx +++ b/resources/js/pages/projects/components/users-action.tsx @@ -20,17 +20,7 @@ import InputError from '@/components/ui/input-error'; import UserSelect from '@/components/user-select'; import { useForm } from '@inertiajs/react'; import { LoaderCircleIcon, TrashIcon } from 'lucide-react'; -import { - AlertDialog, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, - AlertDialogCancel, - AlertDialogAction, -} from '@/components/ui/alert-dialog'; +import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; function AddUser({ project }: { project: Project }) { const [open, setOpen] = useState(false); @@ -41,7 +31,7 @@ function AddUser({ project }: { project: Project }) { const submit = (e: FormEvent) => { e.preventDefault(); - form.post(route('projects.users', project.id), { + form.post(route('projects.users.store', { project: project.id }), { onSuccess: () => { setOpen(false); }, @@ -51,14 +41,14 @@ function AddUser({ project }: { project: Project }) { return ( - + Add user - Here you can add new user to {project.name} + Here you can add new user to {project.name} -
+ @@ -71,7 +61,10 @@ function AddUser({ project }: { project: Project }) { - +
@@ -81,13 +74,11 @@ function AddUser({ project }: { project: Project }) { function RemoveUser({ project, user }: { project: Project; user: User }) { const [open, setOpen] = useState(false); - const form = useForm({ - user: user.id, - }); + const form = useForm(); const submit = (e: FormEvent) => { e.preventDefault(); - form.delete(route('projects.users', project.id), { + form.delete(route('projects.users.destroy', { project: project.id, user: user.id }), { onSuccess: () => { setOpen(false); }, @@ -95,27 +86,33 @@ function RemoveUser({ project, user }: { project: Project; user: User }) { }; return ( - - + + - - - - Remove user - Remove user from {project.name}. - + + + + Remove user + Remove user from {project.name}. + - - Cancel +

+ Are you sure you want to remove {user.name} from this project? +

+ + + + + -
-
-
+ + + ); } @@ -155,18 +152,20 @@ const getColumns = (project: Project): ColumnDef[] => [ export default function UsersAction({ project, children }: { project: Project; children: ReactNode }) { return ( - - {children} - - - Users - Users assigned to this project - - - - - - - + + {children} + + + Project users + Users assigned to this project + + + +
+ +
+
+
+
); } diff --git a/resources/js/pages/projects/index.tsx b/resources/js/pages/projects/index.tsx index d769ed1f..f91aab94 100644 --- a/resources/js/pages/projects/index.tsx +++ b/resources/js/pages/projects/index.tsx @@ -6,7 +6,7 @@ import { Project } from '@/types/project'; import Container from '@/components/container'; import Heading from '@/components/heading'; import React from 'react'; -import CreateProject from '@/pages/projects/components/create-project'; +import ProjectForm from '@/pages/projects/components/project-form'; import { Button } from '@/components/ui/button'; export default function Projects() { @@ -20,13 +20,13 @@ export default function Projects() { - +
- - - + + +
diff --git a/resources/js/pages/server-logs/components/columns.tsx b/resources/js/pages/server-logs/components/columns.tsx index 3be139fe..e47d1b69 100644 --- a/resources/js/pages/server-logs/components/columns.tsx +++ b/resources/js/pages/server-logs/components/columns.tsx @@ -1,13 +1,12 @@ -'use client'; - import { ColumnDef, Row } from '@tanstack/react-table'; import { Button } from '@/components/ui/button'; import { EyeIcon, LoaderCircleIcon } from 'lucide-react'; import type { ServerLog } from '@/types/server-log'; -import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; import { useState } from 'react'; import axios from 'axios'; import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; +import DateTime from '@/components/date-time'; const LogActionCell = ({ row }: { row: Row }) => { const [open, setOpen] = useState(false); @@ -40,15 +39,18 @@ const LogActionCell = ({ row }: { row: Row }) => { {loading ? : } - + View Log - This is all content of the log + This is all content of the log - + {content} + + + @@ -62,9 +64,12 @@ export const columns: ColumnDef[] = [ enableColumnFilter: true, }, { - accessorKey: 'created_at_by_timezone', + accessorKey: 'created_at', header: 'Created At', enableSorting: true, + cell: ({ row }) => { + return ; + }, }, { id: 'actions', diff --git a/resources/js/pages/server-providers/components/create-server-provider.tsx b/resources/js/pages/server-providers/components/create-server-provider.tsx index cc9ecdd1..89944504 100644 --- a/resources/js/pages/server-providers/components/create-server-provider.tsx +++ b/resources/js/pages/server-providers/components/create-server-provider.tsx @@ -81,7 +81,7 @@ export default function CreateServerProvider({ Connect Connect to a new server provider - + diff --git a/resources/js/pages/servers/components/columns.tsx b/resources/js/pages/servers/components/columns.tsx index f27fae06..88df930f 100644 --- a/resources/js/pages/servers/components/columns.tsx +++ b/resources/js/pages/servers/components/columns.tsx @@ -1,11 +1,10 @@ -'use client'; - import { ColumnDef } from '@tanstack/react-table'; import { Server } from '@/types/server'; import { Link } from '@inertiajs/react'; import { Button } from '@/components/ui/button'; import { EyeIcon } from 'lucide-react'; import ServerStatus from '@/pages/servers/components/status'; +import DateTime from '@/components/date-time'; export const columns: ColumnDef[] = [ { @@ -27,6 +26,15 @@ export const columns: ColumnDef[] = [ enableColumnFilter: true, enableSorting: true, }, + { + accessorKey: 'created_at', + header: 'Created at', + enableColumnFilter: true, + enableSorting: true, + cell: ({ row }) => { + return ; + }, + }, { accessorKey: 'status', header: 'Status', @@ -43,7 +51,7 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => { return (
- + diff --git a/resources/js/pages/servers/components/delete-server.tsx b/resources/js/pages/servers/components/delete-server.tsx index cee23362..17c82f20 100644 --- a/resources/js/pages/servers/components/delete-server.tsx +++ b/resources/js/pages/servers/components/delete-server.tsx @@ -37,7 +37,7 @@ export default function DeleteServer({ server, children }: { server: Server; chi Delete server and its resources. - + @@ -49,7 +49,7 @@ export default function DeleteServer({ server, children }: { server: Server; chi - + +
diff --git a/resources/js/pages/servers/installing.tsx b/resources/js/pages/servers/installing.tsx index e6fe18e3..923e924a 100644 --- a/resources/js/pages/servers/installing.tsx +++ b/resources/js/pages/servers/installing.tsx @@ -14,7 +14,7 @@ export default function InstallingServer() { }>(); return ( - + {' '} ); diff --git a/resources/js/pages/servers/overview.tsx b/resources/js/pages/servers/overview.tsx index 51b00167..759119c8 100644 --- a/resources/js/pages/servers/overview.tsx +++ b/resources/js/pages/servers/overview.tsx @@ -15,7 +15,7 @@ export default function ServerOverview() { }>(); return ( - + diff --git a/resources/js/pages/users/components/actions.tsx b/resources/js/pages/users/components/actions.tsx new file mode 100644 index 00000000..6592f1ec --- /dev/null +++ b/resources/js/pages/users/components/actions.tsx @@ -0,0 +1,34 @@ +import { User } from '@/types/user'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { MoreVerticalIcon } from 'lucide-react'; +import DeleteUser from '@/pages/users/components/delete-user'; +import Projects from '@/pages/users/components/projects'; +import UserForm from '@/pages/users/components/form'; + +export default function UserActions({ user }: { user: User }) { + return ( + + + + + + + e.preventDefault()}>Edit + + + e.preventDefault()}>Projects + + + + e.preventDefault()} variant="destructive"> + Delete + + + + + ); +} diff --git a/resources/js/pages/users/components/delete-user.tsx b/resources/js/pages/users/components/delete-user.tsx new file mode 100644 index 00000000..668bf4e7 --- /dev/null +++ b/resources/js/pages/users/components/delete-user.tsx @@ -0,0 +1,58 @@ +import { User } from '@/types/user'; +import { FormEvent, ReactNode, useState } from 'react'; +import { useForm } from '@inertiajs/react'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { LoaderCircleIcon } from 'lucide-react'; + +export default function DeleteUser({ user, children }: { user: User; children: ReactNode }) { + const [open, setOpen] = useState(false); + const form = useForm(); + + const submit = (e: FormEvent) => { + e.preventDefault(); + form.delete(route('users.destroy', user.id), { + onSuccess: () => { + setOpen(false); + }, + }); + }; + return ( + + {children} + + + Delete user + + Delete {user.name}[{user.email}] + + +

+ Are you sure you want to delete{' '} + + {user.name} [{user.email}] + + ? This action cannot be undone. +

+ + + + + + +
+
+ ); +} diff --git a/resources/js/pages/users/components/form.tsx b/resources/js/pages/users/components/form.tsx new file mode 100644 index 00000000..dbcbb3da --- /dev/null +++ b/resources/js/pages/users/components/form.tsx @@ -0,0 +1,125 @@ +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { FormEventHandler, ReactNode, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { CheckIcon, LoaderCircle } from 'lucide-react'; +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 { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { User } from '@/types/user'; +import { Transition } from '@headlessui/react'; + +export default function UserForm({ user, children }: { user?: User; children: ReactNode }) { + const [open, setOpen] = useState(false); + + const form = useForm({ + name: user?.name || '', + email: user?.email || '', + password: '', + role: user?.role || 'user', + }); + + const submit: FormEventHandler = (e) => { + e.preventDefault(); + + if (user) { + form.patch(route('users.update', user.id)); + return; + } + + form.post(route('users.store'), { + onSuccess() { + setOpen(false); + }, + }); + }; + + return ( + + {children} + + + {user ? `Edit ${user.name}` : 'Create user'} + + {user ? `Fill the form to edit ${user.name}` : 'Fill the form to create a new user'} + + + + + + + form.setData('name', e.target.value)} /> + + + + + form.setData('email', e.target.value)} /> + + + + + form.setData('password', e.target.value)} + /> + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/resources/js/pages/users/components/list.tsx b/resources/js/pages/users/components/list.tsx new file mode 100644 index 00000000..3234749a --- /dev/null +++ b/resources/js/pages/users/components/list.tsx @@ -0,0 +1,47 @@ +import { ColumnDef } from '@tanstack/react-table'; +import type { User } from '@/types/user'; +import { DataTable } from '@/components/data-table'; +import { usePage } from '@inertiajs/react'; +import UserActions from '@/pages/users/components/actions'; +import DateTime from '@/components/date-time'; + +const columns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Name', + enableColumnFilter: true, + }, + { + accessorKey: 'email', + header: 'Email', + enableColumnFilter: true, + }, + { + accessorKey: 'created_at', + header: 'Created At', + enableSorting: true, + cell: ({ row }) => , + }, + { + id: 'actions', + enableColumnFilter: false, + enableSorting: false, + cell: ({ row }) => ( +
+ +
+ ), + }, +]; + +type Page = { + users: { + data: User[]; + }; +}; + +export default function UsersList() { + const page = usePage(); + + return ; +} diff --git a/resources/js/pages/users/components/projects.tsx b/resources/js/pages/users/components/projects.tsx new file mode 100644 index 00000000..6d814b39 --- /dev/null +++ b/resources/js/pages/users/components/projects.tsx @@ -0,0 +1,176 @@ +import { User } from '@/types/user'; +import { FormEvent, ReactNode, useState } from 'react'; +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { DialogTrigger } from '@radix-ui/react-dialog'; +import { ColumnDef } from '@tanstack/react-table'; +import type { Project } from '@/types/project'; +import { DataTable } from '@/components/data-table'; +import { useForm, usePage } from '@inertiajs/react'; +import { Button } from '@/components/ui/button'; +import { LoaderCircleIcon, TrashIcon } from 'lucide-react'; +import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; +import { Form, FormField, FormFields } from '@/components/ui/form'; +import { Label } from '@/components/ui/label'; +import InputError from '@/components/ui/input-error'; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { SharedData } from '@/types'; + +function RemoveProject({ user, project }: { user: User; project: Project }) { + const [open, setOpen] = useState(false); + const form = useForm(); + + const submit = () => { + form.delete(route('users.projects.destroy', { user: user.id, project: project.id }), { + preserveScroll: true, + onSuccess: () => { + form.reset(); + }, + }); + }; + + return ( + + + + + + + Remove from project + + Remove {user.name} from {project.name} + + +

+ Are you sure you want to remove {user.name} from {project.name}? +

+ + + + + + +
+
+ ); +} + +function AddToProject({ user }: { user: User }) { + const page = usePage(); + const [open, setOpen] = useState(false); + const form = useForm({ + project: '', + }); + + const submit = (e: FormEvent) => { + e.preventDefault(); + form.post(route('users.projects.store', { user: user.id }), { + onSuccess: () => { + setOpen(false); + }, + }); + }; + + return ( + + + + + + + Add to project + + Add {user.name} to a project + + +
+ + + + + + + +
+ + + + + + +
+
+ ); +} + +const columns = (user: User): ColumnDef[] => { + return [ + { + accessorKey: 'id', + header: 'ID', + enableColumnFilter: true, + enableSorting: true, + enableHiding: true, + }, + { + accessorKey: 'name', + header: 'Name', + enableColumnFilter: true, + enableSorting: true, + }, + { + id: 'actions', + enableColumnFilter: false, + enableSorting: false, + cell: ({ row }) => { + return ( +
+ +
+ ); + }, + }, + ]; +}; + +export default function Projects({ user, children }: { user: User; children: ReactNode }) { + return ( + + {children} + + + Projects + + All projects that {user.name} has access + + + {user.projects && } + +
+ +
+
+
+
+ ); +} diff --git a/resources/js/pages/users/index.tsx b/resources/js/pages/users/index.tsx new file mode 100644 index 00000000..e8bf5730 --- /dev/null +++ b/resources/js/pages/users/index.tsx @@ -0,0 +1,25 @@ +import SettingsLayout from '@/layouts/settings/layout'; +import { Head } from '@inertiajs/react'; +import Container from '@/components/container'; +import Heading from '@/components/heading'; +import UsersList from '@/pages/users/components/list'; +import { Button } from '@/components/ui/button'; +import UserForm from '@/pages/users/components/form'; + +export default function Users() { + return ( + + + + +
+ + + + +
+ +
+
+ ); +} diff --git a/resources/js/types/project.d.ts b/resources/js/types/project.d.ts index ea204888..9894558c 100644 --- a/resources/js/types/project.d.ts +++ b/resources/js/types/project.d.ts @@ -6,7 +6,5 @@ export interface Project { users: User[]; created_at: string; updated_at: string; - created_at_by_timezone: string; - updated_at_by_timezone: string; [key: string]: unknown; } diff --git a/resources/js/types/server-log.d.ts b/resources/js/types/server-log.d.ts index 1bfb668f..1acf5d82 100644 --- a/resources/js/types/server-log.d.ts +++ b/resources/js/types/server-log.d.ts @@ -8,8 +8,6 @@ export interface ServerLog { is_remote: boolean; created_at: string; updated_at: string; - created_at_by_timezone: string; - updated_at_by_timezone: string; [key: string]: unknown; } diff --git a/resources/js/types/server.d.ts b/resources/js/types/server.d.ts index 57d1f97f..7b9456c0 100644 --- a/resources/js/types/server.d.ts +++ b/resources/js/types/server.d.ts @@ -25,8 +25,6 @@ export interface Server { last_update_check?: string; created_at: string; updated_at: string; - created_at_by_timezone: string; - updated_at_by_timezone: string; status_color: 'gray' | 'success' | 'info' | 'warning' | 'danger'; [key: string]: unknown; } diff --git a/resources/js/types/user.d.ts b/resources/js/types/user.d.ts index 9fcf6ffc..b4e9a83f 100644 --- a/resources/js/types/user.d.ts +++ b/resources/js/types/user.d.ts @@ -1,3 +1,5 @@ +import { Project } from '@/types/project'; + export interface User { id: number; name: string; @@ -6,5 +8,8 @@ export interface User { email_verified_at: string | null; created_at: string; updated_at: string; + timezone: string; + projects?: Project[]; + role: string; [key: string]: unknown; // This allows for additional properties... } diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 4658432f..e5e542dc 100644 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -44,7 +44,7 @@ @vite(['resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"]) @inertiaHead - + @inertia diff --git a/tests/Feature/ProfileTest.php b/tests/Feature/ProfileTest.php index f1ede954..0888b98c 100644 --- a/tests/Feature/ProfileTest.php +++ b/tests/Feature/ProfileTest.php @@ -2,12 +2,9 @@ namespace Tests\Feature; -use App\Web\Pages\Settings\Profile\Index; -use App\Web\Pages\Settings\Profile\Widgets\ProfileInformation; -use App\Web\Pages\Settings\Profile\Widgets\UpdatePassword; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Hash; -use Livewire\Livewire; +use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; class ProfileTest extends TestCase @@ -19,44 +16,38 @@ public function test_profile_page_is_displayed(): void $this->actingAs($this->user); $this - ->get(Index::getUrl()) + ->get(route('profile')) ->assertSuccessful() - ->assertSee('Profile Information') - ->assertSee('Update Password') - ->assertSee('Browser Sessions') - ->assertSee('Two Factor Authentication'); + ->assertInertia(fn (Assert $page) => $page->component('profile/index')); } public function test_profile_information_can_be_updated(): void { $this->actingAs($this->user); - Livewire::test(ProfileInformation::class) - ->fill([ - 'name' => 'Test', - 'email' => 'test@example.com', - 'timezone' => 'Europe/Berlin', - ]) - ->call('submit'); + $this->patch(route('profile.update'), [ + 'name' => 'Test', + 'email' => 'test@example.com', + ]) + ->assertRedirect(route('profile')); $this->user->refresh(); $this->assertSame('Test', $this->user->name); $this->assertSame('test@example.com', $this->user->email); - $this->assertSame('Europe/Berlin', $this->user->timezone); } public function test_password_can_be_updated(): void { $this->actingAs($this->user); - Livewire::test(UpdatePassword::class) - ->fill([ - 'current_password' => 'password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]) - ->call('submit'); + $this->put(route('profile.password'), [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]) + ->assertRedirect(route('profile')) + ->assertSessionDoesntHaveErrors(); $this->assertTrue(Hash::check('new-password', $this->user->refresh()->password)); } @@ -64,14 +55,11 @@ public function test_password_can_be_updated(): void public function test_correct_password_must_be_provided_to_update_password(): void { $this->actingAs($this->user); - - Livewire::test(UpdatePassword::class) - ->fill([ - 'current_password' => 'wrong-password', - 'password' => 'new-password', - 'password_confirmation' => 'new-password', - ]) - ->call('submit') - ->assertHasErrors('current_password'); + $this->put(route('profile.password'), [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]) + ->assertSessionHasErrors('current_password'); } } diff --git a/tests/Feature/ProjectsTest.php b/tests/Feature/ProjectsTest.php index 176abb63..d150c4f2 100644 --- a/tests/Feature/ProjectsTest.php +++ b/tests/Feature/ProjectsTest.php @@ -3,11 +3,8 @@ namespace Tests\Feature; use App\Models\Project; -use App\Web\Pages\Settings\Projects\Index; -use App\Web\Pages\Settings\Projects\Settings; -use App\Web\Pages\Settings\Projects\Widgets\UpdateProject; use Illuminate\Foundation\Testing\RefreshDatabase; -use Livewire\Livewire; +use Inertia\Testing\AssertableInertia; use Tests\TestCase; class ProjectsTest extends TestCase @@ -18,15 +15,17 @@ public function test_create_project(): void { $this->actingAs($this->user); - Livewire::test(Index::class) - ->callAction('create', [ - 'name' => 'test', - ]) - ->assertSuccessful(); + $this->post(route('projects.store'), [ + 'name' => 'create-project-test', + ]) + ->assertSessionDoesntHaveErrors() + ->assertRedirect(route('projects')); $this->assertDatabaseHas('projects', [ - 'name' => 'test', + 'name' => 'create-project-test', ]); + + $this->assertEquals($this->user->refresh()->current_project_id, Project::query()->where('name', 'create-project-test')->first()->id); } public function test_see_projects_list(): void @@ -37,9 +36,10 @@ public function test_see_projects_list(): void $this->user->projects()->attach($project); - $this->get(Index::getUrl()) + $this->get(route('projects')) ->assertSuccessful() - ->assertSee($project->name); + ->assertInertia(fn (AssertableInertia $page) => $page->component('projects/index')); + } public function test_delete_project(): void @@ -50,11 +50,11 @@ public function test_delete_project(): void $this->user->projects()->attach($project); - Livewire::test(Settings::class, [ - 'project' => $project, + $this->delete(route('projects.destroy', $project), [ + 'name' => $project->name, ]) - ->callAction('delete') - ->assertSuccessful(); + ->assertSessionDoesntHaveErrors() + ->assertRedirect(route('projects')); $this->assertDatabaseMissing('projects', [ 'id' => $project->id, @@ -69,14 +69,11 @@ public function test_edit_project(): void $this->user->projects()->attach($project); - Livewire::test(UpdateProject::class, [ - 'project' => $project, + $this->patch(route('projects.update', $project), [ + 'name' => 'new-name', ]) - ->fill([ - 'name' => 'new-name', - ]) - ->call('submit') - ->assertSuccessful(); + ->assertSessionDoesntHaveErrors() + ->assertRedirect(route('projects')); $this->assertDatabaseHas('projects', [ 'id' => $project->id, @@ -88,11 +85,12 @@ public function test_cannot_delete_last_project(): void { $this->actingAs($this->user); - Livewire::test(Settings::class, [ - 'project' => $this->user->currentProject, + $this->delete(route('projects.destroy', $this->user->currentProject->id), [ + 'name' => $this->user->currentProject->name, ]) - ->callAction('delete') - ->assertNotified('Cannot delete the last project.'); + ->assertSessionHasErrors([ + 'name' => 'Cannot delete the last project.', + ]); $this->assertDatabaseHas('projects', [ 'id' => $this->user->currentProject->id, diff --git a/tests/Feature/UserTest.php b/tests/Feature/UserTest.php index 00088d94..e60b517c 100644 --- a/tests/Feature/UserTest.php +++ b/tests/Feature/UserTest.php @@ -5,10 +5,8 @@ use App\Enums\UserRole; use App\Models\Project; use App\Models\User; -use App\Web\Pages\Settings\Users\Index; -use App\Web\Pages\Settings\Users\Widgets\UsersList; use Illuminate\Foundation\Testing\RefreshDatabase; -use Livewire\Livewire; +use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; class UserTest extends TestCase @@ -19,13 +17,14 @@ public function test_create_user(): void { $this->actingAs($this->user); - Livewire::test(Index::class) - ->callAction('create', [ - 'name' => 'new user', - 'email' => 'newuser@example.com', - 'password' => 'password', - 'role' => UserRole::USER, - ]); + $this->post(route('users.store'), [ + 'name' => 'new user', + 'email' => 'newuser@example.com', + 'password' => 'password', + 'role' => UserRole::USER, + ]) + ->assertSessionDoesntHaveErrors() + ->assertRedirect(route('users')); $this->assertDatabaseHas('users', [ 'name' => 'new user', @@ -38,13 +37,11 @@ public function test_see_users_list(): void { $this->actingAs($this->user); - $user = User::factory()->create(); + User::factory()->create(); - $this->get(Index::getUrl()) - ->assertSuccessful(); - - Livewire::test(UsersList::class) - ->assertCanSeeTableRecords([$user]); + $this->get(route('users')) + ->assertSuccessful() + ->assertInertia(fn (Assert $page) => $page->component('users/index')); } public function test_must_be_admin_to_see_users_list(): void @@ -54,7 +51,7 @@ public function test_must_be_admin_to_see_users_list(): void $this->actingAs($this->user); - $this->get(Index::getUrl()) + $this->get(route('users')) ->assertForbidden(); } @@ -64,8 +61,9 @@ public function test_delete_user(): void $user = User::factory()->create(); - Livewire::test(UsersList::class) - ->callTableAction('delete', $user); + $this->delete(route('users.destroy', $user)) + ->assertSessionDoesntHaveErrors() + ->assertRedirect(route('users')); $this->assertDatabaseMissing('users', [ 'id' => $user->id, @@ -76,8 +74,8 @@ public function test_cannot_delete_yourself(): void { $this->actingAs($this->user); - Livewire::test(UsersList::class) - ->assertTableActionHidden('delete', $this->user); + $this->delete(route('users.destroy', $this->user)) + ->assertForbidden(); } public function test_edit_user_info(): void @@ -86,36 +84,33 @@ public function test_edit_user_info(): void $user = User::factory()->create(); - Livewire::test(UsersList::class) - ->callTableAction('edit', $user, [ - 'name' => 'new-name', - 'email' => 'newemail@example.com', - 'timezone' => 'Europe/London', - 'role' => UserRole::ADMIN, - ]) - ->assertSuccessful(); + $this->patch(route('users.update', $user), [ + 'name' => 'new-name', + 'email' => 'newemail@example.com', + 'role' => UserRole::ADMIN, + ]) + ->assertRedirect(route('users')); $this->assertDatabaseHas('users', [ 'id' => $user->id, 'name' => 'new-name', 'email' => 'newemail@example.com', - 'timezone' => 'Europe/London', 'role' => UserRole::ADMIN, ]); } - public function test_edit_user_projects(): void + public function test_add_user_to_project(): void { $this->actingAs($this->user); $user = User::factory()->create(); $project = Project::factory()->create(); - Livewire::test(UsersList::class) - ->callTableAction('update-projects', $user, [ - 'projects' => [$project->id], - ]) - ->assertSuccessful(); + $this->post(route('users.projects.store', $user), [ + 'project' => $project->id, + ]) + ->assertSessionDoesntHaveErrors() + ->assertRedirect(route('users')); $this->assertDatabaseHas('user_project', [ 'user_id' => $user->id, @@ -123,32 +118,22 @@ public function test_edit_user_projects(): void ]); } - public function test_edit_user_projects_with_current_project(): void + public function test_remove_user_from_project(): void { $this->actingAs($this->user); - /** @var User $user */ $user = User::factory()->create(); - $user->current_project_id = null; - $user->save(); - - /** @var Project $project */ $project = Project::factory()->create(); - Livewire::test(UsersList::class) - ->callTableAction('update-projects', $user, [ - 'projects' => [$project->id], - ]) - ->assertSuccessful(); + $user->projects()->attach($project); - $this->assertDatabaseHas('user_project', [ + $this->delete(route('users.projects.destroy', [$user, $project])) + ->assertSessionDoesntHaveErrors() + ->assertRedirect(route('users')); + + $this->assertDatabaseMissing('user_project', [ 'user_id' => $user->id, 'project_id' => $project->id, ]); - - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'current_project_id' => $project->id, - ]); } }