Admin panel updated with shadcn-vue components
This commit is contained in:
@@ -1,23 +1,23 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { Head, Link, router, usePage } from "@inertiajs/vue3";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { faArrowLeft } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
faUserGroup,
|
||||
faShieldHalved,
|
||||
faArrowLeft,
|
||||
faFileWord,
|
||||
faBars,
|
||||
faGears,
|
||||
faKey,
|
||||
faEnvelope,
|
||||
faEnvelopeOpenText,
|
||||
faAt,
|
||||
faInbox,
|
||||
faFileLines,
|
||||
faMessage,
|
||||
faAddressBook,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
MenuIcon,
|
||||
ChevronDownIcon,
|
||||
ShieldCheckIcon,
|
||||
UsersIcon,
|
||||
KeyRoundIcon,
|
||||
Settings2Icon,
|
||||
FileTextIcon,
|
||||
MailOpenIcon,
|
||||
InboxIcon,
|
||||
AtSignIcon,
|
||||
BookUserIcon,
|
||||
MessageSquareIcon,
|
||||
ArrowLeftIcon,
|
||||
} from "lucide-vue-next";
|
||||
import Dropdown from "@/Components/Dropdown.vue";
|
||||
import DropdownLink from "@/Components/DropdownLink.vue";
|
||||
import GlobalSearch from "@/Layouts/Partials/GlobalSearch.vue";
|
||||
@@ -29,25 +29,61 @@ import { Button } from "@/Components/ui/button";
|
||||
|
||||
const props = defineProps({ title: { type: String, default: "Administrator" } });
|
||||
|
||||
// Basic state reused (simplified vs AppLayout)
|
||||
// Collapsible sidebar state (persisted when user explicitly toggles)
|
||||
const sidebarCollapsed = ref(false);
|
||||
const hasSavedSidebarPref = ref(false);
|
||||
// Mobile off-canvas state
|
||||
const isMobile = ref(false);
|
||||
const mobileSidebarOpen = ref(false);
|
||||
function handleResize() {
|
||||
|
||||
function applyAutoCollapse() {
|
||||
if (typeof window === "undefined") return;
|
||||
isMobile.value = window.innerWidth < 1024;
|
||||
if (!isMobile.value) mobileSidebarOpen.value = false;
|
||||
sidebarCollapsed.value = isMobile.value; // auto collapse on small
|
||||
isMobile.value = window.innerWidth < 1024; // Tailwind lg breakpoint
|
||||
sidebarCollapsed.value = isMobile.value;
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
if (typeof window !== "undefined") {
|
||||
isMobile.value = window.innerWidth < 1024;
|
||||
if (!isMobile.value) mobileSidebarOpen.value = false; // close drawer when switching to desktop
|
||||
}
|
||||
if (!hasSavedSidebarPref.value) applyAutoCollapse();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleResize();
|
||||
try {
|
||||
const saved = localStorage.getItem("sidebarCollapsed");
|
||||
if (saved !== null) {
|
||||
hasSavedSidebarPref.value = true;
|
||||
sidebarCollapsed.value = saved === "1";
|
||||
} else {
|
||||
applyAutoCollapse();
|
||||
}
|
||||
} catch {}
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => window.removeEventListener("resize", handleResize));
|
||||
|
||||
watch(sidebarCollapsed, (v) => {
|
||||
if (!hasSavedSidebarPref.value) return; // don't persist auto behavior
|
||||
try {
|
||||
localStorage.setItem("sidebarCollapsed", v ? "1" : "0");
|
||||
} catch {}
|
||||
});
|
||||
|
||||
function toggleSidebar() {
|
||||
if (isMobile.value) mobileSidebarOpen.value = !mobileSidebarOpen.value;
|
||||
else sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
hasSavedSidebarPref.value = true; // user explicitly chose
|
||||
sidebarCollapsed.value = !sidebarCollapsed.value;
|
||||
}
|
||||
|
||||
function toggleMobileSidebar() {
|
||||
mobileSidebarOpen.value = !mobileSidebarOpen.value;
|
||||
}
|
||||
|
||||
function handleSidebarToggleClick() {
|
||||
if (isMobile.value) toggleMobileSidebar();
|
||||
else toggleSidebar();
|
||||
}
|
||||
|
||||
const logout = () => router.post(route("logout"));
|
||||
@@ -63,7 +99,7 @@ const navGroups = computed(() => [
|
||||
key: "admin.dashboard",
|
||||
label: "Pregled",
|
||||
route: "admin.index",
|
||||
icon: faShieldHalved,
|
||||
icon: ShieldCheckIcon,
|
||||
active: ["admin.index"],
|
||||
},
|
||||
],
|
||||
@@ -76,14 +112,14 @@ const navGroups = computed(() => [
|
||||
key: "admin.users",
|
||||
label: "Uporabniki",
|
||||
route: "admin.users.index",
|
||||
icon: faUserGroup,
|
||||
icon: UsersIcon,
|
||||
active: ["admin.users.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.permissions.index",
|
||||
label: "Dovoljenja",
|
||||
route: "admin.permissions.index",
|
||||
icon: faKey,
|
||||
icon: KeyRoundIcon,
|
||||
active: ["admin.permissions.index", "admin.permissions.create"],
|
||||
},
|
||||
],
|
||||
@@ -96,14 +132,14 @@ const navGroups = computed(() => [
|
||||
key: "admin.document-settings.index",
|
||||
label: "Nastavitve dokumentov",
|
||||
route: "admin.document-settings.index",
|
||||
icon: faGears,
|
||||
icon: Settings2Icon,
|
||||
active: ["admin.document-settings.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.document-templates.index",
|
||||
label: "Predloge dokumentov",
|
||||
route: "admin.document-templates.index",
|
||||
icon: faFileWord,
|
||||
icon: FileTextIcon,
|
||||
active: ["admin.document-templates.index"],
|
||||
},
|
||||
],
|
||||
@@ -116,7 +152,7 @@ const navGroups = computed(() => [
|
||||
key: "admin.email-templates.index",
|
||||
label: "Email predloge",
|
||||
route: "admin.email-templates.index",
|
||||
icon: faEnvelopeOpenText,
|
||||
icon: MailOpenIcon,
|
||||
active: [
|
||||
"admin.email-templates.index",
|
||||
"admin.email-templates.create",
|
||||
@@ -127,14 +163,14 @@ const navGroups = computed(() => [
|
||||
key: "admin.email-logs.index",
|
||||
label: "Email dnevniki",
|
||||
route: "admin.email-logs.index",
|
||||
icon: faInbox,
|
||||
icon: InboxIcon,
|
||||
active: ["admin.email-logs.index", "admin.email-logs.show"],
|
||||
},
|
||||
{
|
||||
key: "admin.mail-profiles.index",
|
||||
label: "Mail profili",
|
||||
route: "admin.mail-profiles.index",
|
||||
icon: faAt,
|
||||
icon: AtSignIcon,
|
||||
active: ["admin.mail-profiles.index"],
|
||||
},
|
||||
],
|
||||
@@ -147,7 +183,7 @@ const navGroups = computed(() => [
|
||||
key: "admin.sms-templates.index",
|
||||
label: "SMS predloge",
|
||||
route: "admin.sms-templates.index",
|
||||
icon: faFileLines,
|
||||
icon: FileTextIcon,
|
||||
active: [
|
||||
"admin.sms-templates.index",
|
||||
"admin.sms-templates.create",
|
||||
@@ -158,28 +194,28 @@ const navGroups = computed(() => [
|
||||
key: "admin.sms-logs.index",
|
||||
label: "SMS dnevniki",
|
||||
route: "admin.sms-logs.index",
|
||||
icon: faInbox,
|
||||
icon: InboxIcon,
|
||||
active: ["admin.sms-logs.index", "admin.sms-logs.show"],
|
||||
},
|
||||
{
|
||||
key: "admin.sms-senders.index",
|
||||
label: "SMS pošiljatelji",
|
||||
route: "admin.sms-senders.index",
|
||||
icon: faAddressBook,
|
||||
icon: BookUserIcon,
|
||||
active: ["admin.sms-senders.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.sms-profiles.index",
|
||||
label: "SMS profili",
|
||||
route: "admin.sms-profiles.index",
|
||||
icon: faGears,
|
||||
icon: Settings2Icon,
|
||||
active: ["admin.sms-profiles.index"],
|
||||
},
|
||||
{
|
||||
key: "admin.packages.index",
|
||||
label: "SMS paketi",
|
||||
route: "admin.packages.index",
|
||||
icon: faMessage,
|
||||
icon: MessageSquareIcon,
|
||||
active: ["admin.packages.index", "admin.packages.show"],
|
||||
},
|
||||
],
|
||||
@@ -215,7 +251,9 @@ function isActive(patterns) {
|
||||
: 'sticky top-0 h-screen overflow-y-auto',
|
||||
]"
|
||||
>
|
||||
<div class="h-16 px-4 flex items-center justify-between border-b border-gray-200 bg-white">
|
||||
<div
|
||||
class="h-16 px-4 flex items-center justify-between border-b border-gray-200 bg-white"
|
||||
>
|
||||
<Link
|
||||
:href="route('dashboard')"
|
||||
class="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||
@@ -230,49 +268,49 @@ function isActive(patterns) {
|
||||
</Link>
|
||||
</div>
|
||||
<nav class="py-4 overflow-y-auto">
|
||||
<div v-for="group in navGroups" :key="group.key" class="mt-2 first:mt-0 px-2">
|
||||
<p
|
||||
v-if="!sidebarCollapsed"
|
||||
class="px-4 py-1.5 mb-1 mt-4 first:mt-0 text-[11px] font-semibold uppercase tracking-wider text-gray-400"
|
||||
>
|
||||
{{ group.label }}
|
||||
</p>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="item in group.items" :key="item.key">
|
||||
<Link
|
||||
:href="route(item.route)"
|
||||
:title="item.label"
|
||||
:class="[
|
||||
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
||||
isActive(item.active)
|
||||
? 'bg-primary-50 text-primary-700 font-medium shadow-sm'
|
||||
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900',
|
||||
]"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
:icon="item.icon"
|
||||
<ul class="space-y-4 px-2">
|
||||
<li v-for="group in navGroups" :key="group.label">
|
||||
<div
|
||||
v-if="!sidebarCollapsed"
|
||||
class="px-4 py-1.5 text-[11px] font-semibold uppercase tracking-wider text-sidebar-foreground/60"
|
||||
>
|
||||
{{ group.label }}
|
||||
</div>
|
||||
<ul class="space-y-0.5">
|
||||
<li v-for="item in group.items" :key="item.key">
|
||||
<Link
|
||||
:href="route(item.route)"
|
||||
:title="item.label"
|
||||
:class="[
|
||||
'w-5 h-5 flex-shrink-0 transition-colors',
|
||||
isActive(item.active) ? 'text-primary-600' : 'text-gray-500',
|
||||
'flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-all duration-150',
|
||||
isActive(item.active)
|
||||
? 'bg-sidebar-primary/15 text-sidebar-primary font-medium shadow-sm'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
|
||||
]"
|
||||
/>
|
||||
<span
|
||||
v-if="!sidebarCollapsed"
|
||||
class="truncate transition-opacity"
|
||||
:class="{ 'font-medium': isActive(item.active) }"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<component
|
||||
v-if="item.icon"
|
||||
:is="item.icon"
|
||||
class="w-5 h-5 shrink-0 transition-colors"
|
||||
/>
|
||||
<span
|
||||
v-if="!sidebarCollapsed"
|
||||
class="truncate transition-opacity"
|
||||
:class="{ 'font-medium': isActive(item.active) }"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mt-6 border-t border-gray-200 pt-4 space-y-2 px-4">
|
||||
<Link
|
||||
:href="route('dashboard')"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 hover:underline flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
class="text-xs hover:underline flex items-center gap-2 px-3 py-2 rounded-lg text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground transition-all duration-150"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faArrowLeft" class="w-3.5 h-3.5" />
|
||||
<ArrowLeftIcon size="18" />
|
||||
<span v-if="!sidebarCollapsed">Nazaj na aplikacijo</span>
|
||||
</Link>
|
||||
</div>
|
||||
@@ -287,10 +325,11 @@ function isActive(patterns) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@click="toggleSidebar"
|
||||
@click="handleSidebarToggleClick"
|
||||
:title="sidebarCollapsed ? 'Razširi meni' : 'Skrči meni'"
|
||||
aria-label="Toggle sidebar"
|
||||
>
|
||||
<FontAwesomeIcon :icon="faBars" class="w-5 h-5" />
|
||||
<MenuIcon />
|
||||
</Button>
|
||||
<h1 class="text-base font-semibold text-gray-900 hidden sm:block">
|
||||
{{ title }}
|
||||
@@ -314,27 +353,9 @@ function isActive(patterns) {
|
||||
</button>
|
||||
|
||||
<span v-else class="inline-flex">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="default"
|
||||
type="button"
|
||||
class="gap-2"
|
||||
>
|
||||
<Button variant="outline" size="default" type="button" class="gap-2">
|
||||
{{ $page.props.auth.user.name }}
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</span>
|
||||
</template>
|
||||
@@ -358,10 +379,7 @@ function isActive(patterns) {
|
||||
</div>
|
||||
|
||||
<!-- Page Heading -->
|
||||
<header
|
||||
v-if="$slots.header"
|
||||
class="bg-white border-b border-gray-200 shadow-sm"
|
||||
>
|
||||
<header v-if="$slots.header" class="bg-white border-b border-gray-200 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8 space-y-2">
|
||||
<Breadcrumbs
|
||||
v-if="$page.props.breadcrumbs && $page.props.breadcrumbs.length"
|
||||
@@ -376,10 +394,7 @@ function isActive(patterns) {
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<GlobalSearch :open="false" />
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<ToastContainer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user