mirror of
https://github.com/vitodeploy/vito.git
synced 2025-07-03 06:56:15 +00:00
Setup Inertia (#593)
This commit is contained in:
57
resources/js/pages/auth/confirm-password.tsx
Normal file
57
resources/js/pages/auth/confirm-password.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
// Components
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { FormEventHandler } from 'react';
|
||||
|
||||
import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AuthLayout from '@/layouts/auth-layout';
|
||||
|
||||
export default function ConfirmPassword() {
|
||||
const { data, setData, post, processing, errors, reset } = useForm<Required<{ password: string }>>({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
post(route('password.confirm'), {
|
||||
onFinish: () => reset('password'),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout title="Confirm your password" description="This is a secure area of the application. Please confirm your password before continuing.">
|
||||
<Head title="Confirm password" />
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
autoComplete="current-password"
|
||||
value={data.password}
|
||||
autoFocus
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
/>
|
||||
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Button className="w-full" disabled={processing}>
|
||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
Confirm password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
63
resources/js/pages/auth/forgot-password.tsx
Normal file
63
resources/js/pages/auth/forgot-password.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
// Components
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { FormEventHandler } from 'react';
|
||||
|
||||
import InputError from '@/components/input-error';
|
||||
import TextLink from '@/components/text-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AuthLayout from '@/layouts/auth-layout';
|
||||
|
||||
export default function ForgotPassword({ status }: { status?: string }) {
|
||||
const { data, setData, post, processing, errors } = useForm<Required<{ email: string }>>({
|
||||
email: '',
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
post(route('password.email'));
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout title="Forgot password" description="Enter your email to receive a password reset link">
|
||||
<Head title="Forgot password" />
|
||||
|
||||
{status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>}
|
||||
|
||||
<div className="space-y-6">
|
||||
<form onSubmit={submit}>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="off"
|
||||
value={data.email}
|
||||
autoFocus
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
|
||||
<div className="my-6 flex items-center justify-start">
|
||||
<Button className="w-full" disabled={processing}>
|
||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
Email password reset link
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="text-muted-foreground space-x-1 text-center text-sm">
|
||||
<span>Or, return to</span>
|
||||
<TextLink href={route('login')}>log in</TextLink>
|
||||
</div>
|
||||
</div>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
97
resources/js/pages/auth/login.tsx
Normal file
97
resources/js/pages/auth/login.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { FormEventHandler } from 'react';
|
||||
|
||||
import InputError from '@/components/input-error';
|
||||
import TextLink from '@/components/text-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AuthLayout from '@/layouts/auth-layout';
|
||||
|
||||
type LoginForm = {
|
||||
email: string;
|
||||
password: string;
|
||||
remember: boolean;
|
||||
};
|
||||
|
||||
interface LoginProps {
|
||||
status?: string;
|
||||
canResetPassword: boolean;
|
||||
}
|
||||
|
||||
export default function Login({ status, canResetPassword }: LoginProps) {
|
||||
const { data, setData, post, processing, errors, reset } = useForm<Required<LoginForm>>({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
post(route('login'), {
|
||||
onFinish: () => reset('password'),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout title="Log in to your account" description="Enter your email and password below to log in">
|
||||
<Head title="Log in" />
|
||||
|
||||
<form className="flex flex-col gap-6" onSubmit={submit}>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
autoFocus
|
||||
tabIndex={1}
|
||||
autoComplete="email"
|
||||
value={data.email}
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
{canResetPassword && (
|
||||
<TextLink href={route('password.request')} className="ml-auto text-sm" tabIndex={5}>
|
||||
Forgot password?
|
||||
</TextLink>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
required
|
||||
tabIndex={2}
|
||||
autoComplete="current-password"
|
||||
value={data.password}
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox id="remember" name="remember" checked={data.remember} onClick={() => setData('remember', !data.remember)} tabIndex={3} />
|
||||
<Label htmlFor="remember">Remember me</Label>
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="mt-4 w-full" tabIndex={4} disabled={processing}>
|
||||
{processing && <LoaderCircle className="animate-spin" />}
|
||||
Log in
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{status && <div className="mb-4 text-center text-sm font-medium text-green-600">{status}</div>}
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
98
resources/js/pages/auth/reset-password.tsx
Normal file
98
resources/js/pages/auth/reset-password.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
import { LoaderCircle } from 'lucide-react';
|
||||
import { FormEventHandler } from 'react';
|
||||
|
||||
import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AuthLayout from '@/layouts/auth-layout';
|
||||
|
||||
interface ResetPasswordProps {
|
||||
token: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
type ResetPasswordForm = {
|
||||
token: string;
|
||||
email: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
};
|
||||
|
||||
export default function ResetPassword({ token, email }: ResetPasswordProps) {
|
||||
const { data, setData, post, processing, errors, reset } = useForm<Required<ResetPasswordForm>>({
|
||||
token: token,
|
||||
email: email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
post(route('password.store'), {
|
||||
onFinish: () => reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthLayout title="Reset password" description="Please enter your new password below">
|
||||
<Head title="Reset password" />
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<div className="grid gap-6">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
value={data.email}
|
||||
className="mt-1 block w-full"
|
||||
readOnly
|
||||
onChange={(e) => setData('email', e.target.value)}
|
||||
/>
|
||||
<InputError message={errors.email} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
value={data.password}
|
||||
className="mt-1 block w-full"
|
||||
autoFocus
|
||||
onChange={(e) => setData('password', e.target.value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="password_confirmation">Confirm password</Label>
|
||||
<Input
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
name="password_confirmation"
|
||||
autoComplete="new-password"
|
||||
value={data.password_confirmation}
|
||||
className="mt-1 block w-full"
|
||||
onChange={(e) => setData('password_confirmation', e.target.value)}
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<InputError message={errors.password_confirmation} className="mt-2" />
|
||||
</div>
|
||||
|
||||
<Button type="submit" className="mt-4 w-full" disabled={processing}>
|
||||
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
75
resources/js/pages/server-logs/columns.tsx
Normal file
75
resources/js/pages/server-logs/columns.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
'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 { useState } from 'react';
|
||||
import axios from 'axios';
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||
|
||||
const LogActionCell = ({ row }: { row: Row<ServerLog> }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const showLog = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get(route('logs.show', { server: row.original.server_id, log: row.original.id }));
|
||||
setContent(response.data);
|
||||
} catch (error: unknown) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
setContent(error.message);
|
||||
} else {
|
||||
setContent('An unknown error occurred.');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm" onClick={showLog} disabled={loading}>
|
||||
{loading ? <LoaderCircleIcon className="animate-spin" /> : <EyeIcon />}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>View Log</DialogTitle>
|
||||
<DialogDescription>This is all content of the log</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="border-border relative h-[500px] w-full rounded-md border bg-black p-3 font-mono text-sm whitespace-pre-line text-gray-50">
|
||||
{content}
|
||||
<ScrollBar orientation="vertical" />
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const columns: ColumnDef<ServerLog>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Event',
|
||||
enableColumnFilter: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'created_at_by_timezone',
|
||||
header: 'Created At',
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableColumnFilter: false,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => <LogActionCell row={row} />,
|
||||
},
|
||||
];
|
159
resources/js/pages/server-providers/create-server-provider.tsx
Normal file
159
resources/js/pages/server-providers/create-server-provider.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import { LoaderCircle, PlusIcon, WifiIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useForm, usePage } from '@inertiajs/react';
|
||||
import { FormEventHandler, useEffect, useState } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import InputError from '@/components/input-error';
|
||||
import { Form, FormField, FormFields } from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { SharedData } from '@/types';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
||||
type ServerProviderForm = {
|
||||
provider: string;
|
||||
name: string;
|
||||
global: boolean;
|
||||
};
|
||||
|
||||
export default function CreateServerProvider({
|
||||
trigger,
|
||||
providers,
|
||||
defaultProvider,
|
||||
onProviderAdded,
|
||||
}: {
|
||||
trigger: 'icon' | 'button';
|
||||
providers: string[];
|
||||
defaultProvider?: string;
|
||||
onProviderAdded?: () => void;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const page = usePage<SharedData>();
|
||||
|
||||
const form = useForm<Required<ServerProviderForm>>({
|
||||
provider: 'aws',
|
||||
name: '',
|
||||
global: false,
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
form.post(route('server-providers.store'), {
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
if (onProviderAdded) {
|
||||
onProviderAdded();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
form.setData('provider', defaultProvider ?? 'aws');
|
||||
}, [defaultProvider]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="outline">
|
||||
{trigger === 'icon' && <WifiIcon />}
|
||||
{trigger === 'button' && (
|
||||
<>
|
||||
<PlusIcon />
|
||||
Connect
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Connect</DialogTitle>
|
||||
<DialogDescription>Connect to a new server provider</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form id="create-server-provider-form" onSubmit={submit} className="py-4">
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="provider">Provider</Label>
|
||||
<Select
|
||||
value={form.data.provider}
|
||||
onValueChange={(value) => {
|
||||
form.setData('provider', value);
|
||||
form.clearErrors();
|
||||
}}
|
||||
>
|
||||
<SelectTrigger id="provider">
|
||||
<SelectValue placeholder="Select a provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{providers.map((provider) => (
|
||||
<SelectItem key={provider} value={provider}>
|
||||
{provider}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.provider} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Name"
|
||||
value={form.data.name}
|
||||
onChange={(e) => form.setData('name', e.target.value)}
|
||||
/>
|
||||
<InputError message={form.errors.name} />
|
||||
</FormField>
|
||||
{page.props.configs.server_providers_custom_fields[form.data.provider]?.map((item: string) => (
|
||||
<FormField key={item}>
|
||||
<Label htmlFor={item}>{item}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
name={item}
|
||||
id={item}
|
||||
placeholder={item}
|
||||
value={(form.data[item as keyof ServerProviderForm] as string) ?? ''}
|
||||
onChange={(e) => form.setData(item as keyof ServerProviderForm, e.target.value)}
|
||||
/>
|
||||
<InputError message={form.errors[item as keyof ServerProviderForm]} />
|
||||
</FormField>
|
||||
))}
|
||||
<FormField>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox id="global" name="global" checked={form.data.global} onClick={() => form.setData('global', !form.data.global)} />
|
||||
<Label htmlFor="global">Is global (accessible in all projects)</Label>
|
||||
</div>
|
||||
<InputError message={form.errors.global} />
|
||||
</FormField>
|
||||
</FormFields>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="button" onClick={submit} disabled={form.processing}>
|
||||
{form.processing && <LoaderCircle className="animate-spin" />}
|
||||
Connect
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
55
resources/js/pages/servers/columns.tsx
Normal file
55
resources/js/pages/servers/columns.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
import { Server } from '@/types/server';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { EyeIcon } from 'lucide-react';
|
||||
|
||||
export const columns: ColumnDef<Server>[] = [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
enableHiding: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Name',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'ip',
|
||||
header: 'IP',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
enableColumnFilter: true,
|
||||
enableSorting: true,
|
||||
cell: ({ row }) => {
|
||||
return <Badge variant={row.original.status_color}>{row.original.status}</Badge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
enableColumnFilter: false,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<Link href={route('servers.show', { server: row.original.id })}>
|
||||
<Button variant="outline" size="sm">
|
||||
<EyeIcon />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
348
resources/js/pages/servers/create-server.tsx
Normal file
348
resources/js/pages/servers/create-server.tsx
Normal file
@ -0,0 +1,348 @@
|
||||
import { ClipboardCheckIcon, ClipboardIcon, LoaderCircle, TriangleAlert } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { useForm, usePage } from '@inertiajs/react';
|
||||
import React, { FormEventHandler, useState } from 'react';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import InputError from '@/components/input-error';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
import { ServerProvider } from '@/types/server-provider';
|
||||
import CreateServerProvider from '@/pages/server-providers/create-server-provider';
|
||||
import axios from 'axios';
|
||||
import { Form, FormField, FormFields } from '@/components/ui/form';
|
||||
import type { SharedData } from '@/types';
|
||||
|
||||
type CreateServerForm = {
|
||||
provider: string;
|
||||
server_provider: number;
|
||||
name: string;
|
||||
os: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
region: string;
|
||||
plan: string;
|
||||
webserver: string;
|
||||
database: string;
|
||||
php: string;
|
||||
};
|
||||
|
||||
export default function CreateServer({ children }: { children: React.ReactNode }) {
|
||||
const page = usePage<SharedData>();
|
||||
|
||||
const form = useForm<Required<CreateServerForm>>({
|
||||
provider: 'custom',
|
||||
server_provider: 0,
|
||||
name: '',
|
||||
os: '',
|
||||
ip: '',
|
||||
port: 22,
|
||||
region: '',
|
||||
plan: '',
|
||||
webserver: '',
|
||||
database: '',
|
||||
php: '',
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
form.post(route('servers'));
|
||||
};
|
||||
|
||||
const [copySuccess, setCopySuccess] = useState(false);
|
||||
const copyToClipboard = () => {
|
||||
navigator.clipboard.writeText(page.props.publicKeyText).then(() => {
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => {
|
||||
setCopySuccess(false);
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const [serverProviders, setServerProviders] = useState<ServerProvider[]>([]);
|
||||
const fetchServerProviders = async () => {
|
||||
const serverProviders = await axios.get(route('server-providers.all'));
|
||||
setServerProviders(serverProviders.data);
|
||||
};
|
||||
const selectProvider = (provider: string) => {
|
||||
form.setData('provider', provider);
|
||||
form.clearErrors();
|
||||
if (provider !== 'custom') {
|
||||
form.setData('server_provider', 0);
|
||||
form.setData('region', '');
|
||||
form.setData('plan', '');
|
||||
fetchServerProviders();
|
||||
}
|
||||
};
|
||||
|
||||
const selectServerProvider = async (serverProvider: string) => {
|
||||
form.setData('server_provider', parseInt(serverProvider));
|
||||
await fetchRegions(parseInt(serverProvider));
|
||||
};
|
||||
|
||||
const [regions, setRegions] = useState<{ [key: string]: string }>({});
|
||||
const fetchRegions = async (serverProvider: number) => {
|
||||
const regions = await axios.get(route('server-providers.regions', { serverProvider: serverProvider }));
|
||||
setRegions(regions.data);
|
||||
};
|
||||
const selectRegion = async (region: string) => {
|
||||
form.setData('region', region);
|
||||
if (region !== '') {
|
||||
await fetchPlans(form.data.server_provider, region);
|
||||
}
|
||||
};
|
||||
|
||||
const [plans, setPlans] = useState<{ [key: string]: string }>({});
|
||||
const fetchPlans = async (serverProvider: number, region: string) => {
|
||||
const plans = await axios.get(route('server-providers.plans', { serverProvider: serverProvider, region: region }));
|
||||
setPlans(plans.data);
|
||||
};
|
||||
const selectPlan = (plan: string) => {
|
||||
form.setData('plan', plan);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>{children}</SheetTrigger>
|
||||
<SheetContent className="w-full lg:max-w-4xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>Create new server</SheetTitle> <SheetDescription>Fill in the details to create a new server.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Form id="create-server-form" className="p-4" onSubmit={submit}>
|
||||
<FormFields>
|
||||
<FormField>
|
||||
<Label htmlFor="provider">Provider</Label>
|
||||
<Select value={form.data.provider} onValueChange={(value) => selectProvider(value)}>
|
||||
<SelectTrigger id="provider">
|
||||
<SelectValue placeholder="Select a provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{page.props.configs.server_providers.map((provider) => (
|
||||
<SelectItem key={provider} value={provider}>
|
||||
{provider}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError />
|
||||
</FormField>
|
||||
|
||||
{form.data.provider && form.data.provider !== 'custom' && (
|
||||
<FormField>
|
||||
<Label htmlFor="server-provider">Server provider connection</Label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={form.data.server_provider.toString()} onValueChange={selectServerProvider}>
|
||||
<SelectTrigger id="provider">
|
||||
<SelectValue placeholder="Select a provider" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{serverProviders
|
||||
.filter((item: ServerProvider) => item.provider === form.data.provider)
|
||||
.map((provider) => (
|
||||
<SelectItem key={`server-provider-${provider.id}`} value={provider.id.toString()}>
|
||||
{provider.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<CreateServerProvider
|
||||
trigger="icon"
|
||||
providers={page.props.configs.server_providers.filter((item) => item !== 'custom')}
|
||||
defaultProvider={form.data.provider}
|
||||
onProviderAdded={fetchServerProviders}
|
||||
/>
|
||||
</div>
|
||||
<InputError />
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
{form.data.provider && form.data.provider !== 'custom' && (
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<FormField>
|
||||
<Label htmlFor="region">Region</Label>
|
||||
<Select value={form.data.region} onValueChange={selectRegion} disabled={form.data.server_provider === 0}>
|
||||
<SelectTrigger id="region">
|
||||
<SelectValue placeholder="Select a region" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{Object.entries(regions).map(([key, value]) => (
|
||||
<SelectItem key={`region-${key}`} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.region} />
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Label htmlFor="plan">Plan</Label>
|
||||
<Select value={form.data.plan} onValueChange={selectPlan} disabled={form.data.region === ''}>
|
||||
<SelectTrigger id="plan">
|
||||
<SelectValue placeholder="Select a plan" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{Object.entries(plans).map(([key, value]) => (
|
||||
<SelectItem key={`plan-${key}`} value={key}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.plan} />
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{form.data.provider === 'custom' && (
|
||||
<>
|
||||
<Alert>
|
||||
<TriangleAlert size={5} />
|
||||
<AlertDescription>
|
||||
Your server needs to have a new unused installation of supported operating systems and must have a root user. To get started, add
|
||||
our public key to /root/.ssh/authorized_keys file by running the bellow command on your server as root.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<FormField>
|
||||
<Label htmlFor="public_key">Public Key</Label>
|
||||
<Button
|
||||
onClick={copyToClipboard}
|
||||
variant="outline"
|
||||
id="public_key"
|
||||
type="button"
|
||||
value={page.props.publicKeyText}
|
||||
className="justify-between truncate font-normal"
|
||||
>
|
||||
<span className="w-full max-w-2/3 overflow-x-hidden overflow-ellipsis">{page.props.publicKeyText}</span>
|
||||
{copySuccess ? <ClipboardCheckIcon size={5} className="text-success!" /> : <ClipboardIcon size={5} />}
|
||||
</Button>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 items-start gap-6">
|
||||
<FormField>
|
||||
<Label htmlFor="name">Server Name</Label>
|
||||
<Input id="name" type="text" autoComplete="name" value={form.data.name} onChange={(e) => form.setData('name', e.target.value)} />
|
||||
<InputError message={form.errors.name} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="os">Operating System</Label>
|
||||
<Select value={form.data.os} onValueChange={(value) => form.setData('os', value)}>
|
||||
<SelectTrigger id="os">
|
||||
<SelectValue placeholder="Select an operating system" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{page.props.configs.operating_systems.map((value) => (
|
||||
<SelectItem key={`os-${value}`} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.os} />
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
{form.data.provider === 'custom' && (
|
||||
<div className="grid grid-cols-2 items-start gap-6">
|
||||
<FormField>
|
||||
<Label htmlFor="ip">SSH IP</Label>
|
||||
<Input id="ip" type="text" autoComplete="ip" value={form.data.ip} onChange={(e) => form.setData('ip', e.target.value)} />
|
||||
<InputError message={form.errors.ip} />
|
||||
</FormField>
|
||||
|
||||
<FormField>
|
||||
<Label htmlFor="port">SSH Port</Label>
|
||||
<Input
|
||||
id="port"
|
||||
type="text"
|
||||
autoComplete="port"
|
||||
value={form.data.port}
|
||||
onChange={(e) => form.setData('port', parseInt(e.target.value))}
|
||||
/>
|
||||
<InputError message={form.errors.port} />
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3 items-start gap-6">
|
||||
<FormField>
|
||||
<Label htmlFor="webserver">Webserver</Label>
|
||||
<Select value={form.data.webserver} onValueChange={(value) => form.setData('webserver', value)}>
|
||||
<SelectTrigger id="webserver">
|
||||
<SelectValue placeholder="Select webserver" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{page.props.configs.webservers.map((value) => (
|
||||
<SelectItem key={`webserver-${value}`} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.webserver} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="database">Database</Label>
|
||||
<Select value={form.data.database} onValueChange={(value) => form.setData('database', value)}>
|
||||
<SelectTrigger id="database">
|
||||
<SelectValue placeholder="Select database" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{page.props.configs.databases.map((value) => (
|
||||
<SelectItem key={`database-${value}`} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.database} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Label htmlFor="php">PHP</Label>
|
||||
<Select value={form.data.php} onValueChange={(value) => form.setData('php', value)}>
|
||||
<SelectTrigger id="php">
|
||||
<SelectValue placeholder="Select PHP version" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{page.props.configs.php_versions.map((value) => (
|
||||
<SelectItem key={`php-${value}`} value={value}>
|
||||
{value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={form.errors.php} />
|
||||
</FormField>
|
||||
</div>
|
||||
</FormFields>
|
||||
</Form>
|
||||
<SheetFooter>
|
||||
<div className="flex items-center">
|
||||
<Button type="submit" form="create-server-form" tabIndex={4} disabled={form.processing}>
|
||||
{form.processing && <LoaderCircle className="animate-spin" />} Create
|
||||
</Button>
|
||||
</div>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
45
resources/js/pages/servers/index.tsx
Normal file
45
resources/js/pages/servers/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { Head, usePage } from '@inertiajs/react';
|
||||
|
||||
import { type Configs } from '@/types';
|
||||
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { columns } from '@/pages/servers/columns';
|
||||
import { Server } from '@/types/server';
|
||||
import Heading from '@/components/heading';
|
||||
import CreateServer from '@/pages/servers/create-server';
|
||||
import Container from '@/components/container';
|
||||
import { PlusIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import React from 'react';
|
||||
|
||||
type Response = {
|
||||
servers: {
|
||||
data: Server[];
|
||||
};
|
||||
public_key: string;
|
||||
configs: Configs;
|
||||
};
|
||||
|
||||
export default function Servers() {
|
||||
const page = usePage<Response>();
|
||||
return (
|
||||
<AppLayout>
|
||||
<Head title="Servers" />
|
||||
|
||||
<Container>
|
||||
<div className="flex items-start justify-between">
|
||||
<Heading title="Servers" description="All of the servers on your project are here" />
|
||||
<div className="flex items-center gap-2">
|
||||
<CreateServer>
|
||||
<Button variant="outline">
|
||||
<PlusIcon /> Create new server
|
||||
</Button>
|
||||
</CreateServer>
|
||||
</div>
|
||||
</div>
|
||||
<DataTable columns={columns} data={page.props.servers.data} />
|
||||
</Container>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
30
resources/js/pages/servers/installing.tsx
Normal file
30
resources/js/pages/servers/installing.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import type { Server } from '@/types/server';
|
||||
import type { ServerLog } from '@/types/server-log';
|
||||
import Container from '@/components/container';
|
||||
import Heading from '@/components/heading';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { DataTable } from '@/components/data-table';
|
||||
import { columns } from '@/pages/server-logs/columns';
|
||||
import { usePage } from '@inertiajs/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
export default function InstallingServer() {
|
||||
const page = usePage<{
|
||||
server: Server;
|
||||
logs: {
|
||||
data: ServerLog[];
|
||||
};
|
||||
}>();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="flex items-start justify-between">
|
||||
<Heading title={`Installing ${page.props.server.name}`} description="Your server is being installed" />
|
||||
{page.props.server.status === 'installation_failed' && <Button variant="destructive">Delete</Button>}
|
||||
</div>
|
||||
<Progress value={parseInt(page.props.server.progress || '0')} />
|
||||
<div className="mt-2 text-center">{page.props.server.progress}%</div>
|
||||
<DataTable columns={columns} data={page.props.logs.data} />
|
||||
</Container>
|
||||
);
|
||||
}
|
31
resources/js/pages/servers/show.tsx
Normal file
31
resources/js/pages/servers/show.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Head, usePage } from '@inertiajs/react';
|
||||
|
||||
import { type Configs } from '@/types';
|
||||
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import { type Server } from '@/types/server';
|
||||
import InstallingServer from '@/pages/servers/installing';
|
||||
import type { ServerLog } from '@/types/server-log';
|
||||
|
||||
type Response = {
|
||||
servers: {
|
||||
data: Server[];
|
||||
};
|
||||
logs: {
|
||||
data: ServerLog[];
|
||||
};
|
||||
server: Server;
|
||||
public_key: string;
|
||||
configs: Configs;
|
||||
};
|
||||
|
||||
export default function ShowServer() {
|
||||
const page = usePage<Response>();
|
||||
return (
|
||||
<AppLayout>
|
||||
<Head title={page.props.server.name} />
|
||||
|
||||
{['installing', 'installation_failed'].includes(page.props.server.status) && <InstallingServer />}
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
128
resources/js/pages/settings/password.tsx
Normal file
128
resources/js/pages/settings/password.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import InputError from '@/components/input-error';
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import SettingsLayout from '@/layouts/settings/layout';
|
||||
import { type BreadcrumbItem } from '@/types';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { Head, useForm } from '@inertiajs/react';
|
||||
import { FormEventHandler, useRef } from 'react';
|
||||
|
||||
import HeadingSmall from '@/components/heading-small';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Password settings',
|
||||
href: '/settings/password',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Password() {
|
||||
const passwordInput = useRef<HTMLInputElement>(null);
|
||||
const currentPasswordInput = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { data, setData, errors, put, reset, processing, recentlySuccessful } = useForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const updatePassword: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
put(route('password.update'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => reset(),
|
||||
onError: (errors) => {
|
||||
if (errors.password) {
|
||||
reset('password', 'password_confirmation');
|
||||
passwordInput.current?.focus();
|
||||
}
|
||||
|
||||
if (errors.current_password) {
|
||||
reset('current_password');
|
||||
currentPasswordInput.current?.focus();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout breadcrumbs={breadcrumbs}>
|
||||
<Head title="Profile settings" />
|
||||
|
||||
<SettingsLayout>
|
||||
<div className="space-y-6">
|
||||
<HeadingSmall title="Update password" description="Ensure your account is using a long, random password to stay secure" />
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
129
resources/js/pages/settings/profile.tsx
Normal file
129
resources/js/pages/settings/profile.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { type BreadcrumbItem, type SharedData } from '@/types';
|
||||
import { Transition } from '@headlessui/react';
|
||||
import { Head, Link, useForm, usePage } from '@inertiajs/react';
|
||||
import { FormEventHandler } from 'react';
|
||||
|
||||
import DeleteUser from '@/components/delete-user';
|
||||
import HeadingSmall from '@/components/heading-small';
|
||||
import InputError from '@/components/input-error';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import AppLayout from '@/layouts/app-layout';
|
||||
import SettingsLayout from '@/layouts/settings/layout';
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
title: 'Home',
|
||||
href: '/',
|
||||
},
|
||||
{
|
||||
title: 'Profile settings',
|
||||
href: '/settings/profile',
|
||||
},
|
||||
];
|
||||
|
||||
type ProfileForm = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export default function Profile({ mustVerifyEmail, status }: { mustVerifyEmail: boolean; status?: string }) {
|
||||
const { auth } = usePage<SharedData>().props;
|
||||
|
||||
const { data, setData, patch, errors, processing, recentlySuccessful } = useForm<Required<ProfileForm>>({
|
||||
name: auth.user.name,
|
||||
email: auth.user.email,
|
||||
});
|
||||
|
||||
const submit: FormEventHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
patch(route('profile.update'), {
|
||||
preserveScroll: true,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout breadcrumbs={breadcrumbs}>
|
||||
<Head title="Profile settings" />
|
||||
|
||||
<SettingsLayout>
|
||||
<div className="space-y-6">
|
||||
<HeadingSmall title="Profile information" description="Update your name and email address" />
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<DeleteUser />
|
||||
</SettingsLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user