first commit
This commit is contained in:
commit
e687b4437b
418 files changed
+33577
No files matched your search
@@ -0,0 +1,44 @@
|
||||
<script module lang="ts">
|
||||
import { dashboard } from '@/routes';
|
||||
|
||||
export const layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: dashboard(),
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import PlaceholderPattern from '@/components/PlaceholderPattern.svelte';
|
||||
</script>
|
||||
|
||||
<AppHead title="Dashboard" />
|
||||
|
||||
<div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
|
||||
<div class="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div
|
||||
class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||||
>
|
||||
<PlaceholderPattern />
|
||||
</div>
|
||||
<div
|
||||
class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||||
>
|
||||
<PlaceholderPattern />
|
||||
</div>
|
||||
<div
|
||||
class="relative aspect-video overflow-hidden rounded-xl border border-sidebar-border/70 dark:border-sidebar-border"
|
||||
>
|
||||
<PlaceholderPattern />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative min-h-screen flex-1 rounded-xl border border-sidebar-border/70 md:min-h-min dark:border-sidebar-border"
|
||||
>
|
||||
<PlaceholderPattern />
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { Link, page } from '@inertiajs/svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import Features from '@/components/my/Features.svelte';
|
||||
import Contact from '@/components/my/Contact.svelte';
|
||||
import Teams from '@/components/my/Teams.svelte';
|
||||
import Pricing from '@/components/my/Pricing.svelte';
|
||||
import Hero from '@/components/my/Hero.svelte';
|
||||
import Logo from '@/components/my/Logo.svelte';
|
||||
import FAQ from '@/components/my/FAQ.svelte';
|
||||
import Footer from '@/components/my/Footer.svelte';
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
|
||||
let {
|
||||
canRegister = true,
|
||||
}: {
|
||||
canRegister: boolean;
|
||||
} = $props();
|
||||
|
||||
const auth = $derived(page.props.auth);
|
||||
</script>
|
||||
|
||||
<AppHead title="Willkommen">
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</AppHead>
|
||||
|
||||
<ModeWatcher />
|
||||
<main class="container mx-auto">
|
||||
<div class="grid place-items-center lg:max-w-7xl gap-8 py-20 md:py-32">
|
||||
<Logo />
|
||||
<Hero canRegister={true} />
|
||||
<Features />
|
||||
<Teams />
|
||||
<Pricing />
|
||||
<Contact />
|
||||
<FAQ />
|
||||
<Footer />
|
||||
</div>
|
||||
</main>
|
||||
@@ -0,0 +1,50 @@
|
||||
<script module lang="ts">
|
||||
export const layout = {
|
||||
title: 'Confirm your password',
|
||||
description:
|
||||
'This is a secure area of the application. Please confirm your password before continuing.',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Form } from '@inertiajs/svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import PasswordInput from '@/components/PasswordInput.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { store } from '@/routes/password/confirm';
|
||||
</script>
|
||||
|
||||
<AppHead title="Confirm password" />
|
||||
|
||||
<Form {...store.form()} resetOnSuccess>
|
||||
{#snippet children({ errors, processing })}
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
class="mt-1 block w-full"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
disabled={processing}
|
||||
data-test="confirm-password-button"
|
||||
>
|
||||
{#if processing}<Spinner />{/if}
|
||||
Confirm password
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form>
|
||||
@@ -0,0 +1,68 @@
|
||||
<script module lang="ts">
|
||||
export const layout = {
|
||||
title: 'Forgot password',
|
||||
description: 'Enter your email to receive a password reset link',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Form } from '@inertiajs/svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import TextLink from '@/components/TextLink.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { login } from '@/routes';
|
||||
import { email } from '@/routes/password';
|
||||
|
||||
let {
|
||||
status = '',
|
||||
}: {
|
||||
status?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AppHead title="Forgot password" />
|
||||
|
||||
{#if status}
|
||||
<div class="mb-4 text-center text-sm font-medium text-green-600">
|
||||
{status}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-6">
|
||||
<Form {...email.form()}>
|
||||
{#snippet children({ errors, processing })}
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="off"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
|
||||
<div class="my-6 flex items-center justify-start">
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
disabled={processing}
|
||||
data-test="email-password-reset-link-button"
|
||||
>
|
||||
{#if processing}<Spinner />{/if}
|
||||
Email password reset link
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form>
|
||||
|
||||
<div class="space-x-1 text-center text-sm text-muted-foreground">
|
||||
<span>Or, return to</span>
|
||||
<TextLink href={login()}>log in</TextLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,106 @@
|
||||
<script module lang="ts">
|
||||
export const layout = {
|
||||
title: 'Log in to your account',
|
||||
description: 'Enter your email and password below to log in',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Form } from '@inertiajs/svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import PasswordInput from '@/components/PasswordInput.svelte';
|
||||
import TextLink from '@/components/TextLink.svelte';
|
||||
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 { Spinner } from '@/components/ui/spinner';
|
||||
import { register } from '@/routes';
|
||||
import { store } from '@/routes/login';
|
||||
import { request } from '@/routes/password';
|
||||
|
||||
let {
|
||||
status = '',
|
||||
canResetPassword,
|
||||
canRegister,
|
||||
}: {
|
||||
status?: string;
|
||||
canResetPassword: boolean;
|
||||
canRegister: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AppHead title="Log in" />
|
||||
|
||||
{#if status}
|
||||
<div class="mb-4 text-center text-sm font-medium text-green-600">
|
||||
{status}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Form
|
||||
{...store.form()}
|
||||
resetOnSuccess={['password']}
|
||||
class="flex flex-col gap-6"
|
||||
>
|
||||
{#snippet children({ errors, processing })}
|
||||
<div class="grid gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="password">Password</Label>
|
||||
{#if canResetPassword}
|
||||
<TextLink href={request()} class="text-sm">
|
||||
Forgot password?
|
||||
</TextLink>
|
||||
{/if}
|
||||
</div>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<Label for="remember" class="flex items-center space-x-3">
|
||||
<Checkbox id="remember" name="remember" />
|
||||
<span>Remember me</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="mt-4 w-full"
|
||||
disabled={processing}
|
||||
data-test="login-button"
|
||||
>
|
||||
{#if processing}<Spinner />{/if}
|
||||
Log in
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if canRegister}
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
Don't have an account?
|
||||
<TextLink href={register()}>Sign up</TextLink>
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Form>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script module lang="ts">
|
||||
export const layout = {
|
||||
title: 'Create an account',
|
||||
description: 'Enter your details below to create your account',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Form } from '@inertiajs/svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import PasswordInput from '@/components/PasswordInput.svelte';
|
||||
import TextLink from '@/components/TextLink.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { login } from '@/routes';
|
||||
import { store } from '@/routes/register';
|
||||
</script>
|
||||
|
||||
<AppHead title="Register" />
|
||||
|
||||
<Form
|
||||
{...store.form()}
|
||||
resetOnSuccess={['password', 'password_confirmation']}
|
||||
class="flex flex-col gap-6"
|
||||
>
|
||||
{#snippet children({ errors, processing })}
|
||||
<div class="grid gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
required
|
||||
autocomplete="name"
|
||||
name="name"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<InputError message={errors.name} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
autocomplete="email"
|
||||
name="email"
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
<InputError message={errors.email} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation">Confirm password</Label>
|
||||
<PasswordInput
|
||||
id="password_confirmation"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
name="password_confirmation"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<InputError message={errors.password_confirmation} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="mt-2 w-full"
|
||||
disabled={processing}
|
||||
data-test="register-user-button"
|
||||
>
|
||||
{#if processing}<Spinner />{/if}
|
||||
Create account
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
Already have an account?
|
||||
<TextLink href={login()} class="underline underline-offset-4">
|
||||
Log in
|
||||
</TextLink>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script module lang="ts">
|
||||
export const layout = {
|
||||
title: 'Reset password',
|
||||
description: 'Please enter your new password below',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Form } from '@inertiajs/svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import PasswordInput from '@/components/PasswordInput.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { update } from '@/routes/password';
|
||||
|
||||
let {
|
||||
token,
|
||||
email,
|
||||
}: {
|
||||
token: string;
|
||||
email: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AppHead title="Reset password" />
|
||||
|
||||
<Form
|
||||
{...update.form()}
|
||||
transform={(data) => ({ ...data, token, email })}
|
||||
resetOnSuccess={['password', 'password_confirmation']}
|
||||
>
|
||||
{#snippet children({ errors, processing })}
|
||||
<div class="grid gap-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
autocomplete="email"
|
||||
value={email}
|
||||
class="mt-1 block w-full"
|
||||
readonly
|
||||
/>
|
||||
<InputError message={errors.email} class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
class="mt-1 block w-full"
|
||||
placeholder="Password"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation">Confirm password</Label>
|
||||
<PasswordInput
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
autocomplete="new-password"
|
||||
class="mt-1 block w-full"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<InputError message={errors.password_confirmation} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="mt-4 w-full"
|
||||
disabled={processing}
|
||||
data-test="reset-password-button"
|
||||
>
|
||||
{#if processing}<Spinner />{/if}
|
||||
Reset password
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script lang="ts">
|
||||
import { Form, setLayoutProps } from '@inertiajs/svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
InputOTP,
|
||||
InputOTPGroup,
|
||||
InputOTPSlot,
|
||||
} from '@/components/ui/input-otp';
|
||||
import { store } from '@/routes/two-factor/login';
|
||||
import type { TwoFactorConfigContent } from '@/types';
|
||||
|
||||
let showRecoveryInput = $state(false);
|
||||
let code = $state('');
|
||||
|
||||
const authConfigContent: TwoFactorConfigContent = $derived.by(() => {
|
||||
if (showRecoveryInput) {
|
||||
return {
|
||||
title: 'Recovery code',
|
||||
description:
|
||||
'Please confirm access to your account by entering one of your emergency recovery codes.',
|
||||
buttonText: 'login using an authentication code',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Authentication code',
|
||||
description:
|
||||
'Enter the authentication code provided by your authenticator application.',
|
||||
buttonText: 'login using a recovery code',
|
||||
};
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
setLayoutProps({
|
||||
title: authConfigContent.title,
|
||||
description: authConfigContent.description,
|
||||
});
|
||||
});
|
||||
|
||||
function toggleRecoveryMode(clearErrors: () => void) {
|
||||
showRecoveryInput = !showRecoveryInput;
|
||||
clearErrors();
|
||||
code = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppHead title="Two-factor authentication" />
|
||||
|
||||
<div class="space-y-6">
|
||||
{#if !showRecoveryInput}
|
||||
<Form
|
||||
{...store.form()}
|
||||
class="space-y-4"
|
||||
resetOnError
|
||||
onError={() => (code = '')}
|
||||
>
|
||||
{#snippet children({ errors, processing, clearErrors })}
|
||||
<input type="hidden" name="code" value={code} />
|
||||
<div
|
||||
class="flex flex-col items-center justify-center space-y-3 text-center"
|
||||
>
|
||||
<div class="flex w-full items-center justify-center">
|
||||
<InputOTP
|
||||
id="otp"
|
||||
bind:value={code}
|
||||
maxlength={6}
|
||||
disabled={processing}
|
||||
>
|
||||
<InputOTPGroup>
|
||||
{#each { length: 6 } as _, i (i)}
|
||||
<InputOTPSlot index={i} />
|
||||
{/each}
|
||||
</InputOTPGroup>
|
||||
</InputOTP>
|
||||
</div>
|
||||
<InputError message={errors.code} />
|
||||
</div>
|
||||
<Button type="submit" class="w-full" disabled={processing}
|
||||
>Continue</Button
|
||||
>
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
<span>or you can </span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||
onclick={() => toggleRecoveryMode(clearErrors)}
|
||||
>
|
||||
{authConfigContent.buttonText}
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form>
|
||||
{:else}
|
||||
<Form {...store.form()} class="space-y-4" resetOnError>
|
||||
{#snippet children({ errors, processing, clearErrors })}
|
||||
<Input
|
||||
name="recovery_code"
|
||||
type="text"
|
||||
placeholder="Enter recovery code"
|
||||
required
|
||||
/>
|
||||
<InputError message={errors.recovery_code} />
|
||||
<Button type="submit" class="w-full" disabled={processing}
|
||||
>Continue</Button
|
||||
>
|
||||
|
||||
<div class="text-center text-sm text-muted-foreground">
|
||||
<span>or you can </span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-foreground underline decoration-neutral-300 underline-offset-4 transition-colors duration-300 ease-out hover:decoration-current! dark:decoration-neutral-500"
|
||||
onclick={() => toggleRecoveryMode(clearErrors)}
|
||||
>
|
||||
{authConfigContent.buttonText}
|
||||
</button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script module lang="ts">
|
||||
export const layout = {
|
||||
title: 'Verify email',
|
||||
description:
|
||||
'Please verify your email address by clicking on the link we just emailed to you.',
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Form } from '@inertiajs/svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import TextLink from '@/components/TextLink.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { logout } from '@/routes';
|
||||
import { send } from '@/routes/verification';
|
||||
|
||||
let {
|
||||
status = '',
|
||||
}: {
|
||||
status?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<AppHead title="Email verification" />
|
||||
|
||||
{#if status === 'verification-link-sent'}
|
||||
<div class="mb-4 text-center text-sm font-medium text-green-600">
|
||||
A new verification link has been sent to the email address you provided
|
||||
during registration.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Form {...send.form()} class="space-y-6 text-center">
|
||||
{#snippet children({ processing })}
|
||||
<Button type="submit" disabled={processing} variant="secondary">
|
||||
{#if processing}<Spinner />{/if}
|
||||
Resend verification email
|
||||
</Button>
|
||||
|
||||
<TextLink href={logout()} as="button" class="mx-auto block text-sm">
|
||||
Log out
|
||||
</TextLink>
|
||||
{/snippet}
|
||||
</Form>
|
||||
@@ -0,0 +1,102 @@
|
||||
<script module lang="ts">
|
||||
import { index as postsIndex, create as postsCreate } from '@/routes/posts';
|
||||
|
||||
export const layout = {
|
||||
breadcrumbs: [
|
||||
{ title: 'Posts', href: postsIndex() },
|
||||
{ title: 'Create Post', href: postsCreate() },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { useForm } from '@inertiajs/svelte';
|
||||
import { store } from '@/actions/App/Http/Controllers/PostController';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
const form = useForm({
|
||||
title: '',
|
||||
slug: '',
|
||||
content: '',
|
||||
status: 'draft',
|
||||
});
|
||||
|
||||
let slugManuallyEdited = false;
|
||||
|
||||
function handleTitleInput(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
form.title = target.value;
|
||||
if (!slugManuallyEdited) {
|
||||
form.slug = target.value
|
||||
.toLowerCase()
|
||||
.replace(/[\s_]+/g, '-')
|
||||
.replace(/[^\w-]+/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
function submit(e: Event) {
|
||||
e.preventDefault();
|
||||
form.post(store.url());
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppHead title="Create Post" />
|
||||
|
||||
<div class="w-full space-y-6 p-4">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Create Post</h1>
|
||||
|
||||
<form onsubmit={submit} class="space-y-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="title">Title</Label>
|
||||
<Input
|
||||
id="title"
|
||||
value={form.title}
|
||||
oninput={handleTitleInput}
|
||||
required
|
||||
/>
|
||||
<InputError message={form.errors.title} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="slug">Slug</Label>
|
||||
<Input
|
||||
id="slug"
|
||||
value={form.slug}
|
||||
oninput={() => (slugManuallyEdited = true)}
|
||||
required
|
||||
/>
|
||||
<InputError message={form.errors.slug} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="content">Content</Label>
|
||||
<textarea
|
||||
id="content"
|
||||
value={form.content}
|
||||
rows="10"
|
||||
required
|
||||
class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
></textarea>
|
||||
<InputError message={form.errors.content} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Status</Label>
|
||||
<select
|
||||
id="status"
|
||||
bind:value={form.status}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="publish">Publish</option>
|
||||
</select>
|
||||
<InputError message={form.errors.status} />
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={form.processing}>Create Post</Button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script module lang="ts">
|
||||
import { index as postsIndex } from '@/routes/posts';
|
||||
|
||||
export const layout = {
|
||||
breadcrumbs: [
|
||||
{ title: 'Posts', href: postsIndex() },
|
||||
{ title: 'Edit Post', href: postsIndex() },
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { useForm } from '@inertiajs/svelte';
|
||||
import { update } from '@/actions/App/Http/Controllers/PostController';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
let { post = $bindable() } = $props();
|
||||
|
||||
const form = useForm({
|
||||
title: $state.snapshot(post).title,
|
||||
slug: $state.snapshot(post).slug,
|
||||
content: $state.snapshot(post).content,
|
||||
status: $state.snapshot(post).status,
|
||||
});
|
||||
|
||||
function submit(e: Event) {
|
||||
e.preventDefault();
|
||||
form.put(update.url(post.slug));
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppHead title="Edit Post" />
|
||||
|
||||
<div class="w-full space-y-6 p-4">
|
||||
<h1 class="text-2xl font-bold tracking-tight">Edit Post</h1>
|
||||
|
||||
<form onsubmit={submit} class="space-y-6">
|
||||
<div class="grid gap-2">
|
||||
<Label for="title">Title</Label>
|
||||
<Input id="title" value={form.title} required />
|
||||
<InputError message={form.errors.title} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="slug">Slug</Label>
|
||||
<Input id="slug" value={form.slug} required />
|
||||
<InputError message={form.errors.slug} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="content">Content</Label>
|
||||
<textarea
|
||||
id="content"
|
||||
bind:value={form.content}
|
||||
rows="10"
|
||||
required
|
||||
class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
></textarea>
|
||||
<InputError message={form.errors.content} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="status">Status</Label>
|
||||
<select
|
||||
id="status"
|
||||
bind:value={form.status}
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="publish">Publish</option>
|
||||
</select>
|
||||
<InputError message={form.errors.status} />
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={form.processing}>Update Post</Button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script module lang="ts">
|
||||
import { index as postsIndex } from '@/routes/posts';
|
||||
|
||||
export const layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Posts',
|
||||
href: postsIndex(),
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Link, router } from '@inertiajs/svelte';
|
||||
import {
|
||||
create as postsCreate,
|
||||
edit as postsEdit,
|
||||
destroy as postsDestroy,
|
||||
} from '@/routes/posts';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toUrl } from '@/lib/utils';
|
||||
|
||||
let { posts } = $props();
|
||||
|
||||
function deletePost(post: any) {
|
||||
if (confirm(`Are you sure you want to delete "${post.title}"?`)) {
|
||||
router.delete(postsDestroy.url(post.slug));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppHead title="Posts" />
|
||||
|
||||
<div class="w-full space-y-6 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold tracking-tight">Posts</h1>
|
||||
<p class="text-sm text-muted-foreground">Manage your blog posts</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
{#snippet children(props)}
|
||||
<Link
|
||||
{...props}
|
||||
href={toUrl(postsCreate())}
|
||||
class={props.class}
|
||||
>
|
||||
Create Post
|
||||
</Link>
|
||||
{/snippet}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b bg-muted/50">
|
||||
<th class="px-4 py-3 text-left font-medium">Title</th>
|
||||
<th class="px-4 py-3 text-left font-medium">Slug</th>
|
||||
<th class="px-4 py-3 text-left font-medium">Status</th>
|
||||
<th class="px-4 py-3 text-right font-medium">Actions</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each posts.data as post (post.id)}
|
||||
<tr
|
||||
class="border-b transition-colors hover:bg-muted/50"
|
||||
>
|
||||
<td class="px-4 py-3 font-medium">{post.title}</td>
|
||||
<td class="px-4 py-3 text-muted-foreground"
|
||||
>{post.slug}</td
|
||||
>
|
||||
<td class="px-4 py-3">
|
||||
<Badge
|
||||
variant={post.status === 'publish'
|
||||
? 'default'
|
||||
: 'secondary'}
|
||||
>
|
||||
{post.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<div
|
||||
class="flex items-center justify-end gap-2"
|
||||
>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
{#snippet children(props)}
|
||||
<Link
|
||||
{...props}
|
||||
href={toUrl(
|
||||
postsEdit(post.slug),
|
||||
)}
|
||||
class={props.class}>Edit</Link
|
||||
>
|
||||
{/snippet}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onclick={() => deletePost(post)}
|
||||
>Delete</Button
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td
|
||||
colspan="4"
|
||||
class="px-4 py-8 text-center text-muted-foreground"
|
||||
>No posts found.</td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"posts": {
|
||||
"current_page": 1,
|
||||
"data": [
|
||||
{
|
||||
"id": 30,
|
||||
"title": "Non et illum sed quibusdam.",
|
||||
"slug": "non-et-illum-sed-quibusdam",
|
||||
"content": "Dolore animi dolore tempore. Reprehenderit quasi impedit dolor est dolor quis.\n\nPerspiciatis facere necessitatibus exercitationem praesentium. Odio minus itaque distinctio aut aliquid sed. Dolorem provident quo quae.\n\nNon sint asperiores aut sed. In quis fuga expedita numquam nostrum qui. Odio quasi doloribus impedit commodi itaque quidem recusandae. Voluptates qui hic est veritatis et blanditiis.",
|
||||
"status": "draft",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T16:28:03.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"title": "Illum et est maiores enim numquam enim.",
|
||||
"slug": "illum-et-est-maiores-enim-numquam-enim",
|
||||
"content": "Magni quo in nulla ut. Sint maxime voluptatem distinctio eaque recusandae. Ullam repudiandae quisquam vel harum quos nulla corrupti. Corporis molestiae incidunt soluta officiis aut assumenda.\n\nVel sit ipsam quia harum. Autem voluptatum assumenda impedit aut. Quos accusamus repellendus est dignissimos eum voluptate dolores nihil. Autem et dolorum quo mollitia ad illum expedita.\n\nLaudantium placeat iure quas laborum a molestiae laudantium. Est officia voluptas dolor quia. Exercitationem possimus facere ducimus. Animi ut molestias quia.",
|
||||
"status": "publish",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"title": "Nobis sed blanditiis tempore est.",
|
||||
"slug": "nobis-sed-blanditiis-tempore-est",
|
||||
"content": "Deleniti quos veniam ut accusantium voluptas quia. Delectus magni fugiat ullam laborum nihil recusandae et. Expedita praesentium fugit dolore et error qui itaque.\n\nQuod nemo adipisci ad officia animi perspiciatis non. Sed fugiat non aliquam eaque adipisci molestias natus. Veniam provident et ut quia.\n\nEst ipsa reiciendis enim numquam cupiditate et et. Soluta dolorem adipisci impedit quos ducimus laudantium. Ipsa est earum distinctio itaque magnam.",
|
||||
"status": "draft",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"title": "Reiciendis magni consequatur omnis molestiae illo molestiae tempora pariatur.",
|
||||
"slug": "reiciendis-magni-consequatur-omnis-molestiae-illo-molestiae-tempora-pariatur",
|
||||
"content": "Quia labore distinctio odio eligendi est sit sit. Saepe qui eveniet optio quis sapiente mollitia provident. Molestias officiis sed possimus velit. Quam laboriosam magni accusantium fugit error quibusdam reprehenderit.\n\nEa omnis aut sit. Ipsam rem iste quo iure illum et facilis. Labore quasi accusantium et qui est.\n\nExcepturi sed qui quibusdam aut quidem est. Earum est qui ea corporis. Et sed nobis nobis tenetur debitis recusandae aut.",
|
||||
"status": "draft",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"title": "Autem omnis est voluptatem nostrum qui.",
|
||||
"slug": "autem-omnis-est-voluptatem-nostrum-qui",
|
||||
"content": "Reiciendis sed explicabo sunt ducimus. Aut vitae ullam itaque voluptatibus ullam aut vel earum. Quis nisi aut magnam sed placeat aperiam hic. Omnis eligendi facilis et et asperiores et.\n\nAssumenda qui vel eius. Facilis est dolores et earum est dolores nisi labore. Cupiditate blanditiis quis perspiciatis. Corporis quam laboriosam iusto voluptas repudiandae aut quas.\n\nPariatur delectus ea vitae voluptate omnis voluptate optio. Quia eum unde maiores tenetur saepe nemo voluptatibus. Exercitationem ad quis illum. Explicabo omnis nemo ut quia alias quidem. Optio numquam assumenda corporis dolore deserunt.",
|
||||
"status": "publish",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"title": "Provident ipsam non quo culpa.",
|
||||
"slug": "provident-ipsam-non-quo-culpa",
|
||||
"content": "Qui harum aut consequatur labore dolores. Quis at suscipit enim expedita ratione commodi. Mollitia praesentium nesciunt assumenda ut pariatur temporibus vel. Et consequatur est possimus autem provident praesentium dolorem.\n\nEnim eius ipsum at provident consequatur vitae. Voluptates magni fuga accusamus animi cupiditate. Aperiam dolorem incidunt itaque aliquam aut minus. Minima maxime labore aut totam expedita aliquam et.\n\nAut repellat et ea in eveniet incidunt voluptas. Qui dolor quam ut et non. Ullam dolorem vitae occaecati.",
|
||||
"status": "publish",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"title": "Iste enim qui omnis.",
|
||||
"slug": "iste-enim-qui-omnis",
|
||||
"content": "Qui dolor consequatur ut itaque quae eum aut aspernatur. Qui porro vitae accusamus sit in nam quia. Nisi laudantium architecto nostrum. Qui eligendi possimus et similique placeat eos.\n\nImpedit atque eos qui tempora qui omnis deserunt. Veniam quae sapiente nam fugit deserunt. Nisi aut dolorem fugiat est dolorem ipsum fugiat.\n\nLibero provident dolore minima quam modi. Excepturi quod sunt alias voluptates sunt. Dolorem facilis quia et recusandae. Fugit officia aut et ullam minima non officia.",
|
||||
"status": "draft",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"title": "Modi tenetur harum voluptatem accusantium dolorem et dignissimos ut.",
|
||||
"slug": "modi-tenetur-harum-voluptatem-accusantium-dolorem-et-dignissimos-ut",
|
||||
"content": "Fugiat magnam eos architecto alias. Id quod odio laudantium quia numquam qui. In qui doloribus quas fugiat voluptates voluptatum ipsum. In voluptates nobis earum distinctio tempora.\n\nIllum blanditiis qui facilis fugit molestiae et nisi. Quasi quaerat omnis et magni illo rerum. In et eveniet et blanditiis est.\n\nLabore non commodi animi doloremque facilis. Eos sunt ut nulla autem alias. Aut sit rerum nesciunt cum quidem exercitationem voluptatem. Suscipit similique quia non fugiat aut.",
|
||||
"status": "draft",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"title": "Dolores rerum tempore cupiditate minima.",
|
||||
"slug": "dolores-rerum-tempore-cupiditate-minima",
|
||||
"content": "Aliquam harum commodi iusto praesentium. Quisquam repellat nihil repellat autem.\n\nEius tempore voluptatibus illo eum et. Excepturi quia porro unde.\n\nEt vitae odit doloremque blanditiis rerum. Rerum eos est aut et eos magnam aut. Deserunt ab animi adipisci molestias eligendi.",
|
||||
"status": "publish",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"title": "Recusandae aut dolorem velit sit beatae consectetur perspiciatis.",
|
||||
"slug": "recusandae-aut-dolorem-velit-sit-beatae-consectetur-perspiciatis",
|
||||
"content": "Aut omnis dolorem maxime laboriosam tempore. Mollitia sunt unde illo. Suscipit voluptatem rerum ut et. Impedit in tenetur quia hic aperiam consequatur modi autem.\n\nVoluptas voluptatem aut alias quae. Commodi praesentium ab similique recusandae. Aliquam aperiam ullam eius consequuntur reiciendis. Est non cupiditate non.\n\nMaxime veritatis inventore sequi nisi. Est ut illo et deserunt aspernatur. Quis quasi porro facilis excepturi est odio vel eum. Quis pariatur eos repellendus enim eos et maiores.",
|
||||
"status": "draft",
|
||||
"created_at": "2026-04-15T15:02:18.000000Z",
|
||||
"updated_at": "2026-04-15T15:02:18.000000Z"
|
||||
}
|
||||
],
|
||||
"first_page_url": "http://dotter.test/posts?page=1",
|
||||
"from": 1,
|
||||
"last_page": 3,
|
||||
"last_page_url": "http://dotter.test/posts?page=3",
|
||||
"links": [
|
||||
{
|
||||
"url": null,
|
||||
"label": "« Previous",
|
||||
"page": null,
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"url": "http://dotter.test/posts?page=1",
|
||||
"label": "1",
|
||||
"page": 1,
|
||||
"active": true
|
||||
},
|
||||
{
|
||||
"url": "http://dotter.test/posts?page=2",
|
||||
"label": "2",
|
||||
"page": 2,
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"url": "http://dotter.test/posts?page=3",
|
||||
"label": "3",
|
||||
"page": 3,
|
||||
"active": false
|
||||
},
|
||||
{
|
||||
"url": "http://dotter.test/posts?page=2",
|
||||
"label": "Next »",
|
||||
"page": 2,
|
||||
"active": false
|
||||
}
|
||||
],
|
||||
"next_page_url": "http://dotter.test/posts?page=2",
|
||||
"path": "http://dotter.test/posts",
|
||||
"per_page": 10,
|
||||
"prev_page_url": null,
|
||||
"to": 10,
|
||||
"total": 30
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<script module lang="ts">
|
||||
import { edit as editAppearance } from '@/routes/appearance';
|
||||
|
||||
export const layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Appearance settings',
|
||||
href: editAppearance(),
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import AppearanceTabs from '@/components/AppearanceTabs.svelte';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import Heading from '@/components/Heading.svelte';
|
||||
</script>
|
||||
|
||||
<AppHead title="Appearance settings" />
|
||||
|
||||
<h1 class="sr-only">Appearance settings</h1>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Appearance settings"
|
||||
description="Update your account's appearance settings"
|
||||
/>
|
||||
<AppearanceTabs />
|
||||
</div>
|
||||
@@ -0,0 +1,113 @@
|
||||
<script module lang="ts">
|
||||
import { edit } from '@/routes/profile';
|
||||
|
||||
export const layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Profile settings',
|
||||
href: edit(),
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Form, page } from '@inertiajs/svelte';
|
||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import DeleteUser from '@/components/DeleteUser.svelte';
|
||||
import Heading from '@/components/Heading.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import TextLink from '@/components/TextLink.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { send } from '@/routes/verification';
|
||||
|
||||
let {
|
||||
mustVerifyEmail,
|
||||
status = '',
|
||||
}: {
|
||||
mustVerifyEmail: boolean;
|
||||
status?: string;
|
||||
} = $props();
|
||||
|
||||
const user = $derived(page.props.auth.user);
|
||||
</script>
|
||||
|
||||
<AppHead title="Profile settings" />
|
||||
|
||||
<h1 class="sr-only">Profile settings</h1>
|
||||
|
||||
<div class="flex flex-col space-y-6">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Profile information"
|
||||
description="Update your name and email address"
|
||||
/>
|
||||
|
||||
<Form
|
||||
{...ProfileController.update.form()}
|
||||
class="space-y-6"
|
||||
options={{ preserveScroll: true }}
|
||||
>
|
||||
{#snippet children({ errors, processing })}
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
class="mt-1 block w-full"
|
||||
value={user.name}
|
||||
required
|
||||
autocomplete="name"
|
||||
placeholder="Full name"
|
||||
/>
|
||||
<InputError class="mt-2" message={errors.name} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="email">Email address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
name="email"
|
||||
class="mt-1 block w-full"
|
||||
value={user.email}
|
||||
required
|
||||
autocomplete="username"
|
||||
placeholder="Email address"
|
||||
/>
|
||||
<InputError class="mt-2" message={errors.email} />
|
||||
</div>
|
||||
|
||||
{#if mustVerifyEmail && !user.email_verified_at}
|
||||
<div>
|
||||
<p class="-mt-4 text-sm text-muted-foreground">
|
||||
Your email address is unverified.
|
||||
<TextLink href={send()} as="button">
|
||||
Click here to resend the verification email.
|
||||
</TextLink>
|
||||
</p>
|
||||
|
||||
{#if status === 'verification-link-sent'}
|
||||
<div class="mt-2 text-sm font-medium text-green-600">
|
||||
A new verification link has been sent to your email
|
||||
address.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
data-test="update-profile-button">Save</Button
|
||||
>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<DeleteUser />
|
||||
@@ -0,0 +1,181 @@
|
||||
<script module lang="ts">
|
||||
import { edit } from '@/routes/security';
|
||||
|
||||
export const layout = {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Security settings',
|
||||
href: edit(),
|
||||
},
|
||||
],
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Form } from '@inertiajs/svelte';
|
||||
import ShieldCheck from 'lucide-svelte/icons/shield-check';
|
||||
import { onDestroy } from 'svelte';
|
||||
import SecurityController from '@/actions/App/Http/Controllers/Settings/SecurityController';
|
||||
import AppHead from '@/components/AppHead.svelte';
|
||||
import Heading from '@/components/Heading.svelte';
|
||||
import InputError from '@/components/InputError.svelte';
|
||||
import PasswordInput from '@/components/PasswordInput.svelte';
|
||||
import TwoFactorRecoveryCodes from '@/components/TwoFactorRecoveryCodes.svelte';
|
||||
import TwoFactorSetupModal from '@/components/TwoFactorSetupModal.svelte';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { twoFactorAuthState } from '@/lib/twoFactorAuth.svelte';
|
||||
import { disable, enable } from '@/routes/two-factor';
|
||||
|
||||
let {
|
||||
canManageTwoFactor = false,
|
||||
requiresConfirmation = false,
|
||||
twoFactorEnabled = false,
|
||||
}: {
|
||||
canManageTwoFactor?: boolean;
|
||||
requiresConfirmation?: boolean;
|
||||
twoFactorEnabled?: boolean;
|
||||
} = $props();
|
||||
|
||||
const twoFactorAuth = twoFactorAuthState();
|
||||
let showSetupModal = $state(false);
|
||||
|
||||
onDestroy(() => twoFactorAuth.clearTwoFactorAuthData());
|
||||
</script>
|
||||
|
||||
<AppHead title="Security settings" />
|
||||
|
||||
<h1 class="sr-only">Security settings</h1>
|
||||
|
||||
<div class="space-y-6">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Update password"
|
||||
description="Ensure your account is using a long, random password to stay secure"
|
||||
/>
|
||||
|
||||
<Form
|
||||
{...SecurityController.update.form()}
|
||||
class="space-y-6"
|
||||
options={{ preserveScroll: true }}
|
||||
resetOnSuccess
|
||||
resetOnError={['password', 'password_confirmation', 'current_password']}
|
||||
>
|
||||
{#snippet children({ errors, processing })}
|
||||
<div class="grid gap-2">
|
||||
<Label for="current_password">Current password</Label>
|
||||
<PasswordInput
|
||||
id="current_password"
|
||||
name="current_password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="current-password"
|
||||
placeholder="Current password"
|
||||
/>
|
||||
<InputError message={errors.current_password} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password">New password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
placeholder="New password"
|
||||
/>
|
||||
<InputError message={errors.password} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password_confirmation">Confirm password</Label>
|
||||
<PasswordInput
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
placeholder="Confirm password"
|
||||
/>
|
||||
<InputError message={errors.password_confirmation} />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
data-test="update-password-button"
|
||||
>
|
||||
Save password
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
{#if canManageTwoFactor}
|
||||
<div class="space-y-6">
|
||||
<Heading
|
||||
variant="small"
|
||||
title="Two-factor authentication"
|
||||
description="Manage your two-factor authentication settings"
|
||||
/>
|
||||
|
||||
{#if !twoFactorEnabled}
|
||||
<div class="flex flex-col items-start justify-start space-y-4">
|
||||
<p class="text-muted-foreground text-sm">
|
||||
When you enable two-factor authentication, you will be
|
||||
prompted for a secure pin during login. This pin can be
|
||||
retrieved from a TOTP-supported application on your phone.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
{#if twoFactorAuth.hasSetupData()}
|
||||
<Button onclick={() => (showSetupModal = true)}>
|
||||
<ShieldCheck class="size-4" />Continue setup
|
||||
</Button>
|
||||
{:else}
|
||||
<Form
|
||||
{...enable.form()}
|
||||
onSuccess={() => (showSetupModal = true)}
|
||||
>
|
||||
{#snippet children({ processing })}
|
||||
<Button type="submit" disabled={processing}>
|
||||
Enable 2FA
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Form>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-start justify-start space-y-4">
|
||||
<p class="text-muted-foreground text-sm">
|
||||
You will be prompted for a secure, random pin during login,
|
||||
which you can retrieve from the TOTP-supported application
|
||||
on your phone.
|
||||
</p>
|
||||
|
||||
<div class="relative inline">
|
||||
<Form {...disable.form()}>
|
||||
{#snippet children({ processing })}
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="submit"
|
||||
disabled={processing}
|
||||
>
|
||||
Disable 2FA
|
||||
</Button>
|
||||
{/snippet}
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
<TwoFactorRecoveryCodes />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<TwoFactorSetupModal
|
||||
bind:isOpen={showSetupModal}
|
||||
{requiresConfirmation}
|
||||
{twoFactorEnabled}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in new issue
Block a user