Visual changes to profile page

This commit is contained in:
Simon Pocrnjič 2026-01-19 19:24:41 +01:00
parent 068bbdf583
commit d64a67cf76
5 changed files with 603 additions and 578 deletions

View File

@ -23,9 +23,11 @@ class PackageController extends Controller
{
public function index(Request $request): Response
{
$perPage = $request->input('per_page') ?? 25;
$packages = Package::query()
->latest('id')
->paginate(25);
->paginate($perPage);
return Inertia::render('Admin/Packages/Index', [
'packages' => $packages,

View File

@ -20,6 +20,7 @@ import {
} from "@/Components/ui/dialog";
import InputError from "@/Components/InputError.vue";
import { Monitor, Smartphone, LogOut, CheckCircle } from "lucide-vue-next";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
defineProps({
sessions: Array,
@ -55,68 +56,64 @@ const closeModal = () => {
</script>
<template>
<Card>
<CardHeader>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class=""
>
<template #header>
<div class="flex items-center gap-2">
<LogOut class="h-5 w-5 text-muted-foreground" />
<CardTitle>Browser Sessions</CardTitle>
<LogOut size="18" />
<CardTitle>Aktivne prijave</CardTitle>
</div>
<CardDescription>
Manage and log out your active sessions on other browsers and devices.
Upravljanje in izpis aktivnih prijav no drugih brskalnikih in napravah.
</CardDescription>
</CardHeader>
</template>
<!-- Other Browser Sessions -->
<div v-if="sessions && 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="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>
<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>
<!-- Other Browser Sessions -->
<div v-if="sessions && 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="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 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>
<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>
<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 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"
>
Ta naprava
</span>
<span v-else class="ml-1"> · Aktiven {{ session.last_active }} </span>
</div>
</div>
</div>
</div>
<!-- Empty State -->
<div v-else class="rounded-lg border border-dashed p-8 text-center">
<Monitor class="h-12 w-12 mx-auto text-muted-foreground mb-3" />
<p class="text-sm text-muted-foreground">
No active sessions found. This feature requires session data to be configured in your Laravel application.
</p>
</div>
<!-- Empty State -->
<div v-else class="rounded-lg border border-dashed p-8 text-center">
<Monitor class="h-12 w-12 mx-auto text-muted-foreground mb-3" />
<p class="text-sm text-muted-foreground">Najdena nobena odprta prijava.</p>
</div>
<div class="flex items-center gap-3">
<template #footer>
<div class="flex flex-row gap-1 items-center justify-end w-full">
<Button @click="confirmLogout">
<LogOut class="h-4 w-4 mr-2" />
Log Out Other Browser Sessions
@ -130,38 +127,38 @@ const closeModal = () => {
<span>Done.</span>
</div>
</div>
</CardContent>
</template>
</AppCard>
<!-- 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>
<!-- 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>
<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="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>
<DialogFooter>
<Button variant="outline" @click="closeModal"> Cancel </Button>
<Button :disabled="form.processing" @click="logoutOtherBrowserSessions">
Log Out Other Browser Sessions
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
<DialogFooter>
<Button variant="outline" @click="closeModal"> Cancel </Button>
<Button :disabled="form.processing" @click="logoutOtherBrowserSessions">
Log Out Other Browser Sessions
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@ -1,17 +1,24 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { router, useForm, usePage } 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 { Label } from '@/Components/ui/label';
import { Badge } from '@/Components/ui/badge';
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue';
import InputError from '@/Components/InputError.vue';
import { Shield, Key, Copy, RefreshCw, CheckCircle, AlertCircle } from 'lucide-vue-next';
import { ref, computed, watch } from "vue";
import { router, useForm, usePage } 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 { Label } from "@/Components/ui/label";
import { Badge } from "@/Components/ui/badge";
import ConfirmsPassword from "@/Components/ConfirmsPassword.vue";
import InputError from "@/Components/InputError.vue";
import { Shield, Key, Copy, RefreshCw, CheckCircle, AlertCircle } from "lucide-vue-next";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
const props = defineProps({
requiresConfirmation: Boolean,
requiresConfirmation: Boolean,
});
const page = usePage();
@ -23,284 +30,298 @@ const setupKey = ref(null);
const recoveryCodes = ref([]);
const confirmationForm = useForm({
code: '',
code: "",
});
const twoFactorEnabled = computed(
() => ! enabling.value && page.props.auth.user?.two_factor_enabled,
() => !enabling.value && page.props.auth.user?.two_factor_enabled
);
watch(twoFactorEnabled, () => {
if (! twoFactorEnabled.value) {
confirmationForm.reset();
confirmationForm.clearErrors();
}
if (!twoFactorEnabled.value) {
confirmationForm.reset();
confirmationForm.clearErrors();
}
});
const enableTwoFactorAuthentication = () => {
enabling.value = true;
enabling.value = true;
router.post(route('two-factor.enable'), {}, {
preserveScroll: true,
onSuccess: () => Promise.all([
showQrCode(),
showSetupKey(),
showRecoveryCodes(),
]),
onFinish: () => {
enabling.value = false;
confirming.value = props.requiresConfirmation;
},
});
router.post(
route("two-factor.enable"),
{},
{
preserveScroll: true,
onSuccess: () => Promise.all([showQrCode(), showSetupKey(), showRecoveryCodes()]),
onFinish: () => {
enabling.value = false;
confirming.value = props.requiresConfirmation;
},
}
);
};
const showQrCode = () => {
return axios.get(route('two-factor.qr-code')).then(response => {
qrCode.value = response.data.svg;
});
return axios.get(route("two-factor.qr-code")).then((response) => {
qrCode.value = response.data.svg;
});
};
const showSetupKey = () => {
return axios.get(route('two-factor.secret-key')).then(response => {
setupKey.value = response.data.secretKey;
});
}
return axios.get(route("two-factor.secret-key")).then((response) => {
setupKey.value = response.data.secretKey;
});
};
const showRecoveryCodes = () => {
return axios.get(route('two-factor.recovery-codes')).then(response => {
recoveryCodes.value = response.data;
});
return axios.get(route("two-factor.recovery-codes")).then((response) => {
recoveryCodes.value = response.data;
});
};
const confirmTwoFactorAuthentication = () => {
confirmationForm.post(route('two-factor.confirm'), {
errorBag: "confirmTwoFactorAuthentication",
preserveScroll: true,
preserveState: true,
onSuccess: () => {
confirming.value = false;
qrCode.value = null;
setupKey.value = null;
},
});
confirmationForm.post(route("two-factor.confirm"), {
errorBag: "confirmTwoFactorAuthentication",
preserveScroll: true,
preserveState: true,
onSuccess: () => {
confirming.value = false;
qrCode.value = null;
setupKey.value = null;
},
});
};
const regenerateRecoveryCodes = () => {
axios
.post(route('two-factor.recovery-codes'))
.then(() => showRecoveryCodes());
axios.post(route("two-factor.recovery-codes")).then(() => showRecoveryCodes());
};
const disableTwoFactorAuthentication = () => {
disabling.value = true;
disabling.value = true;
router.delete(route('two-factor.disable'), {
preserveScroll: true,
onSuccess: () => {
disabling.value = false;
confirming.value = false;
},
});
router.delete(route("two-factor.disable"), {
preserveScroll: true,
onSuccess: () => {
disabling.value = false;
confirming.value = false;
},
});
};
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy:', err);
}
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error("Failed to copy:", err);
}
};
</script>
<template>
<Card>
<CardHeader>
<div class="flex items-center gap-2">
<Shield class="h-5 w-5 text-muted-foreground" />
<CardTitle>Two Factor Authentication</CardTitle>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class="p-4 border-t"
>
<template #header>
<div class="flex items-center gap-2">
<Shield size="18" />
<CardTitle>Dvonivojska overitev</CardTitle>
</div>
<CardDescription>
Dodatna varnost za vaš račun z dvonivojsko overitvijo.
</CardDescription>
</template>
<!-- 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" />
Dvonivojska overitev omogočena
</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" />
Dokončaj namestitev dvonivojske overitve
</h3>
<h3 v-else class="text-lg font-semibold flex items-center gap-2">
Dvonivojska overitev onemogočena
</h3>
</div>
</div>
<!-- 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">
Za dokončanje omogočanja dvostopenjske overitve skenirajte naslednjo QR-kodo z
aplikacijo za preverjanje pristnosti na vašem telefonu ali vnesite
namestitveno kodo in vpišite ustvarjeno OTP-kodo.
</p>
<p v-else class="text-sm text-muted-foreground mb-4">
Dvonivojska overitev je zdaj omogočena. Skenirajte QR kodo z aplikacijo za
preverjanje pristnosti na vašem telefonu ali vnesite namestitveni ključ.
</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">Namestitveni Ključ</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>
<CardDescription>
Add additional security to your account using two factor authentication.
</CardDescription>
</CardHeader>
</div>
</div>
<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>
<!-- Confirmation Code Input -->
<div v-if="confirming" class="space-y-2">
<Label for="code">Potrdite kodo</Label>
<Input
id="code"
v-model="confirmationForm.code"
type="text"
name="code"
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>
<!-- 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">
Shranite to obnovitveno kodo v upravitelja gesel. Lahko se uporabi za obnovo
vstopa v vaš račun, če se izgubi naprava z dvostopenjskim overjanjem.
</p>
</div>
</div>
<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>
<template #footer>
<!-- Action Buttons -->
<div class="flex flex-row gap-2 items-center justify-end w-full">
<!-- Enable -->
<div v-if="!twoFactorEnabled">
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
<Button type="button" :disabled="enabling">
<Shield class="h-4 w-4 mr-2" />
Enable
</Button>
</ConfirmsPassword>
</div>
<!-- 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 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>
<!-- Confirm -->
<template v-else>
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
<Button v-if="confirming" type="button" :disabled="enabling">
<CheckCircle class="h-4 w-4 mr-2" />
Confirm
</Button>
</ConfirmsPassword>
<!-- QR Code -->
<div class="flex justify-center p-4 bg-white rounded-lg" v-html="qrCode" />
<!-- Regenerate Recovery Codes -->
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
<Button
v-if="recoveryCodes.length > 0 && !confirming"
type="button"
variant="outline"
>
<RefreshCw class="h-4 w-4 mr-2" />
Regenerate Recovery Codes
</Button>
</ConfirmsPassword>
<!-- 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>
<!-- Show Recovery Codes -->
<ConfirmsPassword @confirmed="showRecoveryCodes">
<Button
v-if="recoveryCodes.length === 0 && !confirming"
type="button"
variant="outline"
>
<Key class="h-4 w-4 mr-2" />
Show Recovery Codes
</Button>
</ConfirmsPassword>
<!-- 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"
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>
<!-- Cancel/Disable -->
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<Button
v-if="confirming"
type="button"
variant="outline"
:disabled="disabling"
>
Cancel
</Button>
</ConfirmsPassword>
<!-- 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="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>
<!-- Action Buttons -->
<div class="flex flex-wrap gap-2">
<!-- Enable -->
<div v-if="! twoFactorEnabled">
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
<Button type="button" :disabled="enabling">
<Shield class="h-4 w-4 mr-2" />
Enable
</Button>
</ConfirmsPassword>
</div>
<!-- Confirm -->
<template v-else>
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
<Button
v-if="confirming"
type="button"
:disabled="enabling"
>
<CheckCircle class="h-4 w-4 mr-2" />
Confirm
</Button>
</ConfirmsPassword>
<!-- Regenerate Recovery Codes -->
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
<Button
v-if="recoveryCodes.length > 0 && ! confirming"
type="button"
variant="outline"
>
<RefreshCw class="h-4 w-4 mr-2" />
Regenerate Recovery Codes
</Button>
</ConfirmsPassword>
<!-- Show Recovery Codes -->
<ConfirmsPassword @confirmed="showRecoveryCodes">
<Button
v-if="recoveryCodes.length === 0 && ! confirming"
type="button"
variant="outline"
>
<Key class="h-4 w-4 mr-2" />
Show Recovery Codes
</Button>
</ConfirmsPassword>
<!-- Cancel/Disable -->
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<Button
v-if="confirming"
type="button"
variant="outline"
:disabled="disabling"
>
Cancel
</Button>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<Button
v-if="! confirming"
type="button"
variant="destructive"
:disabled="disabling"
>
Disable
</Button>
</ConfirmsPassword>
</template>
</div>
</CardContent>
</Card>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<Button
v-if="!confirming"
type="button"
variant="destructive"
:disabled="disabling"
>
Disable
</Button>
</ConfirmsPassword>
</template>
</div>
</template>
</AppCard>
</template>

View File

@ -1,101 +1,106 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
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 { CheckCircle, Lock } from 'lucide-vue-next';
import { ref } from "vue";
import { useForm } from "@inertiajs/vue3";
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 { CheckCircle, Lock } from "lucide-vue-next";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import { CardTitle } from "@/Components/ui/card";
const passwordInput = ref(null);
const currentPasswordInput = ref(null);
const form = useForm({
current_password: '',
password: '',
password_confirmation: '',
current_password: "",
password: "",
password_confirmation: "",
});
const updatePassword = () => {
form.put(route('user-password.update'), {
errorBag: 'updatePassword',
preserveScroll: true,
onSuccess: () => form.reset(),
onError: () => {
if (form.errors.password) {
form.reset('password', 'password_confirmation');
passwordInput.value.focus();
}
form.put(route("user-password.update"), {
errorBag: "updatePassword",
preserveScroll: true,
onSuccess: () => form.reset(),
onError: () => {
if (form.errors.password) {
form.reset("password", "password_confirmation");
passwordInput.value.focus();
}
if (form.errors.current_password) {
form.reset('current_password');
currentPasswordInput.value.focus();
}
},
});
if (form.errors.current_password) {
form.reset("current_password");
currentPasswordInput.value.focus();
}
},
});
};
</script>
<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>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class="p-4 border-t"
>
<template #header>
<div class="flex items-center gap-2">
<Lock size="18" />
<CardTitle>Posodobi geslo</CardTitle>
</div>
<p class="text-sm">
Poskrbite, da vaš račun uporablja dolgo, naključno geslo za varnost.
</p>
</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>
<form @submit.prevent="updatePassword" class="space-y-6">
<div class="space-y-2">
<Label for="current_password">Trenutno geslo</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>
<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="space-y-2">
<Label for="password">Novo geslo</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="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="space-y-2">
<Label for="password_confirmation">Potrdi geslo</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>
</form>
<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 #footer>
<div class="flex items-center justify-between w-full">
<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">Shranjeno.</span>
</div>
<Button type="submit" :disabled="form.processing"> Shrani </Button>
</div>
</template>
</AppCard>
</template>

View File

@ -1,23 +1,24 @@
<script setup>
import { ref } from 'vue';
import { Link, router, useForm } from '@inertiajs/vue3';
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 { User, Mail, Camera, Trash2, CheckCircle, AlertCircle } from 'lucide-vue-next';
import { ref } from "vue";
import { Link, router, useForm } from "@inertiajs/vue3";
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 { User, Mail, Camera, Trash2, CheckCircle, AlertCircle } from "lucide-vue-next";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
import { CardTitle } from "@/Components/ui/card";
const props = defineProps({
user: Object,
user: Object,
});
const form = useForm({
_method: 'PUT',
name: props.user.name,
email: props.user.email,
photo: null,
_method: "PUT",
name: props.user.name,
email: props.user.email,
photo: null,
});
const verificationLinkSent = ref(null);
@ -25,189 +26,188 @@ const photoPreview = ref(null);
const photoInput = ref(null);
const updateProfileInformation = () => {
if (photoInput.value) {
form.photo = photoInput.value.files[0];
}
if (photoInput.value) {
form.photo = photoInput.value.files[0];
}
form.post(route('user-profile-information.update'), {
errorBag: 'updateProfileInformation',
preserveScroll: true,
onSuccess: () => clearPhotoFileInput(),
});
form.post(route("user-profile-information.update"), {
errorBag: "updateProfileInformation",
preserveScroll: true,
onSuccess: () => clearPhotoFileInput(),
});
};
const sendEmailVerification = () => {
verificationLinkSent.value = true;
verificationLinkSent.value = true;
};
const selectNewPhoto = () => {
photoInput.value.click();
photoInput.value.click();
};
const updatePhotoPreview = () => {
const photo = photoInput.value.files[0];
const photo = photoInput.value.files[0];
if (! photo) return;
if (!photo) return;
const reader = new FileReader();
const reader = new FileReader();
reader.onload = (e) => {
photoPreview.value = e.target.result;
};
reader.onload = (e) => {
photoPreview.value = e.target.result;
};
reader.readAsDataURL(photo);
reader.readAsDataURL(photo);
};
const deletePhoto = () => {
router.delete(route('current-user-photo.destroy'), {
preserveScroll: true,
onSuccess: () => {
photoPreview.value = null;
clearPhotoFileInput();
},
});
router.delete(route("current-user-photo.destroy"), {
preserveScroll: true,
onSuccess: () => {
photoPreview.value = null;
clearPhotoFileInput();
},
});
};
const clearPhotoFileInput = () => {
if (photoInput.value?.value) {
photoInput.value.value = null;
}
if (photoInput.value?.value) {
photoInput.value.value = null;
}
};
</script>
<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>
<AppCard
title=""
padding="none"
class="p-0! gap-0"
header-class="py-3! px-4 gap-0 text-muted-foreground"
body-class="p-4 border-t"
>
<template #header>
<div class="flex items-center gap-2">
<User size="18" />
<CardTitle>Informacije profila</CardTitle>
</div>
<p class="text-sm">Posodobite informacije vašega profila in e-poštni naslov.</p>
</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"
>
<form @submit.prevent="updateProfileInformation" 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"
/>
<Label for="photo">Photo</Label>
<Label for="photo">Fotografija</Label>
<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>
<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>
<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>
<div class="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
@click.prevent="selectNewPhoto"
>
<Camera class="h-4 w-4 mr-2" />
Izberi fotografijo
</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>
<Button
v-if="user.profile_photo_path"
type="button"
variant="outline"
size="sm"
@click.prevent="deletePhoto"
>
<Trash2 class="h-4 w-4 mr-2" />
Odstrani
</Button>
</div>
</div>
<InputError :message="form.errors.photo" class="mt-2" />
</div>
<InputError :message="form.errors.photo" class="mt-2" />
</div>
<!-- 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>
<!-- Name -->
<div class="space-y-2">
<Label for="name">Ime</Label>
<Input id="name" v-model="form.name" type="text" required autocomplete="name" />
<InputError :message="form.errors.name" class="mt-2" />
</div>
<!-- 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" />
<!-- Email -->
<div class="space-y-2">
<Label for="email">E-pošta</Label>
<Input
id="email"
v-model="form.email"
type="email"
required
autocomplete="username"
/>
<InputError :message="form.errors.email" class="mt-2" />
<!-- 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>
</CardContent>
<!-- 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">
Vaš e-poštni naslov ni potrjen.
<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"
>
Kliknite tukaj za ponovno pošiljanje potrditvenega e-sporočila.
</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
>Nova povezava za potrditev je bila poslana na vaš e-poštni
naslov.</span
>
</div>
</div>
</div>
</div>
</div>
</form>
<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 #footer>
<div class="flex items-center justify-between w-full">
<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">Shranjeno.</span>
</div>
<Button type="submit" :disabled="form.processing"> Shrani </Button>
</div>
</template>
</AppCard>
</template>