first commit
linter / quality (push) Canceled after 0s
tests / ci (8.3) (push) Canceled after 0s
tests / ci (8.4) (push) Canceled after 0s
tests / ci (8.5) (push) Canceled after 0s

This commit is contained in:
salam committed 2026-10-06 18:53:03 +02:00
commit e687b4437b
418 files changed
+33577

No files matched your search

+44
View File
@@ -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>
+40
View File
@@ -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>
+106
View File
@@ -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>
+99
View File
@@ -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>
+102
View File
@@ -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>
+81
View File
@@ -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>
+123
View File
@@ -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>
+139
View File
@@ -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": "&laquo; 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 &raquo;",
"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>
+113
View File
@@ -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 />
+181
View File
@@ -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}