Admin panel updated with shadcn-vue components

This commit is contained in:
Simon Pocrnjič
2026-01-05 18:27:35 +01:00
parent 70a5d015e0
commit c4d9ecb39e
37 changed files with 5407 additions and 3740 deletions
@@ -2,6 +2,34 @@
import AdminLayout from "@/Layouts/AdminLayout.vue";
import { Link, useForm } from "@inertiajs/vue3";
import { computed, ref } from "vue";
import {
UploadIcon,
FileTextIcon,
Power,
PowerOffIcon,
PencilIcon,
CheckIcon,
XIcon,
} from "lucide-vue-next";
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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/Components/ui/select";
import { Badge } from "@/Components/ui/badge";
import { Progress } from "@/Components/ui/progress";
const props = defineProps({
templates: { type: Array, default: () => [] },
@@ -9,7 +37,7 @@ const props = defineProps({
// Upload form state
const uploadForm = useForm({ name: "", slug: "", file: null });
const selectedSlug = ref("");
const selectedSlug = ref(null);
const uniqueSlugs = computed(() => {
const s = new Set(props.templates.map((t) => t.slug));
return Array.from(s).sort();
@@ -60,186 +88,164 @@ const groups = computed(() => {
<template>
<AdminLayout title="Dokumentne predloge">
<div class="mb-8 space-y-6">
<div class="space-y-6">
<!-- Header & Upload -->
<div class="flex flex-col xl:flex-row xl:items-start gap-6">
<div class="flex-1 min-w-[280px]">
<h1 class="text-2xl font-semibold tracking-tight flex items-center gap-2">
<span>Dokumentne predloge</span>
<span
class="text-xs font-medium bg-gray-200 text-gray-600 px-2 py-0.5 rounded"
>{{ groups.length }} skupin</span
>
</h1>
<p class="text-sm text-gray-500 mt-1 max-w-prose">
<div class="flex items-center gap-2 mb-2">
<h1 class="text-2xl font-semibold tracking-tight">Dokumentne predloge</h1>
<Badge variant="secondary">{{ groups.length }} skupin</Badge>
</div>
<p class="text-sm text-muted-foreground max-w-prose">
Upravljaj verzije DOCX predlog. Naloži novo verzijo obstoječega sluga ali
ustvari popolnoma novo predlogo.
</p>
</div>
<form
@submit.prevent="submitUpload"
class="flex-1 bg-white/70 backdrop-blur border rounded-lg shadow-sm p-4 flex flex-col gap-3"
>
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold text-gray-700 flex items-center gap-2">
<span class="i-lucide-upload-cloud w-4 h-4" /> Nova / nova verzija
</h2>
<div
v-if="uploadForm.progress"
class="w-40 h-1 bg-gray-200 rounded overflow-hidden"
>
<div
class="h-full bg-indigo-500 transition-all"
:style="{ width: uploadForm.progress.percentage + '%' }"
<Card class="flex-1">
<CardHeader>
<div class="flex items-center justify-between">
<CardTitle class="text-base flex items-center gap-2">
<UploadIcon class="h-4 w-4" />
Nova / nova verzija
</CardTitle>
<Progress
v-if="uploadForm.progress"
:model-value="uploadForm.progress.percentage"
class="w-40 h-2"
/>
</div>
</div>
<div class="grid md:grid-cols-5 gap-3 text-xs">
<div class="md:col-span-1">
<label class="block font-medium mb-1">Obstoječi slug</label>
<select
v-model="selectedSlug"
class="select select-bordered select-sm w-full"
>
<option value="">(nov)</option>
<option v-for="s in uniqueSlugs" :key="s" :value="s">{{ s }}</option>
</select>
</div>
<div class="md:col-span-1">
<label class="block font-medium mb-1">Nov slug</label>
<input
v-model="uploadForm.slug"
:disabled="!!selectedSlug"
type="text"
class="input input-bordered input-sm w-full"
placeholder="opomin"
/>
</div>
<div class="md:col-span-1">
<label class="block font-medium mb-1">Naziv</label>
<input
v-model="uploadForm.name"
type="text"
class="input input-bordered input-sm w-full"
placeholder="Ime predloge"
/>
</div>
<div class="md:col-span-2 flex items-end">
<label class="w-full">
<input
id="docx-upload-input"
@change="handleFile"
type="file"
accept=".docx"
class="file-input file-input-bordered file-input-sm w-full"
/>
</label>
</div>
</div>
<div class="flex items-center justify-end gap-3 pt-1">
<span class="text-[11px] text-gray-500" v-if="!uploadForm.file"
>Izberi DOCX datoteko</span
>
<button
type="submit"
class="btn btn-sm btn-primary"
:disabled="
uploadForm.processing ||
!uploadForm.file ||
(!uploadForm.slug && !selectedSlug)
"
>
<span v-if="uploadForm.processing">Nalaganje</span>
<span v-else>Shrani verzijo</span>
</button>
</div>
<div v-if="uploadForm.errors.file" class="text-rose-600 text-xs">
{{ uploadForm.errors.file }}
</div>
</form>
</CardHeader>
<CardContent>
<form @submit.prevent="submitUpload" class="space-y-4">
<div class="grid md:grid-cols-5 gap-4">
<div class="space-y-2">
<Label for="existing_slug">Obstoječi slug</Label>
<Select v-model="selectedSlug">
<SelectTrigger id="existing_slug">
<SelectValue placeholder="(nov)" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="s in uniqueSlugs" :key="s" :value="s">{{
s
}}</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">
<Label for="new_slug">Nov slug</Label>
<Input
id="new_slug"
v-model="uploadForm.slug"
:disabled="!!selectedSlug"
placeholder="opomin"
/>
</div>
<div class="space-y-2">
<Label for="template_name">Naziv</Label>
<Input
id="template_name"
v-model="uploadForm.name"
placeholder="Ime predloge"
/>
</div>
<div class="md:col-span-2 space-y-2">
<Label for="docx-upload-input">DOCX datoteka</Label>
<Input
id="docx-upload-input"
@change="handleFile"
type="file"
accept=".docx"
/>
</div>
</div>
<div class="flex items-center justify-end gap-3">
<span class="text-xs text-muted-foreground" v-if="!uploadForm.file">
Izberi DOCX datoteko
</span>
<Button
type="submit"
size="sm"
:disabled="
uploadForm.processing ||
!uploadForm.file ||
(!uploadForm.slug && !selectedSlug)
"
>
<UploadIcon class="h-4 w-4 mr-2" />
{{ uploadForm.processing ? "Nalaganje…" : "Shrani verzijo" }}
</Button>
</div>
<p v-if="uploadForm.errors.file" class="text-sm text-destructive">
{{ uploadForm.errors.file }}
</p>
</form>
</CardContent>
</Card>
</div>
<!-- Groups -->
<div v-if="groups.length" class="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
<div
v-for="g in groups"
:key="g.slug"
class="group relative flex flex-col bg-white border rounded-lg shadow-sm overflow-hidden"
>
<div
class="px-4 py-3 border-b bg-gradient-to-r from-gray-50 to-white flex items-start justify-between gap-3"
>
<div class="min-w-0">
<h3 class="font-medium text-sm leading-5 truncate">{{ g.name }}</h3>
<div
class="flex flex-wrap items-center gap-2 mt-1 text-[11px] text-gray-500"
>
<span class="px-1.5 py-0.5 bg-gray-100 rounded">{{ g.slug }}</span>
<span>Zadnja: v{{ g.versions[0].version }}</span>
<span
class="flex items-center gap-1"
:class="
g.versions.filter((v) => v.active).length
? 'text-emerald-600'
: 'text-gray-400'
"
>
<span
class="w-1.5 h-1.5 rounded-full"
:class="
g.versions.filter((v) => v.active).length
? 'bg-emerald-500'
: 'bg-gray-300'
<Card v-for="g in groups" :key="g.slug">
<CardHeader>
<div class="flex items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<CardTitle class="text-base truncate">{{ g.name }}</CardTitle>
<CardDescription class="flex flex-wrap items-center gap-2 mt-1">
<Badge variant="secondary" class="text-xs">{{ g.slug }}</Badge>
<span class="text-xs">Zadnja: v{{ g.versions[0].version }}</span>
<Badge
:variant="
g.versions.filter((v) => v.active).length ? 'default' : 'outline'
"
/>
{{ g.versions.filter((v) => v.active).length }} aktivnih
</span>
class="text-xs"
>
{{ g.versions.filter((v) => v.active).length }} aktivnih
</Badge>
</CardDescription>
</div>
<Button size="sm" variant="ghost" as-child>
<Link :href="route('admin.document-templates.show', g.versions[0].id)">
Detalji
</Link>
</Button>
</div>
<Link
:href="route('admin.document-templates.show', g.versions[0].id)"
class="text-xs text-indigo-600 hover:underline whitespace-nowrap mt-1"
>Detalji</Link
>
</div>
<div class="p-3 flex-1 flex flex-col gap-2">
</CardHeader>
<CardContent class="flex flex-col gap-4">
<div class="flex flex-wrap gap-2">
<div v-for="v in g.versions" :key="v.id" class="flex items-center gap-1">
<Link
:href="route('admin.document-templates.edit', v.id)"
class="px-2 py-0.5 rounded-md border text-[11px] font-medium transition-colors"
:class="
v.active
? 'border-emerald-500/60 bg-emerald-50 text-emerald-700 hover:bg-emerald-100'
: 'border-gray-300 bg-white text-gray-600 hover:bg-gray-50'
"
>v{{ v.version }}</Link
<Button
size="sm"
:variant="v.active ? 'default' : 'outline'"
class="h-7 px-2 text-xs"
as-child
>
<button
<Link :href="route('admin.document-templates.edit', v.id)">
v{{ v.version }}
</Link>
</Button>
<Button
type="button"
size="icon"
:variant="v.active ? 'destructive' : 'outline'"
class="h-7 w-7"
@click="toggle(v.id)"
class="rounded-md border px-1.5 py-0.5 text-[10px] font-medium transition-colors"
:class="
v.active
? 'bg-amber-500 border-amber-500 text-white hover:bg-amber-600'
: 'bg-gray-100 border-gray-300 text-gray-600 hover:bg-gray-200'
"
>
{{ v.active ? "✕" : "✓" }}
</button>
<XIcon v-if="v.active" class="h-3 w-3" />
<CheckIcon v-else class="h-3 w-3" />
</Button>
</div>
</div>
<div class="mt-auto pt-2 border-t flex justify-end">
<Link
:href="route('admin.document-templates.edit', g.versions[0].id)"
class="text-[11px] text-indigo-600 hover:underline"
>Uredi zadnjo verzijo </Link
>
<div class="pt-2 border-t flex justify-end">
<Button size="sm" variant="link" class="h-auto p-0" as-child>
<Link :href="route('admin.document-templates.edit', g.versions[0].id)">
Uredi zadnjo verziju
</Link>
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
<p v-else class="text-sm text-gray-500">Ni predlog.</p>
<p v-else class="text-sm text-muted-foreground">Ni predlog.</p>
</div>
</AdminLayout>
</template>