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,25 +56,22 @@ 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>
<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>
<!-- Other Browser Sessions -->
<div v-if="sessions && sessions.length > 0" class="space-y-4">
<div
@ -100,9 +98,9 @@ const closeModal = () => {
v-if="session.is_current_device"
class="inline-flex items-center ml-2 text-green-600 dark:text-green-400 font-semibold"
>
This device
Ta naprava
</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>
@ -111,12 +109,11 @@ const closeModal = () => {
<!-- 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>
<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,7 +127,8 @@ const closeModal = () => {
<span>Done.</span>
</div>
</div>
</CardContent>
</template>
</AppCard>
<!-- Log Out Other Devices Confirmation Dialog -->
<Dialog :open="confirmingLogout" @update:open="closeModal">
@ -163,5 +161,4 @@ const closeModal = () => {
</DialogFooter>
</DialogContent>
</Dialog>
</Card>
</template>

View File

@ -1,14 +1,21 @@
<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,
@ -23,11 +30,11 @@ 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, () => {
@ -40,40 +47,40 @@ watch(twoFactorEnabled, () => {
const enableTwoFactorAuthentication = () => {
enabling.value = true;
router.post(route('two-factor.enable'), {}, {
router.post(
route("two-factor.enable"),
{},
{
preserveScroll: true,
onSuccess: () => Promise.all([
showQrCode(),
showSetupKey(),
showRecoveryCodes(),
]),
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 => {
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 => {
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 => {
return axios.get(route("two-factor.recovery-codes")).then((response) => {
recoveryCodes.value = response.data;
});
};
const confirmTwoFactorAuthentication = () => {
confirmationForm.post(route('two-factor.confirm'), {
confirmationForm.post(route("two-factor.confirm"), {
errorBag: "confirmTwoFactorAuthentication",
preserveScroll: true,
preserveState: true,
@ -86,15 +93,13 @@ const confirmTwoFactorAuthentication = () => {
};
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;
router.delete(route('two-factor.disable'), {
router.delete(route("two-factor.disable"), {
preserveScroll: true,
onSuccess: () => {
disabling.value = false;
@ -107,42 +112,50 @@ const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text);
} catch (err) {
console.error('Failed to copy:', err);
console.error("Failed to copy:", err);
}
};
</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="p-4 border-t"
>
<template #header>
<div class="flex items-center gap-2">
<Shield class="h-5 w-5 text-muted-foreground" />
<CardTitle>Two Factor Authentication</CardTitle>
<Shield size="18" />
<CardTitle>Dvonivojska overitev</CardTitle>
</div>
<CardDescription>
Add additional security to your account using two factor authentication.
Dodatna varnost za vaš račun z dvonivojsko overitvijo.
</CardDescription>
</CardHeader>
</template>
<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">
<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
Dvonivojska overitev omogočena
</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" />
Finish enabling two factor authentication
Dokončaj namestitev dvonivojske overitve
</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
Dvonivojska overitev onemogočena
</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>
@ -151,10 +164,13 @@ const copyToClipboard = async (text) => {
<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.
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">
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>
<!-- 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 class="flex items-center justify-between gap-2">
<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>
</div>
<Button
@ -181,7 +197,7 @@ const copyToClipboard = async (text) => {
<!-- Confirmation Code Input -->
<div v-if="confirming" class="space-y-2">
<Label for="code">Confirmation Code</Label>
<Label for="code">Potrdite kodo</Label>
<Input
id="code"
v-model="confirmationForm.code"
@ -200,18 +216,27 @@ const copyToClipboard = async (text) => {
<!-- 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="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" />
<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.
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">
<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"
@ -227,9 +252,9 @@ const copyToClipboard = async (text) => {
</div>
</div>
</div>
<template #footer>
<!-- Action Buttons -->
<div class="flex flex-wrap gap-2">
<div class="flex flex-row gap-2 items-center justify-end w-full">
<!-- Enable -->
<div v-if="!twoFactorEnabled">
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
@ -243,11 +268,7 @@ const copyToClipboard = async (text) => {
<!-- Confirm -->
<template v-else>
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
<Button
v-if="confirming"
type="button"
:disabled="enabling"
>
<Button v-if="confirming" type="button" :disabled="enabling">
<CheckCircle class="h-4 w-4 mr-2" />
Confirm
</Button>
@ -301,6 +322,6 @@ const copyToClipboard = async (text) => {
</ConfirmsPassword>
</template>
</div>
</CardContent>
</Card>
</template>
</AppCard>
</template>

View File

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

View File

@ -1,20 +1,21 @@
<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,
});
const form = useForm({
_method: 'PUT',
_method: "PUT",
name: props.user.name,
email: props.user.email,
photo: null,
@ -29,8 +30,8 @@ const updateProfileInformation = () => {
form.photo = photoInput.value.files[0];
}
form.post(route('user-profile-information.update'), {
errorBag: 'updateProfileInformation',
form.post(route("user-profile-information.update"), {
errorBag: "updateProfileInformation",
preserveScroll: true,
onSuccess: () => clearPhotoFileInput(),
});
@ -59,7 +60,7 @@ const updatePhotoPreview = () => {
};
const deletePhoto = () => {
router.delete(route('current-user-photo.destroy'), {
router.delete(route("current-user-photo.destroy"), {
preserveScroll: true,
onSuccess: () => {
photoPreview.value = null;
@ -76,19 +77,22 @@ const clearPhotoFileInput = () => {
</script>
<template>
<Card>
<form @submit.prevent="updateProfileInformation">
<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 class="h-5 w-5 text-muted-foreground" />
<CardTitle>Profile Information</CardTitle>
<User size="18" />
<CardTitle>Informacije profila</CardTitle>
</div>
<CardDescription>
Update your account's profile information and email address.
</CardDescription>
</CardHeader>
<p class="text-sm">Posodobite informacije vašega profila in e-poštni naslov.</p>
</template>
<CardContent class="space-y-6">
<form @submit.prevent="updateProfileInformation" class="space-y-6">
<!-- Profile Photo -->
<div v-if="$page.props.jetstream.managesProfilePhotos" class="space-y-4">
<input
@ -98,23 +102,15 @@ const clearPhotoFileInput = () => {
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"
/>
<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>
@ -128,7 +124,7 @@ const clearPhotoFileInput = () => {
@click.prevent="selectNewPhoto"
>
<Camera class="h-4 w-4 mr-2" />
Select Photo
Izberi fotografijo
</Button>
<Button
@ -139,7 +135,7 @@ const clearPhotoFileInput = () => {
@click.prevent="deletePhoto"
>
<Trash2 class="h-4 w-4 mr-2" />
Remove
Odstrani
</Button>
</div>
</div>
@ -149,20 +145,14 @@ const clearPhotoFileInput = () => {
<!-- Name -->
<div class="space-y-2">
<Label for="name">Name</Label>
<Input
id="name"
v-model="form.name"
type="text"
required
autocomplete="name"
/>
<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>
<Label for="email">E-pošta</Label>
<Input
id="email"
v-model="form.email"
@ -173,12 +163,17 @@ const clearPhotoFileInput = () => {
<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
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.
Vaš e-poštni naslov ni potrjen.
<Link
:href="route('verification.send')"
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"
@click.prevent="sendEmailVerification"
>
Click here to re-send the verification email.
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">
<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>
<span
>Nova povezava za potrditev je bila poslana na vaš e-poštni
naslov.</span
>
</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">
<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>
<Button type="submit" :disabled="form.processing"> Shrani </Button>
</div>
<Button type="submit" :disabled="form.processing">
Save
</Button>
</CardFooter>
</form>
</Card>
</template>
</AppCard>
</template>