This commit is contained in:
Simon Pocrnjič
2026-01-10 20:11:20 +01:00
parent 711438d79f
commit c4d2f6e473
21 changed files with 1744 additions and 1889 deletions
@@ -1,12 +1,12 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import ActionSection from '@/Components/ActionSection.vue';
import DangerButton from '@/Components/DangerButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/Components/ui/alert-dialog';
import InputError from '@/Components/InputError.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Trash2, AlertTriangle } from 'lucide-vue-next';
const confirmingUserDeletion = ref(false);
const passwordInput = ref(null);
@@ -38,65 +38,68 @@ const closeModal = () => {
</script>
<template>
<ActionSection>
<template #title>
Delete Account
</template>
<Card>
<CardHeader>
<div class="flex items-center gap-2">
<Trash2 class="h-5 w-5 text-destructive" />
<CardTitle>Delete Account</CardTitle>
</div>
<CardDescription>
Permanently delete your account.
</CardDescription>
</CardHeader>
<template #description>
Permanently delete your account.
</template>
<template #content>
<div class="max-w-xl text-sm text-gray-600">
Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.
<CardContent class="space-y-4">
<div class="rounded-lg border border-destructive/50 bg-destructive/10 p-4">
<div class="flex gap-3">
<AlertTriangle class="h-5 w-5 text-destructive flex-shrink-0 mt-0.5" />
<p class="text-sm text-foreground">
Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.
</p>
</div>
</div>
<div class="mt-5">
<DangerButton @click="confirmUserDeletion">
Delete Account
</DangerButton>
</div>
<Button variant="destructive" @click="confirmUserDeletion">
<Trash2 class="h-4 w-4 mr-2" />
Delete Account
</Button>
</CardContent>
<!-- Delete Account Confirmation Modal -->
<DialogModal :show="confirmingUserDeletion" @close="closeModal">
<template #title>
Delete Account
</template>
<!-- Delete Account Confirmation Dialog -->
<AlertDialog :open="confirmingUserDeletion" @update:open="closeModal">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Account</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.
</AlertDialogDescription>
</AlertDialogHeader>
<template #content>
Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.
<div class="py-4">
<Input
ref="passwordInput"
v-model="form.password"
type="password"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="deleteUser"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
<div class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="deleteUser"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
</template>
<template #footer>
<SecondaryButton @click="closeModal">
<AlertDialogFooter>
<Button variant="outline" @click="closeModal">
Cancel
</SecondaryButton>
<DangerButton
class="ms-3"
:class="{ 'opacity-25': form.processing }"
</Button>
<Button
variant="destructive"
:disabled="form.processing"
@click="deleteUser"
>
Delete Account
</DangerButton>
</template>
</DialogModal>
</template>
</ActionSection>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card>
</template>
@@ -1,141 +1,159 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import ActionSection from '@/Components/ActionSection.vue';
import DialogModal from '@/Components/DialogModal.vue';
import InputError from '@/Components/InputError.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { ref } from "vue";
import { useForm } from "@inertiajs/vue3";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/Components/ui/card";
import { Button } from "@/Components/ui/button";
import { Input } from "@/Components/ui/input";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/Components/ui/dialog";
import InputError from "@/Components/InputError.vue";
import { Monitor, Smartphone, LogOut, CheckCircle } from "lucide-vue-next";
defineProps({
sessions: Array,
sessions: Array,
});
const confirmingLogout = ref(false);
const passwordInput = ref(null);
const form = useForm({
password: '',
password: "",
});
const confirmLogout = () => {
confirmingLogout.value = true;
confirmingLogout.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
setTimeout(() => passwordInput.value.focus(), 250);
};
const logoutOtherBrowserSessions = () => {
form.delete(route('other-browser-sessions.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value.focus(),
onFinish: () => form.reset(),
});
form.delete(route("other-browser-sessions.destroy"), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value.focus(),
onFinish: () => form.reset(),
});
};
const closeModal = () => {
confirmingLogout.value = false;
confirmingLogout.value = false;
form.reset();
form.reset();
};
</script>
<template>
<ActionSection>
<template #title>
Browser Sessions
</template>
<Card>
<CardHeader>
<div class="flex items-center gap-2">
<LogOut class="h-5 w-5 text-muted-foreground" />
<CardTitle>Browser Sessions</CardTitle>
</div>
<CardDescription>
Manage and log out your active sessions on other browsers and devices.
</CardDescription>
</CardHeader>
<template #description>
Manage and log out your active sessions on other browsers and devices.
</template>
<CardContent class="space-y-6">
<p class="text-sm text-muted-foreground">
If necessary, you may log out of all of your other browser sessions across all of
your devices. Some of your recent sessions are listed below; however, this list
may not be exhaustive. If you feel your account has been compromised, you should
also update your password.
</p>
<template #content>
<div class="max-w-xl text-sm text-gray-600">
If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.
<!-- Other Browser Sessions -->
<div v-if="sessions.length > 0" class="space-y-4">
<div
v-for="(session, i) in sessions"
:key="i"
class="flex items-center gap-3 rounded-lg border p-3"
>
<div class="flex-shrink-0">
<Monitor
v-if="session.agent.is_desktop"
class="h-8 w-8 text-muted-foreground"
/>
<Smartphone v-else class="h-8 w-8 text-muted-foreground" />
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium">
{{ session.agent.platform ? session.agent.platform : "Unknown" }} -
{{ session.agent.browser ? session.agent.browser : "Unknown" }}
</div>
<!-- Other Browser Sessions -->
<div v-if="sessions.length > 0" class="mt-5 space-y-6">
<div v-for="(session, i) in sessions" :key="i" class="flex items-center">
<div>
<svg v-if="session.agent.is_desktop" class="w-8 h-8 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
</svg>
<svg v-else class="w-8 h-8 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</svg>
</div>
<div class="ms-3">
<div class="text-sm text-gray-600">
{{ session.agent.platform ? session.agent.platform : 'Unknown' }} - {{ session.agent.browser ? session.agent.browser : 'Unknown' }}
</div>
<div>
<div class="text-xs text-gray-500">
{{ session.ip_address }},
<span v-if="session.is_current_device" class="text-green-500 font-semibold">This device</span>
<span v-else>Last active {{ session.last_active }}</span>
</div>
</div>
</div>
</div>
<div class="text-xs text-muted-foreground mt-1">
{{ session.ip_address }}
<span
v-if="session.is_current_device"
class="inline-flex items-center ml-2 text-green-600 dark:text-green-400 font-semibold"
>
This device
</span>
<span v-else class="ml-1"> · Last active {{ session.last_active }} </span>
</div>
</div>
</div>
</div>
<div class="flex items-center mt-5">
<PrimaryButton @click="confirmLogout">
Log Out Other Browser Sessions
</PrimaryButton>
<div class="flex items-center gap-3">
<Button @click="confirmLogout">
<LogOut class="h-4 w-4 mr-2" />
Log Out Other Browser Sessions
</Button>
<ActionMessage :on="form.recentlySuccessful" class="ms-3">
Done.
</ActionMessage>
</div>
<div
v-if="form.recentlySuccessful"
class="flex items-center gap-1.5 text-sm text-muted-foreground"
>
<CheckCircle class="h-4 w-4 text-green-600" />
<span>Done.</span>
</div>
</div>
</CardContent>
<!-- Log Out Other Devices Confirmation Modal -->
<DialogModal :show="confirmingLogout" @close="closeModal">
<template #title>
Log Out Other Browser Sessions
</template>
<!-- Log Out Other Devices Confirmation Dialog -->
<Dialog :open="confirmingLogout" @update:open="closeModal">
<DialogContent>
<DialogHeader>
<DialogTitle>Log Out Other Browser Sessions</DialogTitle>
<DialogDescription>
Please enter your password to confirm you would like to log out of your other
browser sessions across all of your devices.
</DialogDescription>
</DialogHeader>
<template #content>
Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.
<div class="py-4">
<Input
ref="passwordInput"
v-model="form.password"
type="password"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="logoutOtherBrowserSessions"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
<div class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="logoutOtherBrowserSessions"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
</template>
<template #footer>
<SecondaryButton @click="closeModal">
Cancel
</SecondaryButton>
<PrimaryButton
class="ms-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="logoutOtherBrowserSessions"
>
Log Out Other Browser Sessions
</PrimaryButton>
</template>
</DialogModal>
</template>
</ActionSection>
<DialogFooter>
<Button variant="outline" @click="closeModal"> Cancel </Button>
<Button :disabled="form.processing" @click="logoutOtherBrowserSessions">
Log Out Other Browser Sessions
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
</template>
@@ -1,14 +1,14 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { router, useForm, usePage } from '@inertiajs/vue3';
import ActionSection from '@/Components/ActionSection.vue';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Badge } from '@/Components/ui/badge';
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue';
import DangerButton from '@/Components/DangerButton.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Shield, Key, Copy, RefreshCw, CheckCircle, AlertCircle } from 'lucide-vue-next';
const props = defineProps({
requiresConfirmation: Boolean,
@@ -102,152 +102,205 @@ const disableTwoFactorAuthentication = () => {
},
});
};
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy:', err);
}
};
</script>
<template>
<ActionSection>
<template #title>
Two Factor Authentication
</template>
<Card>
<CardHeader>
<div class="flex items-center gap-2">
<Shield class="h-5 w-5 text-muted-foreground" />
<CardTitle>Two Factor Authentication</CardTitle>
</div>
<CardDescription>
Add additional security to your account using two factor authentication.
</CardDescription>
</CardHeader>
<template #description>
Add additional security to your account using two factor authentication.
</template>
<template #content>
<h3 v-if="twoFactorEnabled && ! confirming" class="text-lg font-medium text-gray-900">
You have enabled two factor authentication.
</h3>
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-medium text-gray-900">
Finish enabling two factor authentication.
</h3>
<h3 v-else class="text-lg font-medium text-gray-900">
You have not enabled two factor authentication.
</h3>
<div class="mt-3 max-w-xl text-sm text-gray-600">
<p>
When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone's Google Authenticator application.
</p>
<CardContent class="space-y-6">
<!-- Status Header -->
<div class="flex items-start gap-3">
<div class="flex-1">
<h3 v-if="twoFactorEnabled && ! confirming" class="text-lg font-semibold flex items-center gap-2">
<CheckCircle class="h-5 w-5 text-green-600" />
Two factor authentication is enabled
</h3>
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-semibold flex items-center gap-2">
<AlertCircle class="h-5 w-5 text-amber-600" />
Finish enabling two factor authentication
</h3>
<h3 v-else class="text-lg font-semibold flex items-center gap-2">
<Shield class="h-5 w-5 text-muted-foreground" />
Two factor authentication is disabled
</h3>
<p class="mt-2 text-sm text-muted-foreground">
When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone's Google Authenticator application.
</p>
</div>
</div>
<div v-if="twoFactorEnabled">
<div v-if="qrCode">
<div class="mt-4 max-w-xl text-sm text-gray-600">
<p v-if="confirming" class="font-semibold">
<!-- QR Code & Setup -->
<div v-if="twoFactorEnabled" class="space-y-6">
<div v-if="qrCode" class="space-y-4">
<div class="rounded-lg border bg-muted/50 p-4">
<p v-if="confirming" class="text-sm font-medium mb-4">
To finish enabling two factor authentication, scan the following QR code using your phone's authenticator application or enter the setup key and provide the generated OTP code.
</p>
<p v-else>
<p v-else class="text-sm text-muted-foreground mb-4">
Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application or enter the setup key.
</p>
<!-- QR Code -->
<div class="flex justify-center p-4 bg-white rounded-lg" v-html="qrCode" />
<!-- Setup Key -->
<div v-if="setupKey" class="mt-4 p-3 bg-background rounded-lg border">
<div class="flex items-center justify-between gap-2">
<div class="flex-1">
<Label class="text-xs text-muted-foreground">Setup Key</Label>
<p class="font-mono text-sm font-semibold mt-1" v-html="setupKey"></p>
</div>
<Button
type="button"
variant="ghost"
size="sm"
@click="copyToClipboard(setupKey)"
>
<Copy class="h-4 w-4" />
</Button>
</div>
</div>
</div>
<div class="mt-4 p-2 inline-block bg-white" v-html="qrCode" />
<div v-if="setupKey" class="mt-4 max-w-xl text-sm text-gray-600">
<p class="font-semibold">
Setup Key: <span v-html="setupKey"></span>
</p>
</div>
<div v-if="confirming" class="mt-4">
<InputLabel for="code" value="Code" />
<TextInput
<!-- Confirmation Code Input -->
<div v-if="confirming" class="space-y-2">
<Label for="code">Confirmation Code</Label>
<Input
id="code"
v-model="confirmationForm.code"
type="text"
name="code"
class="block mt-1 w-1/2"
inputmode="numeric"
autofocus
autocomplete="one-time-code"
placeholder="Enter 6-digit code"
class="max-w-xs"
@keyup.enter="confirmTwoFactorAuthentication"
/>
<InputError :message="confirmationForm.errors.code" class="mt-2" />
</div>
</div>
<div v-if="recoveryCodes.length > 0 && ! confirming">
<div class="mt-4 max-w-xl text-sm text-gray-600">
<p class="font-semibold">
Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
</p>
<!-- Recovery Codes -->
<div v-if="recoveryCodes.length > 0 && ! confirming" class="space-y-4">
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950">
<div class="flex items-start gap-2">
<Key class="h-5 w-5 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<p class="text-sm font-medium text-amber-900 dark:text-amber-100">
Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
</p>
</div>
</div>
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
<div v-for="code in recoveryCodes" :key="code">
{{ code }}
<div class="rounded-lg border bg-muted p-4">
<div class="grid grid-cols-2 gap-2 font-mono text-sm">
<div v-for="code in recoveryCodes" :key="code" class="flex items-center justify-between p-2 bg-background rounded border">
<span>{{ code }}</span>
<Button
type="button"
variant="ghost"
size="sm"
class="h-6 w-6 p-0"
@click="copyToClipboard(code)"
>
<Copy class="h-3 w-3" />
</Button>
</div>
</div>
</div>
</div>
</div>
<div class="mt-5">
<!-- Action Buttons -->
<div class="flex flex-wrap gap-2">
<!-- Enable -->
<div v-if="! twoFactorEnabled">
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
<PrimaryButton type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
<Button type="button" :disabled="enabling">
<Shield class="h-4 w-4 mr-2" />
Enable
</PrimaryButton>
</Button>
</ConfirmsPassword>
</div>
<div v-else>
<!-- Confirm -->
<template v-else>
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
<PrimaryButton
<Button
v-if="confirming"
type="button"
class="me-3"
:class="{ 'opacity-25': enabling }"
:disabled="enabling"
>
<CheckCircle class="h-4 w-4 mr-2" />
Confirm
</PrimaryButton>
</Button>
</ConfirmsPassword>
<!-- Regenerate Recovery Codes -->
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
<SecondaryButton
<Button
v-if="recoveryCodes.length > 0 && ! confirming"
class="me-3"
type="button"
variant="outline"
>
<RefreshCw class="h-4 w-4 mr-2" />
Regenerate Recovery Codes
</SecondaryButton>
</Button>
</ConfirmsPassword>
<!-- Show Recovery Codes -->
<ConfirmsPassword @confirmed="showRecoveryCodes">
<SecondaryButton
<Button
v-if="recoveryCodes.length === 0 && ! confirming"
class="me-3"
type="button"
variant="outline"
>
<Key class="h-4 w-4 mr-2" />
Show Recovery Codes
</SecondaryButton>
</Button>
</ConfirmsPassword>
<!-- Cancel/Disable -->
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<SecondaryButton
<Button
v-if="confirming"
:class="{ 'opacity-25': disabling }"
type="button"
variant="outline"
:disabled="disabling"
>
Cancel
</SecondaryButton>
</Button>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<DangerButton
<Button
v-if="! confirming"
:class="{ 'opacity-25': disabling }"
type="button"
variant="destructive"
:disabled="disabling"
>
Disable
</DangerButton>
</Button>
</ConfirmsPassword>
</div>
</template>
</div>
</template>
</ActionSection>
</CardContent>
</Card>
</template>
@@ -1,12 +1,12 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import FormSection from '@/Components/FormSection.vue';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/Components/ui/card';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { CheckCircle, Lock } from 'lucide-vue-next';
const passwordInput = ref(null);
const currentPasswordInput = ref(null);
@@ -38,63 +38,64 @@ const updatePassword = () => {
</script>
<template>
<FormSection @submitted="updatePassword">
<template #title>
Update Password
</template>
<Card>
<form @submit.prevent="updatePassword">
<CardHeader>
<div class="flex items-center gap-2">
<Lock class="h-5 w-5 text-muted-foreground" />
<CardTitle>Update Password</CardTitle>
</div>
<CardDescription>
Ensure your account is using a long, random password to stay secure.
</CardDescription>
</CardHeader>
<template #description>
Ensure your account is using a long, random password to stay secure.
</template>
<CardContent class="space-y-6">
<div class="space-y-2">
<Label for="current_password">Current Password</Label>
<Input
id="current_password"
ref="currentPasswordInput"
v-model="form.current_password"
type="password"
autocomplete="current-password"
/>
<InputError :message="form.errors.current_password" class="mt-2" />
</div>
<template #form>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="current_password" value="Current Password" />
<TextInput
id="current_password"
ref="currentPasswordInput"
v-model="form.current_password"
type="password"
class="mt-1 block w-full"
autocomplete="current-password"
/>
<InputError :message="form.errors.current_password" class="mt-2" />
</div>
<div class="space-y-2">
<Label for="password">New Password</Label>
<Input
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
autocomplete="new-password"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="password" value="New Password" />
<TextInput
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
<div class="space-y-2">
<Label for="password_confirmation">Confirm Password</Label>
<Input
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
autocomplete="new-password"
/>
<InputError :message="form.errors.password_confirmation" class="mt-2" />
</div>
</CardContent>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"
/>
<InputError :message="form.errors.password_confirmation" class="mt-2" />
</div>
</template>
<template #actions>
<ActionMessage :on="form.recentlySuccessful" class="me-3">
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Save
</PrimaryButton>
</template>
</FormSection>
<CardFooter class="flex items-center justify-between">
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
<span v-if="form.recentlySuccessful">Saved.</span>
</div>
<Button type="submit" :disabled="form.processing">
Save
</Button>
</CardFooter>
</form>
</Card>
</template>
@@ -1,13 +1,13 @@
<script setup>
import { ref } from 'vue';
import { Link, router, useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import FormSection from '@/Components/FormSection.vue';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/Components/ui/card';
import { Button } from '@/Components/ui/button';
import { Input } from '@/Components/ui/input';
import { Label } from '@/Components/ui/label';
import { Avatar, AvatarImage, AvatarFallback } from '@/Components/ui/avatar';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { User, Mail, Camera, Trash2, CheckCircle, AlertCircle } from 'lucide-vue-next';
const props = defineProps({
user: Object,
@@ -76,115 +76,138 @@ const clearPhotoFileInput = () => {
</script>
<template>
<FormSection @submitted="updateProfileInformation">
<template #title>
Profile Information
</template>
<Card>
<form @submit.prevent="updateProfileInformation">
<CardHeader>
<div class="flex items-center gap-2">
<User class="h-5 w-5 text-muted-foreground" />
<CardTitle>Profile Information</CardTitle>
</div>
<CardDescription>
Update your account's profile information and email address.
</CardDescription>
</CardHeader>
<template #description>
Update your account's profile information and email address.
</template>
<CardContent class="space-y-6">
<!-- Profile Photo -->
<div v-if="$page.props.jetstream.managesProfilePhotos" class="space-y-4">
<input
id="photo"
ref="photoInput"
type="file"
class="hidden"
accept="image/*"
@change="updatePhotoPreview"
>
<template #form>
<!-- Profile Photo -->
<div v-if="$page.props.jetstream.managesProfilePhotos" class="col-span-6 sm:col-span-4">
<!-- Profile Photo File Input -->
<input
id="photo"
ref="photoInput"
type="file"
class="hidden"
@change="updatePhotoPreview"
>
<Label for="photo">Photo</Label>
<InputLabel for="photo" value="Photo" />
<div class="flex items-center gap-4">
<!-- Current/Preview Photo -->
<Avatar class="h-20 w-20">
<AvatarImage
v-if="photoPreview"
:src="photoPreview"
:alt="user.name"
/>
<AvatarImage
v-else
:src="user.profile_photo_url"
:alt="user.name"
/>
<AvatarFallback>
<User class="h-8 w-8" />
</AvatarFallback>
</Avatar>
<!-- Current Profile Photo -->
<div v-show="! photoPreview" class="mt-2">
<img :src="user.profile_photo_url" :alt="user.name" class="rounded-full h-20 w-20 object-cover">
<div class="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
@click.prevent="selectNewPhoto"
>
<Camera class="h-4 w-4 mr-2" />
Select Photo
</Button>
<Button
v-if="user.profile_photo_path"
type="button"
variant="outline"
size="sm"
@click.prevent="deletePhoto"
>
<Trash2 class="h-4 w-4 mr-2" />
Remove
</Button>
</div>
</div>
<InputError :message="form.errors.photo" class="mt-2" />
</div>
<!-- New Profile Photo Preview -->
<div v-show="photoPreview" class="mt-2">
<span
class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
:style="'background-image: url(\'' + photoPreview + '\');'"
<!-- Name -->
<div class="space-y-2">
<Label for="name">Name</Label>
<Input
id="name"
v-model="form.name"
type="text"
required
autocomplete="name"
/>
<InputError :message="form.errors.name" class="mt-2" />
</div>
<SecondaryButton class="mt-2 me-2" type="button" @click.prevent="selectNewPhoto">
Select A New Photo
</SecondaryButton>
<!-- Email -->
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
v-model="form.email"
type="email"
required
autocomplete="username"
/>
<InputError :message="form.errors.email" class="mt-2" />
<SecondaryButton
v-if="user.profile_photo_path"
type="button"
class="mt-2"
@click.prevent="deletePhoto"
>
Remove Photo
</SecondaryButton>
<InputError :message="form.errors.photo" class="mt-2" />
</div>
<!-- Name -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
required
autocomplete="name"
/>
<InputError :message="form.errors.name" class="mt-2" />
</div>
<!-- Email -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autocomplete="username"
/>
<InputError :message="form.errors.email" class="mt-2" />
<div v-if="$page.props.jetstream.hasEmailVerification && user.email_verified_at === null">
<p class="text-sm mt-2">
Your email address is unverified.
<Link
:href="route('verification.send')"
method="post"
as="button"
class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click.prevent="sendEmailVerification"
>
Click here to re-send the verification email.
</Link>
</p>
<div v-show="verificationLinkSent" class="mt-2 font-medium text-sm text-green-600">
A new verification link has been sent to your email address.
<!-- Email Verification -->
<div v-if="$page.props.jetstream.hasEmailVerification && user.email_verified_at === null" class="rounded-lg border border-amber-200 bg-amber-50 p-3 dark:border-amber-800 dark:bg-amber-950">
<div class="flex items-start gap-2">
<AlertCircle class="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5" />
<div class="flex-1 text-sm">
<p class="text-amber-800 dark:text-amber-200">
Your email address is unverified.
<Link
:href="route('verification.send')"
method="post"
as="button"
class="underline text-amber-900 hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300 font-medium"
@click.prevent="sendEmailVerification"
>
Click here to re-send the verification email.
</Link>
</p>
<div v-show="verificationLinkSent" class="mt-2 flex items-center gap-1.5 text-green-700 dark:text-green-400">
<CheckCircle class="h-4 w-4" />
<span>A new verification link has been sent to your email address.</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</CardContent>
<template #actions>
<ActionMessage :on="form.recentlySuccessful" class="me-3">
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Save
</PrimaryButton>
</template>
</FormSection>
<CardFooter class="flex items-center justify-between">
<div class="flex items-center gap-2 text-sm text-muted-foreground">
<CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
<span v-if="form.recentlySuccessful">Saved.</span>
</div>
<Button type="submit" :disabled="form.processing">
Save
</Button>
</CardFooter>
</form>
</Card>
</template>