production #1

Merged
sipo merged 45 commits from production into master 2026-01-27 18:02:44 +00:00
5 changed files with 603 additions and 578 deletions
Showing only changes of commit d64a67cf76 - Show all commits

View File

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

View File

@ -20,6 +20,7 @@ import {
} from "@/Components/ui/dialog"; } from "@/Components/ui/dialog";
import InputError from "@/Components/InputError.vue"; import InputError from "@/Components/InputError.vue";
import { Monitor, Smartphone, LogOut, CheckCircle } from "lucide-vue-next"; import { Monitor, Smartphone, LogOut, CheckCircle } from "lucide-vue-next";
import AppCard from "@/Components/app/ui/card/AppCard.vue";
defineProps({ defineProps({
sessions: Array, sessions: Array,
@ -55,25 +56,22 @@ const closeModal = () => {
</script> </script>
<template> <template>
<Card> <AppCard
<CardHeader> 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"> <div class="flex items-center gap-2">
<LogOut class="h-5 w-5 text-muted-foreground" /> <LogOut size="18" />
<CardTitle>Browser Sessions</CardTitle> <CardTitle>Aktivne prijave</CardTitle>
</div> </div>
<CardDescription> <CardDescription>
Manage and log out your active sessions on other browsers and devices. Upravljanje in izpis aktivnih prijav no drugih brskalnikih in napravah.
</CardDescription> </CardDescription>
</CardHeader> </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>
<!-- Other Browser Sessions --> <!-- Other Browser Sessions -->
<div v-if="sessions && sessions.length > 0" class="space-y-4"> <div v-if="sessions && sessions.length > 0" class="space-y-4">
<div <div
@ -100,9 +98,9 @@ const closeModal = () => {
v-if="session.is_current_device" v-if="session.is_current_device"
class="inline-flex items-center ml-2 text-green-600 dark:text-green-400 font-semibold" class="inline-flex items-center ml-2 text-green-600 dark:text-green-400 font-semibold"
> >
This device Ta naprava
</span> </span>
<span v-else class="ml-1"> · Last active {{ session.last_active }} </span> <span v-else class="ml-1"> · Aktiven {{ session.last_active }} </span>
</div> </div>
</div> </div>
</div> </div>
@ -111,12 +109,11 @@ const closeModal = () => {
<!-- Empty State --> <!-- Empty State -->
<div v-else class="rounded-lg border border-dashed p-8 text-center"> <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" /> <Monitor class="h-12 w-12 mx-auto text-muted-foreground mb-3" />
<p class="text-sm text-muted-foreground"> <p class="text-sm text-muted-foreground">Najdena nobena odprta prijava.</p>
No active sessions found. This feature requires session data to be configured in your Laravel application.
</p>
</div> </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"> <Button @click="confirmLogout">
<LogOut class="h-4 w-4 mr-2" /> <LogOut class="h-4 w-4 mr-2" />
Log Out Other Browser Sessions Log Out Other Browser Sessions
@ -130,7 +127,8 @@ const closeModal = () => {
<span>Done.</span> <span>Done.</span>
</div> </div>
</div> </div>
</CardContent> </template>
</AppCard>
<!-- Log Out Other Devices Confirmation Dialog --> <!-- Log Out Other Devices Confirmation Dialog -->
<Dialog :open="confirmingLogout" @update:open="closeModal"> <Dialog :open="confirmingLogout" @update:open="closeModal">
@ -163,5 +161,4 @@ const closeModal = () => {
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</Card>
</template> </template>

View File

@ -1,14 +1,21 @@
<script setup> <script setup>
import { ref, computed, watch } from 'vue'; import { ref, computed, watch } from "vue";
import { router, useForm, usePage } from '@inertiajs/vue3'; import { router, useForm, usePage } from "@inertiajs/vue3";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/Components/ui/card'; import {
import { Button } from '@/Components/ui/button'; Card,
import { Input } from '@/Components/ui/input'; CardContent,
import { Label } from '@/Components/ui/label'; CardDescription,
import { Badge } from '@/Components/ui/badge'; CardHeader,
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue'; CardTitle,
import InputError from '@/Components/InputError.vue'; } from "@/Components/ui/card";
import { Shield, Key, Copy, RefreshCw, CheckCircle, AlertCircle } from 'lucide-vue-next'; 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({ const props = defineProps({
requiresConfirmation: Boolean, requiresConfirmation: Boolean,
@ -23,15 +30,15 @@ const setupKey = ref(null);
const recoveryCodes = ref([]); const recoveryCodes = ref([]);
const confirmationForm = useForm({ const confirmationForm = useForm({
code: '', code: "",
}); });
const twoFactorEnabled = computed( const twoFactorEnabled = computed(
() => ! enabling.value && page.props.auth.user?.two_factor_enabled, () => !enabling.value && page.props.auth.user?.two_factor_enabled
); );
watch(twoFactorEnabled, () => { watch(twoFactorEnabled, () => {
if (! twoFactorEnabled.value) { if (!twoFactorEnabled.value) {
confirmationForm.reset(); confirmationForm.reset();
confirmationForm.clearErrors(); confirmationForm.clearErrors();
} }
@ -40,40 +47,40 @@ watch(twoFactorEnabled, () => {
const enableTwoFactorAuthentication = () => { const enableTwoFactorAuthentication = () => {
enabling.value = true; enabling.value = true;
router.post(route('two-factor.enable'), {}, { router.post(
route("two-factor.enable"),
{},
{
preserveScroll: true, preserveScroll: true,
onSuccess: () => Promise.all([ onSuccess: () => Promise.all([showQrCode(), showSetupKey(), showRecoveryCodes()]),
showQrCode(),
showSetupKey(),
showRecoveryCodes(),
]),
onFinish: () => { onFinish: () => {
enabling.value = false; enabling.value = false;
confirming.value = props.requiresConfirmation; confirming.value = props.requiresConfirmation;
}, },
}); }
);
}; };
const showQrCode = () => { const showQrCode = () => {
return axios.get(route('two-factor.qr-code')).then(response => { return axios.get(route("two-factor.qr-code")).then((response) => {
qrCode.value = response.data.svg; qrCode.value = response.data.svg;
}); });
}; };
const showSetupKey = () => { const showSetupKey = () => {
return axios.get(route('two-factor.secret-key')).then(response => { return axios.get(route("two-factor.secret-key")).then((response) => {
setupKey.value = response.data.secretKey; setupKey.value = response.data.secretKey;
}); });
} };
const showRecoveryCodes = () => { const showRecoveryCodes = () => {
return axios.get(route('two-factor.recovery-codes')).then(response => { return axios.get(route("two-factor.recovery-codes")).then((response) => {
recoveryCodes.value = response.data; recoveryCodes.value = response.data;
}); });
}; };
const confirmTwoFactorAuthentication = () => { const confirmTwoFactorAuthentication = () => {
confirmationForm.post(route('two-factor.confirm'), { confirmationForm.post(route("two-factor.confirm"), {
errorBag: "confirmTwoFactorAuthentication", errorBag: "confirmTwoFactorAuthentication",
preserveScroll: true, preserveScroll: true,
preserveState: true, preserveState: true,
@ -86,15 +93,13 @@ const confirmTwoFactorAuthentication = () => {
}; };
const regenerateRecoveryCodes = () => { const regenerateRecoveryCodes = () => {
axios axios.post(route("two-factor.recovery-codes")).then(() => showRecoveryCodes());
.post(route('two-factor.recovery-codes'))
.then(() => showRecoveryCodes());
}; };
const disableTwoFactorAuthentication = () => { const disableTwoFactorAuthentication = () => {
disabling.value = true; disabling.value = true;
router.delete(route('two-factor.disable'), { router.delete(route("two-factor.disable"), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
disabling.value = false; disabling.value = false;
@ -107,42 +112,50 @@ const copyToClipboard = async (text) => {
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
} catch (err) { } catch (err) {
console.error('Failed to copy:', err); console.error("Failed to copy:", err);
} }
}; };
</script> </script>
<template> <template>
<Card> <AppCard
<CardHeader> 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"> <div class="flex items-center gap-2">
<Shield class="h-5 w-5 text-muted-foreground" /> <Shield size="18" />
<CardTitle>Two Factor Authentication</CardTitle> <CardTitle>Dvonivojska overitev</CardTitle>
</div> </div>
<CardDescription> <CardDescription>
Add additional security to your account using two factor authentication. Dodatna varnost za vaš račun z dvonivojsko overitvijo.
</CardDescription> </CardDescription>
</CardHeader> </template>
<CardContent class="space-y-6">
<!-- Status Header --> <!-- Status Header -->
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<div class="flex-1"> <div class="flex-1">
<h3 v-if="twoFactorEnabled && ! confirming" class="text-lg font-semibold flex items-center gap-2"> <h3
v-if="twoFactorEnabled && !confirming"
class="text-lg font-semibold flex items-center gap-2"
>
<CheckCircle class="h-5 w-5 text-green-600" /> <CheckCircle class="h-5 w-5 text-green-600" />
Two factor authentication is enabled Dvonivojska overitev omogočena
</h3> </h3>
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-semibold flex items-center gap-2"> <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" /> <AlertCircle class="h-5 w-5 text-amber-600" />
Finish enabling two factor authentication
Dokončaj namestitev dvonivojske overitve
</h3> </h3>
<h3 v-else class="text-lg font-semibold flex items-center gap-2"> <h3 v-else class="text-lg font-semibold flex items-center gap-2">
<Shield class="h-5 w-5 text-muted-foreground" /> Dvonivojska overitev onemogočena
Two factor authentication is disabled
</h3> </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> </div>
@ -151,10 +164,13 @@ const copyToClipboard = async (text) => {
<div v-if="qrCode" class="space-y-4"> <div v-if="qrCode" class="space-y-4">
<div class="rounded-lg border bg-muted/50 p-4"> <div class="rounded-lg border bg-muted/50 p-4">
<p v-if="confirming" class="text-sm font-medium mb-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. 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>
<p v-else class="text-sm text-muted-foreground mb-4"> <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. Dvonivojska overitev je zdaj omogočena. Skenirajte QR kodo z aplikacijo za
preverjanje pristnosti na vašem telefonu ali vnesite namestitveni ključ.
</p> </p>
<!-- QR Code --> <!-- QR Code -->
@ -164,7 +180,7 @@ const copyToClipboard = async (text) => {
<div v-if="setupKey" class="mt-4 p-3 bg-background rounded-lg border"> <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 items-center justify-between gap-2">
<div class="flex-1"> <div class="flex-1">
<Label class="text-xs text-muted-foreground">Setup Key</Label> <Label class="text-xs text-muted-foreground">Namestitveni Ključ</Label>
<p class="font-mono text-sm font-semibold mt-1" v-html="setupKey"></p> <p class="font-mono text-sm font-semibold mt-1" v-html="setupKey"></p>
</div> </div>
<Button <Button
@ -181,7 +197,7 @@ const copyToClipboard = async (text) => {
<!-- Confirmation Code Input --> <!-- Confirmation Code Input -->
<div v-if="confirming" class="space-y-2"> <div v-if="confirming" class="space-y-2">
<Label for="code">Confirmation Code</Label> <Label for="code">Potrdite kodo</Label>
<Input <Input
id="code" id="code"
v-model="confirmationForm.code" v-model="confirmationForm.code"
@ -199,19 +215,28 @@ const copyToClipboard = async (text) => {
</div> </div>
<!-- Recovery Codes --> <!-- Recovery Codes -->
<div v-if="recoveryCodes.length > 0 && ! confirming" class="space-y-4"> <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="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"> <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" /> <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"> <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. 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> </p>
</div> </div>
</div> </div>
<div class="rounded-lg border bg-muted p-4"> <div class="rounded-lg border bg-muted p-4">
<div class="grid grid-cols-2 gap-2 font-mono text-sm"> <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"> <div
v-for="code in recoveryCodes"
:key="code"
class="flex items-center justify-between p-2 bg-background rounded border"
>
<span>{{ code }}</span> <span>{{ code }}</span>
<Button <Button
type="button" type="button"
@ -227,11 +252,11 @@ const copyToClipboard = async (text) => {
</div> </div>
</div> </div>
</div> </div>
<template #footer>
<!-- Action Buttons --> <!-- Action Buttons -->
<div class="flex flex-wrap gap-2"> <div class="flex flex-row gap-2 items-center justify-end w-full">
<!-- Enable --> <!-- Enable -->
<div v-if="! twoFactorEnabled"> <div v-if="!twoFactorEnabled">
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication"> <ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
<Button type="button" :disabled="enabling"> <Button type="button" :disabled="enabling">
<Shield class="h-4 w-4 mr-2" /> <Shield class="h-4 w-4 mr-2" />
@ -243,11 +268,7 @@ const copyToClipboard = async (text) => {
<!-- Confirm --> <!-- Confirm -->
<template v-else> <template v-else>
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication"> <ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
<Button <Button v-if="confirming" type="button" :disabled="enabling">
v-if="confirming"
type="button"
:disabled="enabling"
>
<CheckCircle class="h-4 w-4 mr-2" /> <CheckCircle class="h-4 w-4 mr-2" />
Confirm Confirm
</Button> </Button>
@ -256,7 +277,7 @@ const copyToClipboard = async (text) => {
<!-- Regenerate Recovery Codes --> <!-- Regenerate Recovery Codes -->
<ConfirmsPassword @confirmed="regenerateRecoveryCodes"> <ConfirmsPassword @confirmed="regenerateRecoveryCodes">
<Button <Button
v-if="recoveryCodes.length > 0 && ! confirming" v-if="recoveryCodes.length > 0 && !confirming"
type="button" type="button"
variant="outline" variant="outline"
> >
@ -268,7 +289,7 @@ const copyToClipboard = async (text) => {
<!-- Show Recovery Codes --> <!-- Show Recovery Codes -->
<ConfirmsPassword @confirmed="showRecoveryCodes"> <ConfirmsPassword @confirmed="showRecoveryCodes">
<Button <Button
v-if="recoveryCodes.length === 0 && ! confirming" v-if="recoveryCodes.length === 0 && !confirming"
type="button" type="button"
variant="outline" variant="outline"
> >
@ -291,7 +312,7 @@ const copyToClipboard = async (text) => {
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication"> <ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<Button <Button
v-if="! confirming" v-if="!confirming"
type="button" type="button"
variant="destructive" variant="destructive"
:disabled="disabling" :disabled="disabling"
@ -301,6 +322,6 @@ const copyToClipboard = async (text) => {
</ConfirmsPassword> </ConfirmsPassword>
</template> </template>
</div> </div>
</CardContent> </template>
</Card> </AppCard>
</template> </template>

View File

@ -1,35 +1,36 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from "vue";
import { useForm } from '@inertiajs/vue3'; import { useForm } from "@inertiajs/vue3";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/Components/ui/card'; import { Button } from "@/Components/ui/button";
import { Button } from '@/Components/ui/button'; import { Input } from "@/Components/ui/input";
import { Input } from '@/Components/ui/input'; import { Label } from "@/Components/ui/label";
import { Label } from '@/Components/ui/label'; import InputError from "@/Components/InputError.vue";
import InputError from '@/Components/InputError.vue'; import { CheckCircle, Lock } from "lucide-vue-next";
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 passwordInput = ref(null);
const currentPasswordInput = ref(null); const currentPasswordInput = ref(null);
const form = useForm({ const form = useForm({
current_password: '', current_password: "",
password: '', password: "",
password_confirmation: '', password_confirmation: "",
}); });
const updatePassword = () => { const updatePassword = () => {
form.put(route('user-password.update'), { form.put(route("user-password.update"), {
errorBag: 'updatePassword', errorBag: "updatePassword",
preserveScroll: true, preserveScroll: true,
onSuccess: () => form.reset(), onSuccess: () => form.reset(),
onError: () => { onError: () => {
if (form.errors.password) { if (form.errors.password) {
form.reset('password', 'password_confirmation'); form.reset("password", "password_confirmation");
passwordInput.value.focus(); passwordInput.value.focus();
} }
if (form.errors.current_password) { if (form.errors.current_password) {
form.reset('current_password'); form.reset("current_password");
currentPasswordInput.value.focus(); currentPasswordInput.value.focus();
} }
}, },
@ -38,21 +39,26 @@ const updatePassword = () => {
</script> </script>
<template> <template>
<Card> <AppCard
<form @submit.prevent="updatePassword"> title=""
<CardHeader> 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"> <div class="flex items-center gap-2">
<Lock class="h-5 w-5 text-muted-foreground" /> <Lock size="18" />
<CardTitle>Update Password</CardTitle> <CardTitle>Posodobi geslo</CardTitle>
</div> </div>
<CardDescription> <p class="text-sm">
Ensure your account is using a long, random password to stay secure. Poskrbite, da vaš račun uporablja dolgo, naključno geslo za varnost.
</CardDescription> </p>
</CardHeader> </template>
<CardContent class="space-y-6"> <form @submit.prevent="updatePassword" class="space-y-6">
<div class="space-y-2"> <div class="space-y-2">
<Label for="current_password">Current Password</Label> <Label for="current_password">Trenutno geslo</Label>
<Input <Input
id="current_password" id="current_password"
ref="currentPasswordInput" ref="currentPasswordInput"
@ -64,7 +70,7 @@ const updatePassword = () => {
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="password">New Password</Label> <Label for="password">Novo geslo</Label>
<Input <Input
id="password" id="password"
ref="passwordInput" ref="passwordInput"
@ -76,7 +82,7 @@ const updatePassword = () => {
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="password_confirmation">Confirm Password</Label> <Label for="password_confirmation">Potrdi geslo</Label>
<Input <Input
id="password_confirmation" id="password_confirmation"
v-model="form.password_confirmation" v-model="form.password_confirmation"
@ -85,17 +91,16 @@ const updatePassword = () => {
/> />
<InputError :message="form.errors.password_confirmation" class="mt-2" /> <InputError :message="form.errors.password_confirmation" class="mt-2" />
</div> </div>
</CardContent> </form>
<CardFooter class="flex items-center justify-between"> <template #footer>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2 text-sm text-muted-foreground"> <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" /> <CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
<span v-if="form.recentlySuccessful">Saved.</span> <span v-if="form.recentlySuccessful">Shranjeno.</span>
</div> </div>
<Button type="submit" :disabled="form.processing"> <Button type="submit" :disabled="form.processing"> Shrani </Button>
Save </div>
</Button> </template>
</CardFooter> </AppCard>
</form>
</Card>
</template> </template>

View File

@ -1,20 +1,21 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from "vue";
import { Link, router, useForm } from '@inertiajs/vue3'; 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 { Button } from '@/Components/ui/button'; import { Input } from "@/Components/ui/input";
import { Input } from '@/Components/ui/input'; import { Label } from "@/Components/ui/label";
import { Label } from '@/Components/ui/label'; import { Avatar, AvatarImage, AvatarFallback } from "@/Components/ui/avatar";
import { Avatar, AvatarImage, AvatarFallback } from '@/Components/ui/avatar'; import InputError from "@/Components/InputError.vue";
import InputError from '@/Components/InputError.vue'; import { User, Mail, Camera, Trash2, CheckCircle, AlertCircle } from "lucide-vue-next";
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({ const props = defineProps({
user: Object, user: Object,
}); });
const form = useForm({ const form = useForm({
_method: 'PUT', _method: "PUT",
name: props.user.name, name: props.user.name,
email: props.user.email, email: props.user.email,
photo: null, photo: null,
@ -29,8 +30,8 @@ const updateProfileInformation = () => {
form.photo = photoInput.value.files[0]; form.photo = photoInput.value.files[0];
} }
form.post(route('user-profile-information.update'), { form.post(route("user-profile-information.update"), {
errorBag: 'updateProfileInformation', errorBag: "updateProfileInformation",
preserveScroll: true, preserveScroll: true,
onSuccess: () => clearPhotoFileInput(), onSuccess: () => clearPhotoFileInput(),
}); });
@ -47,7 +48,7 @@ const selectNewPhoto = () => {
const updatePhotoPreview = () => { 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();
@ -59,7 +60,7 @@ const updatePhotoPreview = () => {
}; };
const deletePhoto = () => { const deletePhoto = () => {
router.delete(route('current-user-photo.destroy'), { router.delete(route("current-user-photo.destroy"), {
preserveScroll: true, preserveScroll: true,
onSuccess: () => { onSuccess: () => {
photoPreview.value = null; photoPreview.value = null;
@ -76,19 +77,22 @@ const clearPhotoFileInput = () => {
</script> </script>
<template> <template>
<Card> <AppCard
<form @submit.prevent="updateProfileInformation"> title=""
<CardHeader> 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"> <div class="flex items-center gap-2">
<User class="h-5 w-5 text-muted-foreground" /> <User size="18" />
<CardTitle>Profile Information</CardTitle> <CardTitle>Informacije profila</CardTitle>
</div> </div>
<CardDescription> <p class="text-sm">Posodobite informacije vašega profila in e-poštni naslov.</p>
Update your account's profile information and email address. </template>
</CardDescription>
</CardHeader>
<CardContent class="space-y-6"> <form @submit.prevent="updateProfileInformation" class="space-y-6">
<!-- Profile Photo --> <!-- Profile Photo -->
<div v-if="$page.props.jetstream.managesProfilePhotos" class="space-y-4"> <div v-if="$page.props.jetstream.managesProfilePhotos" class="space-y-4">
<input <input
@ -98,23 +102,15 @@ const clearPhotoFileInput = () => {
class="hidden" class="hidden"
accept="image/*" accept="image/*"
@change="updatePhotoPreview" @change="updatePhotoPreview"
> />
<Label for="photo">Photo</Label> <Label for="photo">Fotografija</Label>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<!-- Current/Preview Photo --> <!-- Current/Preview Photo -->
<Avatar class="h-20 w-20"> <Avatar class="h-20 w-20">
<AvatarImage <AvatarImage v-if="photoPreview" :src="photoPreview" :alt="user.name" />
v-if="photoPreview" <AvatarImage v-else :src="user.profile_photo_url" :alt="user.name" />
:src="photoPreview"
:alt="user.name"
/>
<AvatarImage
v-else
:src="user.profile_photo_url"
:alt="user.name"
/>
<AvatarFallback> <AvatarFallback>
<User class="h-8 w-8" /> <User class="h-8 w-8" />
</AvatarFallback> </AvatarFallback>
@ -128,7 +124,7 @@ const clearPhotoFileInput = () => {
@click.prevent="selectNewPhoto" @click.prevent="selectNewPhoto"
> >
<Camera class="h-4 w-4 mr-2" /> <Camera class="h-4 w-4 mr-2" />
Select Photo Izberi fotografijo
</Button> </Button>
<Button <Button
@ -139,7 +135,7 @@ const clearPhotoFileInput = () => {
@click.prevent="deletePhoto" @click.prevent="deletePhoto"
> >
<Trash2 class="h-4 w-4 mr-2" /> <Trash2 class="h-4 w-4 mr-2" />
Remove Odstrani
</Button> </Button>
</div> </div>
</div> </div>
@ -149,20 +145,14 @@ const clearPhotoFileInput = () => {
<!-- Name --> <!-- Name -->
<div class="space-y-2"> <div class="space-y-2">
<Label for="name">Name</Label> <Label for="name">Ime</Label>
<Input <Input id="name" v-model="form.name" type="text" required autocomplete="name" />
id="name"
v-model="form.name"
type="text"
required
autocomplete="name"
/>
<InputError :message="form.errors.name" class="mt-2" /> <InputError :message="form.errors.name" class="mt-2" />
</div> </div>
<!-- Email --> <!-- Email -->
<div class="space-y-2"> <div class="space-y-2">
<Label for="email">Email</Label> <Label for="email">E-pošta</Label>
<Input <Input
id="email" id="email"
v-model="form.email" v-model="form.email"
@ -173,12 +163,17 @@ const clearPhotoFileInput = () => {
<InputError :message="form.errors.email" class="mt-2" /> <InputError :message="form.errors.email" class="mt-2" />
<!-- Email Verification --> <!-- 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
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"> <div class="flex items-start gap-2">
<AlertCircle class="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5" /> <AlertCircle class="h-4 w-4 text-amber-600 dark:text-amber-400 mt-0.5" />
<div class="flex-1 text-sm"> <div class="flex-1 text-sm">
<p class="text-amber-800 dark:text-amber-200"> <p class="text-amber-800 dark:text-amber-200">
Your email address is unverified. Vaš e-poštni naslov ni potrjen.
<Link <Link
:href="route('verification.send')" :href="route('verification.send')"
method="post" method="post"
@ -186,28 +181,33 @@ const clearPhotoFileInput = () => {
class="underline text-amber-900 hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300 font-medium" class="underline text-amber-900 hover:text-amber-700 dark:text-amber-100 dark:hover:text-amber-300 font-medium"
@click.prevent="sendEmailVerification" @click.prevent="sendEmailVerification"
> >
Click here to re-send the verification email. Kliknite tukaj za ponovno pošiljanje potrditvenega e-sporočila.
</Link> </Link>
</p> </p>
<div v-show="verificationLinkSent" class="mt-2 flex items-center gap-1.5 text-green-700 dark:text-green-400"> <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" /> <CheckCircle class="h-4 w-4" />
<span>A new verification link has been sent to your email address.</span> <span
>Nova povezava za potrditev je bila poslana na vaš e-poštni
naslov.</span
>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</CardContent> </form>
<CardFooter class="flex items-center justify-between"> <template #footer>
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-2 text-sm text-muted-foreground"> <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" /> <CheckCircle v-if="form.recentlySuccessful" class="h-4 w-4 text-green-600" />
<span v-if="form.recentlySuccessful">Saved.</span> <span v-if="form.recentlySuccessful">Shranjeno.</span>
</div> </div>
<Button type="submit" :disabled="form.processing"> <Button type="submit" :disabled="form.processing"> Shrani </Button>
Save </div>
</Button> </template>
</CardFooter> </AppCard>
</form>
</Card>
</template> </template>