mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-01 05:56:16 +00:00
#591 - profile, users and projects
This commit is contained in:
@ -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<string, mixed> $input
|
||||
*/
|
||||
private function validate(array $input): void
|
||||
{
|
||||
Validator::make($input, self::rules())->validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
|
@ -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]),
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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')),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
10
package-lock.json
generated
10
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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() {
|
||||
<header className="bg-background -ml-1 flex h-12 shrink-0 items-center justify-between gap-2 border-b p-4 md:-ml-2">
|
||||
<div className="flex items-center">
|
||||
<SidebarTrigger className="-ml-1 md:hidden" />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<ProjectSwitch />
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator>
|
||||
<SlashIcon />
|
||||
</BreadcrumbSeparator>
|
||||
<BreadcrumbItem>
|
||||
<ServerSwitch />
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<div className="flex items-center space-x-2 text-xs">
|
||||
<ProjectSwitch />
|
||||
<SlashIcon className="size-3" />
|
||||
<ServerSwitch />
|
||||
</div>
|
||||
</div>
|
||||
<AppCommand />
|
||||
</header>
|
||||
|
14
resources/js/components/app-logo-icon-html.tsx
Normal file
14
resources/js/components/app-logo-icon-html.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export default function AppLogoIconHtml({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-primary dark:bg-primary/60 from-primary to-primary/80 flex size-7 items-center justify-center rounded-md border bg-gradient-to-br font-bold text-white! shadow-xs dark:from-transparent dark:to-transparent',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
V
|
||||
</div>
|
||||
);
|
||||
}
|
@ -4,7 +4,7 @@ export default function AppLogo() {
|
||||
return (
|
||||
<>
|
||||
<div className="flex aspect-square size-8 items-center justify-center rounded-md">
|
||||
<AppLogoIcon className="size-7 rounded-sm fill-current text-white dark:text-black" />
|
||||
<AppLogoIcon />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -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<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
className?: string;
|
||||
modal?: boolean;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
|
||||
export function DataTable<TData, TValue>({ columns, data, className, modal }: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
const extraClasses = modal && 'border-none shadow-none';
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<div className={cn('rounded-md border shadow-xs', className, extraClasses)}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
|
9
resources/js/components/date-time.tsx
Normal file
9
resources/js/components/date-time.tsx
Normal file
@ -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 (
|
||||
<time dateTime={date} className={className}>
|
||||
{moment(date).format(format)}
|
||||
</time>
|
||||
);
|
||||
}
|
@ -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<SharedData>();
|
||||
@ -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() {
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<CreateProject>
|
||||
<ProjectForm>
|
||||
<DropdownMenuItem className="gap-0" asChild onSelect={(e) => e.preventDefault()}>
|
||||
<div className="flex items-center">
|
||||
<PlusIcon size={5} />
|
||||
<span className="ml-2">Create new project</span>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</CreateProject>
|
||||
</ProjectForm>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
@ -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<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
@ -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',
|
||||
|
@ -3,13 +3,11 @@ import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div data-slot="card" className={cn('bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm', className)} {...props} />
|
||||
);
|
||||
return <div data-slot="card" className={cn('bg-card text-card-foreground flex flex-col rounded-xl border shadow-xs', className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="card-header" className={cn('flex flex-col gap-1.5 px-6', className)} {...props} />;
|
||||
return <div data-slot="card-header" className={cn('flex flex-col gap-1.5 border-b p-4', className)} {...props} />;
|
||||
}
|
||||
|
||||
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 <div data-slot="card-content" className={cn('px-6', className)} {...props} />;
|
||||
return <div data-slot="card-content" className={cn('', className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="card-footer" className={cn('flex items-center px-6', className)} {...props} />;
|
||||
return <div data-slot="card-footer" className={cn('flex items-center border-t p-4', className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
|
55
resources/js/components/ui/combobox.tsx
Normal file
55
resources/js/components/ui/combobox.tsx
Normal file
@ -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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" role="combobox" aria-expanded={open} className="w-[400px] justify-between">
|
||||
{value ? items.find((item) => item.value === value)?.label : 'Select item...'}
|
||||
<ChevronsUpDown className="opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search item..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No item found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{open &&
|
||||
items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
onSelect={(currentValue) => {
|
||||
value = currentValue;
|
||||
onValueChange(value);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
<Check className={cn('ml-auto', value === item.value ? 'opacity-100' : 'opacity-0')} />
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
@ -25,7 +25,7 @@ function DialogOverlay({ className, ...props }: React.ComponentProps<typeof Dial
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/40 backdrop-blur-xs',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -40,7 +40,7 @@ function DialogContent({ className, children, ...props }: React.ComponentProps<t
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg',
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] rounded-lg border shadow-lg duration-200 sm:max-w-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -56,11 +56,13 @@ function DialogContent({ className, children, ...props }: React.ComponentProps<t
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="dialog-header" className={cn('flex flex-col gap-2 text-center sm:text-left', className)} {...props} />;
|
||||
return <div data-slot="dialog-header" className={cn('flex flex-col gap-2 border-b p-4 text-center sm:text-left', className)} {...props} />;
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="dialog-footer" className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)} {...props} />;
|
||||
return (
|
||||
<div data-slot="dialog-footer" className={cn('flex flex-col-reverse gap-2 border-t p-4 sm:flex-row sm:justify-end', className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
|
@ -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,
|
||||
|
@ -21,7 +21,7 @@ function SelectTrigger({ className, children, ...props }: React.ComponentProps<t
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
className={cn(
|
||||
"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 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",
|
||||
"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}
|
||||
|
@ -1,5 +1,3 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import * as SeparatorPrimitive from '@radix-ui/react-separator';
|
||||
|
||||
|
@ -25,7 +25,7 @@ function SheetOverlay({ className, ...props }: React.ComponentProps<typeof Sheet
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/20 backdrop-blur-xs',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -47,7 +47,7 @@ function SheetContent({
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
|
||||
side === 'right' &&
|
||||
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
|
||||
side === 'left' &&
|
||||
@ -69,11 +69,11 @@ function SheetContent({
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="sheet-header" className={cn('flex flex-col gap-1.5 p-4', className)} {...props} />;
|
||||
return <div data-slot="sheet-header" className={cn('flex flex-col gap-1.5 border-b p-4', className)} {...props} />;
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return <div data-slot="sheet-footer" className={cn('mt-auto flex flex-col gap-2 p-4', className)} {...props} />;
|
||||
return <div data-slot="sheet-footer" className={cn('mt-auto flex flex-col gap-2 border-t p-4', className)} {...props} />;
|
||||
}
|
||||
|
||||
function SheetTitle({ className, ...props }: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
|
@ -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: {
|
||||
|
@ -33,7 +33,7 @@ function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
'text-foreground h-10 px-3 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[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 (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn('p-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[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}
|
||||
/>
|
||||
);
|
||||
|
@ -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<typeof TooltipPrimitive.Provider>) {
|
||||
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />;
|
||||
}
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
function TooltipContent({ className, sideOffset = 0, children, ...props }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-[--radix-tooltip-content-transform-origin] overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
@ -14,7 +14,7 @@ export default function UserSelect({ onChange }: { onChange: (selectedUser: User
|
||||
const [value, setValue] = useState<string>();
|
||||
|
||||
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[]);
|
||||
|
@ -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'),
|
||||
|
420
resources/js/lib/timezones.ts
Normal file
420
resources/js/lib/timezones.ts
Normal file
@ -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' },
|
||||
];
|
@ -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<HTMLInputElement>(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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Delete account</CardTitle>
|
||||
<CardDescription>Delete your account and all of its resources</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
|
||||
<div className="relative space-y-0.5 text-red-600 dark:text-red-100">
|
||||
<p className="font-medium">Warning</p>
|
||||
<p className="text-sm">Please proceed with caution, this cannot be undone.</p>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive">Delete account</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogTitle>Are you sure you want to delete your account?</DialogTitle>
|
||||
<DialogDescription>
|
||||
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.
|
||||
</DialogDescription>
|
||||
<form className="space-y-6" onSubmit={deleteUser}>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password" className="sr-only">
|
||||
Password
|
||||
</Label>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
ref={passwordInput}
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="Password"
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary" onClick={closeModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button variant="destructive" disabled={processing} asChild>
|
||||
<button type="submit">Delete account</button>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
@ -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<HTMLInputElement>(null);
|
||||
@ -44,73 +46,62 @@ export default function UpdatePassword() {
|
||||
<CardTitle>Update password</CardTitle>
|
||||
<CardDescription>Ensure your account is using a long, random password to stay secure.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={updatePassword} className="space-y-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="current_password">Current password</Label>
|
||||
|
||||
<Input
|
||||
id="current_password"
|
||||
ref={currentPasswordInput}
|
||||
value={data.current_password}
|
||||
onChange={(e) => setData('current_password', e.target.value)}
|
||||
type="password"
|
||||
className="mt-1 block w-full"
|
||||
autoComplete="current-password"
|
||||
placeholder="Current password"
|
||||
/>
|
||||
|
||||
<InputError message={errors.current_password} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">New password</Label>
|
||||
|
||||
<Input
|
||||
id="password"
|
||||
ref={passwordInput}
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
type="password"
|
||||
className="mt-1 block w-full"
|
||||
autoComplete="new-password"
|
||||
placeholder="New password"
|
||||
/>
|
||||
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password_confirmation">Confirm password</Label>
|
||||
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
value={data.password_confirmation}
|
||||
onChange={(e) => setData('password_confirmation', e.target.value)}
|
||||
type="password"
|
||||
className="mt-1 block w-full"
|
||||
autoComplete="new-password"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
|
||||
<InputError message={errors.password_confirmation} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button disabled={processing}>Save password</Button>
|
||||
|
||||
<Transition
|
||||
show={recentlySuccessful}
|
||||
enter="transition ease-in-out"
|
||||
enterFrom="opacity-0"
|
||||
leave="transition ease-in-out"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<p className="text-sm text-neutral-600">Saved</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</form>
|
||||
<CardContent className="p-4">
|
||||
<Form id="update-password-form" onSubmit={updatePassword}>
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="current_password">Current password</Label>
|
||||
<Input
|
||||
id="current_password"
|
||||
ref={currentPasswordInput}
|
||||
value={data.current_password}
|
||||
onChange={(e) => setData('current_password', e.target.value)}
|
||||
type="password"
|
||||
className="mt-1 block w-full"
|
||||
autoComplete="current-password"
|
||||
placeholder="Current password"
|
||||
/>
|
||||
<InputError message={errors.current_password} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="password">New password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
ref={passwordInput}
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
type="password"
|
||||
className="mt-1 block w-full"
|
||||
autoComplete="new-password"
|
||||
placeholder="New password"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="password_confirmation">Confirm password</Label>
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
value={data.password_confirmation}
|
||||
onChange={(e) => setData('password_confirmation', e.target.value)}
|
||||
type="password"
|
||||
className="mt-1 block w-full"
|
||||
autoComplete="new-password"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<InputError message={errors.password_confirmation} />
|
||||
</FormField>
|
||||
</FormFields>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button form="update-password-form" disabled={processing}>
|
||||
{processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Save password
|
||||
</Button>
|
||||
<Transition show={recentlySuccessful} enter="transition ease-in-out" enterFrom="opacity-0" leave="transition ease-in-out" leaveTo="opacity-0">
|
||||
<CheckIcon className="text-success" />
|
||||
</Transition>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
@ -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<SharedData>().props;
|
||||
|
||||
const { data, setData, patch, errors, processing, recentlySuccessful } = useForm<Required<ProfileForm>>({
|
||||
@ -35,76 +37,46 @@ export default function UpdateUser({ mustVerifyEmail, status }: { mustVerifyEmai
|
||||
<CardTitle>Profile information</CardTitle>
|
||||
<CardDescription>Update your profile information and email address.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={submit} className="space-y-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name</Label>
|
||||
|
||||
<Input
|
||||
id="name"
|
||||
className="mt-1 block w-full"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
required
|
||||
autoComplete="name"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
|
||||
<InputError className="mt-2" message={errors.name} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
className="mt-1 block w-full"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
required
|
||||
autoComplete="username"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
|
||||
<InputError className="mt-2" message={errors.email} />
|
||||
</div>
|
||||
|
||||
{mustVerifyEmail && auth.user.email_verified_at === null && (
|
||||
<div>
|
||||
<p className="text-muted-foreground -mt-4 text-sm">
|
||||
Your email address is unverified.{' '}
|
||||
<Link
|
||||
href={route('verification.send')}
|
||||
method="post"
|
||||
as="button"
|
||||
className="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||
>
|
||||
Click here to resend the verification email.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
{status === 'verification-link-sent' && (
|
||||
<div className="mt-2 text-sm font-medium text-green-600">A new verification link has been sent to your email address.</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button disabled={processing}>Save</Button>
|
||||
|
||||
<Transition
|
||||
show={recentlySuccessful}
|
||||
enter="transition ease-in-out"
|
||||
enterFrom="opacity-0"
|
||||
leave="transition ease-in-out"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<p className="text-sm text-neutral-600">Saved</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</form>
|
||||
<CardContent className="p-4">
|
||||
<Form id="update-profile-form" onSubmit={submit}>
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={data.name}
|
||||
onChange={(e) => setData('name', e.target.value)}
|
||||
required
|
||||
autoComplete="name"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<InputError message={errors.name} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
required
|
||||
autoComplete="username"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
</FormField>
|
||||
</FormFields>
|
||||
</Form>
|
||||
</CardContent>
|
||||
<CardFooter className="gap-2">
|
||||
<Button form="update-profile-form" disabled={processing}>
|
||||
{processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Save
|
||||
</Button>
|
||||
<Transition show={recentlySuccessful} enter="transition ease-in-out" enterFrom="opacity-0" leave="transition ease-in-out" leaveTo="opacity-0">
|
||||
<CheckIcon className="text-success" />
|
||||
</Transition>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<SettingsLayout>
|
||||
<Head title="Profile settings" />
|
||||
<Container className="max-w-xl">
|
||||
<Container className="max-w-5xl">
|
||||
<Heading title="Profile settings" description="Manage your profile settings." />
|
||||
<UpdateUser mustVerifyEmail={mustVerifyEmail} status={status} />
|
||||
<UpdateUser />
|
||||
<UpdatePassword />
|
||||
<DeleteUser />
|
||||
</Container>
|
||||
</SettingsLayout>
|
||||
);
|
||||
|
@ -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 }) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<ProjectForm project={project}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Edit</DropdownMenuItem>
|
||||
</ProjectForm>
|
||||
<UsersAction project={project}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Users</DropdownMenuItem>
|
||||
</UsersAction>
|
||||
|
@ -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<SharedData>();
|
||||
@ -34,10 +33,13 @@ export const columns: ColumnDef<Project>[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at_by_timezone',
|
||||
accessorKey: 'created_at',
|
||||
header: 'Created at',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
return <DateTime date={row.original.created_at} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
@ -34,10 +34,11 @@ export default function DeleteProject({ project, children }: { project: Project;
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete {project.name}</DialogTitle>
|
||||
<DialogDescription>Delete project and all its resources.</DialogDescription>
|
||||
<DialogDescription className="sr-only">Delete project and all its resources.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form id="delete-project-form" onSubmit={submit}>
|
||||
<Form id="delete-project-form" onSubmit={submit} className="p-4">
|
||||
<p>Are you sure you want to delete this project? This action cannot be undone.</p>
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="project-name">Name</Label>
|
||||
@ -49,7 +50,7 @@ export default function DeleteProject({ project, children }: { project: Project;
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button form="delete-project-form" variant="destructive" disabled={form.processing}>
|
||||
|
@ -10,23 +10,30 @@ import {
|
||||
} from '@/components/ui/dialog';
|
||||
import { FormEventHandler, ReactNode, useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
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 { Project } from '@/types/project';
|
||||
import { Transition } from '@headlessui/react';
|
||||
|
||||
export default function CreateProject({ children }: { children: ReactNode }) {
|
||||
export default function ProjectForm({ project, children }: { project?: Project; children: ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
name: project?.name || '',
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (project) {
|
||||
form.patch(route('projects.update', project.id));
|
||||
return;
|
||||
}
|
||||
|
||||
form.post(route('projects.store'), {
|
||||
onSuccess() {
|
||||
setOpen(false);
|
||||
@ -39,10 +46,10 @@ export default function CreateProject({ children }: { children: ReactNode }) {
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create project</DialogTitle>
|
||||
<DialogDescription>Fill the form to create a new project</DialogDescription>
|
||||
<DialogTitle>{project ? 'Edit Project' : 'Create Project'}</DialogTitle>
|
||||
<DialogDescription className="sr-only">{project ? 'Edit the project details.' : 'Create a new project.'}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form id="create-project-form" onSubmit={submit}>
|
||||
<Form id="project-form" onSubmit={submit} className="p-4">
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
@ -51,15 +58,24 @@ export default function CreateProject({ children }: { children: ReactNode }) {
|
||||
</FormField>
|
||||
</FormFields>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="items-center">
|
||||
<Transition
|
||||
show={form.recentlySuccessful}
|
||||
enter="transition ease-in-out"
|
||||
enterFrom="opacity-0"
|
||||
leave="transition ease-in-out"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<CheckIcon className="text-success" />
|
||||
</Transition>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button form="create-project-form" type="button" onClick={submit} disabled={form.processing}>
|
||||
<Button form="project-form" type="button" onClick={submit} disabled={form.processing}>
|
||||
{form.processing && <LoaderCircle className="animate-spin" />}
|
||||
Create
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
@ -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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">Add user</Button>
|
||||
<Button>Add user</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add user</DialogTitle>
|
||||
<DialogDescription>Here you can add new user to {project.name}</DialogDescription>
|
||||
<DialogDescription className="sr-only">Here you can add new user to {project.name}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form id="add-user-form" onSubmit={submit}>
|
||||
<Form id="add-user-form" onSubmit={submit} className="p-4">
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="user">User</Label>
|
||||
@ -71,7 +61,10 @@ function AddUser({ project }: { project: Project }) {
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button form="add-user-form">Add</Button>
|
||||
<Button form="add-user-form" disabled={form.processing}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Add
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@ -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 (
|
||||
<AlertDialog open={open} onOpenChange={setOpen}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove user</AlertDialogTitle>
|
||||
<AlertDialogDescription>Remove user from {project.name}.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Remove user</DialogTitle>
|
||||
<DialogDescription className="sr-only">Remove user from {project.name}.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<AlertDialogFooter className="gap-2">
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<p className="p-4">
|
||||
Are you sure you want to remove <b>{user.name}</b> from this project?
|
||||
</p>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={submit} variant="destructive" disabled={form.processing}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Remove user
|
||||
</Button>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@ -155,18 +152,20 @@ const getColumns = (project: Project): ColumnDef<User>[] => [
|
||||
|
||||
export default function UsersAction({ project, children }: { project: Project; children: ReactNode }) {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Users</DialogTitle>
|
||||
<DialogDescription>Users assigned to this project</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DataTable columns={getColumns(project)} data={project.users} />
|
||||
<DialogFooter className="gap-2">
|
||||
<AddUser project={project} />
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent className="sm:max-w-3xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Project users</SheetTitle>
|
||||
<SheetDescription className="sr-only">Users assigned to this project</SheetDescription>
|
||||
</SheetHeader>
|
||||
<DataTable columns={getColumns(project)} data={project.users} modal />
|
||||
<SheetFooter className="gap-2">
|
||||
<div className="flex items-center">
|
||||
<AddUser project={project} />
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
@ -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() {
|
||||
<SettingsLayout>
|
||||
<Head title="Projects" />
|
||||
|
||||
<Container className="max-w-3xl">
|
||||
<Container className="max-w-5xl">
|
||||
<div className="flex items-start justify-between">
|
||||
<Heading title="Projects" description="Here you can manage your projects" />
|
||||
<div className="flex items-center gap-2">
|
||||
<CreateProject>
|
||||
<Button variant="outline">Create project</Button>
|
||||
</CreateProject>
|
||||
<ProjectForm>
|
||||
<Button>Create project</Button>
|
||||
</ProjectForm>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable columns={columns} data={page.props.projects.data} />
|
||||
|
@ -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<ServerLog> }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -40,15 +39,18 @@ const LogActionCell = ({ row }: { row: Row<ServerLog> }) => {
|
||||
{loading ? <LoaderCircleIcon className="animate-spin" /> : <EyeIcon />}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogContent className="sm:max-w-5xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>View Log</DialogTitle>
|
||||
<DialogDescription>This is all content of the log</DialogDescription>
|
||||
<DialogDescription className="sr-only">This is all content of the log</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="border-border bg-accent text-accent-foreground relative h-[500px] w-full rounded-md border p-3 font-mono text-sm whitespace-pre-line">
|
||||
<ScrollArea className="bg-accent text-accent-foreground relative h-[500px] w-full p-4 font-mono text-sm whitespace-pre-line">
|
||||
{content}
|
||||
<ScrollBar orientation="vertical" />
|
||||
</ScrollArea>
|
||||
<DialogFooter>
|
||||
<Button variant="outline">Download</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
@ -62,9 +64,12 @@ export const columns: ColumnDef<ServerLog>[] = [
|
||||
enableColumnFilter: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at_by_timezone',
|
||||
accessorKey: 'created_at',
|
||||
header: 'Created At',
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
return <DateTime date={row.original.created_at} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
@ -81,7 +81,7 @@ export default function CreateServerProvider({
|
||||
<DialogTitle>Connect</DialogTitle>
|
||||
<DialogDescription>Connect to a new server provider</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form id="create-server-provider-form" onSubmit={submit} className="py-4">
|
||||
<Form id="create-server-provider-form" onSubmit={submit} className="p-4">
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="provider">Provider</Label>
|
||||
|
@ -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<Server>[] = [
|
||||
{
|
||||
@ -27,6 +26,15 @@ export const columns: ColumnDef<Server>[] = [
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Created at',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
return <DateTime date={row.original.created_at} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
@ -43,7 +51,7 @@ export const columns: ColumnDef<Server>[] = [
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<Link href={route('servers.show', { server: row.original.id })}>
|
||||
<Link href={route('servers.show', { server: row.original.id })} prefetch>
|
||||
<Button variant="outline" size="sm">
|
||||
<EyeIcon />
|
||||
</Button>
|
||||
|
@ -37,7 +37,7 @@ export default function DeleteServer({ server, children }: { server: Server; chi
|
||||
<DialogDescription>Delete server and its resources.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form id="delete-server-form" onSubmit={submit}>
|
||||
<Form id="delete-server-form" onSubmit={submit} className="p-4">
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="server-name">Name</Label>
|
||||
@ -49,7 +49,7 @@ export default function DeleteServer({ server, children }: { server: Server; chi
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">Cancel</Button>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
|
||||
<Button form="delete-server-form" variant="destructive" disabled={form.processing}>
|
||||
|
@ -9,7 +9,7 @@ export default function ServerHeader({ server }: { server: Server }) {
|
||||
return (
|
||||
<div className="flex items-center justify-between border-b px-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<div className="text-accent-foreground/50 flex items-center space-x-2 text-xs">
|
||||
<div className="flex items-center space-x-2 text-xs">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center space-x-1">
|
||||
|
@ -31,7 +31,7 @@ export default function Servers() {
|
||||
<Heading title="Servers" description="All of the servers of your project listed here" />
|
||||
<div className="flex items-center gap-2">
|
||||
<CreateServer>
|
||||
<Button variant="outline">Create server</Button>
|
||||
<Button>Create server</Button>
|
||||
</CreateServer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,7 +14,7 @@ export default function InstallingServer() {
|
||||
}>();
|
||||
|
||||
return (
|
||||
<Container className="max-w-3xl">
|
||||
<Container className="max-w-5xl">
|
||||
<DataTable columns={columns} data={page.props.logs.data} />{' '}
|
||||
</Container>
|
||||
);
|
||||
|
@ -15,7 +15,7 @@ export default function ServerOverview() {
|
||||
}>();
|
||||
|
||||
return (
|
||||
<Container className="max-w-3xl">
|
||||
<Container className="max-w-5xl">
|
||||
<Heading title="Overview" description="Here you can see an overview of your server" />
|
||||
<DataTable columns={columns} data={page.props.logs.data} />
|
||||
</Container>
|
||||
|
34
resources/js/pages/users/components/actions.tsx
Normal file
34
resources/js/pages/users/components/actions.tsx
Normal file
@ -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 (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreVerticalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<UserForm user={user}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Edit</DropdownMenuItem>
|
||||
</UserForm>
|
||||
<Projects user={user}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>Projects</DropdownMenuItem>
|
||||
</Projects>
|
||||
<DropdownMenuSeparator />
|
||||
<DeleteUser user={user}>
|
||||
<DropdownMenuItem onSelect={(e) => e.preventDefault()} variant="destructive">
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DeleteUser>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
58
resources/js/pages/users/components/delete-user.tsx
Normal file
58
resources/js/pages/users/components/delete-user.tsx
Normal file
@ -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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete user</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Delete {user.name}[{user.email}]
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<p className="p-4">
|
||||
Are you sure you want to delete{' '}
|
||||
<b>
|
||||
{user.name} [{user.email}]
|
||||
</b>
|
||||
? This action cannot be undone.
|
||||
</p>
|
||||
<DialogFooter className="gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button onClick={submit} variant="destructive" disabled={form.processing}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Delete user
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
125
resources/js/pages/users/components/form.tsx
Normal file
125
resources/js/pages/users/components/form.tsx
Normal file
@ -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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{user ? `Edit ${user.name}` : 'Create user'}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{user ? `Fill the form to edit ${user.name}` : 'Fill the form to create a new user'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form id="user-form" onSubmit={submit} className="p-4">
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input type="text" id="name" name="name" value={form.data.name} onChange={(e) => form.setData('name', e.target.value)} />
|
||||
<InputError message={form.errors.name} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input type="email" id="email" name="email" value={form.data.email} onChange={(e) => form.setData('email', e.target.value)} />
|
||||
<InputError message={form.errors.email} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={form.data.password}
|
||||
onChange={(e) => form.setData('password', e.target.value)}
|
||||
/>
|
||||
<InputError message={form.errors.password} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select value={form.data.role} onValueChange={(value) => form.setData('role', value)}>
|
||||
<SelectTrigger id="role">
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem key="role-user" value="user">
|
||||
user
|
||||
</SelectItem>
|
||||
<SelectItem key="role-admin" value="admin">
|
||||
admin
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.role} />
|
||||
</FormField>
|
||||
</FormFields>
|
||||
</Form>
|
||||
<DialogFooter className="items-center">
|
||||
<Transition
|
||||
show={form.recentlySuccessful}
|
||||
enter="transition ease-in-out"
|
||||
enterFrom="opacity-0"
|
||||
leave="transition ease-in-out"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<CheckIcon className="text-success" />
|
||||
</Transition>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button form="create-user-form" type="button" onClick={submit} disabled={form.processing}>
|
||||
{form.processing && <LoaderCircle className="animate-spin" />}
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
47
resources/js/pages/users/components/list.tsx
Normal file
47
resources/js/pages/users/components/list.tsx
Normal file
@ -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<User>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
enableColumnFilter: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
enableColumnFilter: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at',
|
||||
header: 'Created At',
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => <DateTime date={row.original.created_at} />,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableColumnFilter: false,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end">
|
||||
<UserActions user={row.original} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
type Page = {
|
||||
users: {
|
||||
data: User[];
|
||||
};
|
||||
};
|
||||
|
||||
export default function UsersList() {
|
||||
const page = usePage<Page>();
|
||||
|
||||
return <DataTable columns={columns} data={page.props.users.data} />;
|
||||
}
|
176
resources/js/pages/users/components/projects.tsx
Normal file
176
resources/js/pages/users/components/projects.tsx
Normal file
@ -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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button key={`remove-user-${user.id}`} variant="outline" size="sm" tabIndex={-1}>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Remove from project</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Remove <b>{user.name}</b> from <b>{project.name}</b>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<p className="p-4">
|
||||
Are you sure you want to remove <b>{user.name}</b> from <b>{project.name}</b>?
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button variant="destructive" disabled={form.processing} onClick={submit}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Remove
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function AddToProject({ user }: { user: User }) {
|
||||
const page = usePage<SharedData>();
|
||||
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 (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button key={`add-project-${user.id}`}>Add to project</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add to project</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
Add <b>{user.name}</b> to a project
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form id="add-to-project-form" className="p-4" onSubmit={submit}>
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="project">Project</Label>
|
||||
<Select value={form.data.project} onValueChange={(value) => form.setData('project', value)}>
|
||||
<SelectTrigger id="project">
|
||||
<SelectValue placeholder="Select a project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{page.props.auth.projects.map((project: Project) => (
|
||||
<SelectItem key={`project-${project.id}`} value={project.id.toString()}>
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.project} />
|
||||
</FormField>
|
||||
</FormFields>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</DialogClose>
|
||||
<Button form="add-to-project-form" disabled={form.processing} onClick={submit}>
|
||||
{form.processing && <LoaderCircleIcon className="animate-spin" />}
|
||||
Add
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
const columns = (user: User): ColumnDef<Project>[] => {
|
||||
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 (
|
||||
<div className="flex items-center justify-end" key={row.id}>
|
||||
<RemoveProject user={user} project={row.original} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default function Projects({ user, children }: { user: User; children: ReactNode }) {
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent className="sm:max-w-3xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Projects</SheetTitle>
|
||||
<SheetDescription className="sr-only">
|
||||
All projects that <b>{user.name}</b> has access
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
{user.projects && <DataTable columns={columns(user)} data={user.projects} modal />}
|
||||
<SheetFooter>
|
||||
<div className="flex items-center">
|
||||
<AddToProject user={user} />
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
25
resources/js/pages/users/index.tsx
Normal file
25
resources/js/pages/users/index.tsx
Normal file
@ -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 (
|
||||
<SettingsLayout>
|
||||
<Head title="Users" />
|
||||
|
||||
<Container className="max-w-5xl">
|
||||
<div className="flex items-start justify-between">
|
||||
<Heading title="Users" description="Here you can manage all users" />
|
||||
<UserForm>
|
||||
<Button>Create user</Button>
|
||||
</UserForm>
|
||||
</div>
|
||||
<UsersList />
|
||||
</Container>
|
||||
</SettingsLayout>
|
||||
);
|
||||
}
|
2
resources/js/types/project.d.ts
vendored
2
resources/js/types/project.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
2
resources/js/types/server-log.d.ts
vendored
2
resources/js/types/server-log.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
2
resources/js/types/server.d.ts
vendored
2
resources/js/types/server.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
5
resources/js/types/user.d.ts
vendored
5
resources/js/types/user.d.ts
vendored
@ -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...
|
||||
}
|
||||
|
@ -44,7 +44,7 @@
|
||||
@vite(['resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"])
|
||||
@inertiaHead
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
<body class="selection:bg-brand font-sans antialiased selection:text-white">
|
||||
@inertia
|
||||
</body>
|
||||
</html>
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user