210 lines
6.7 KiB
Vue
210 lines
6.7 KiB
Vue
<script setup lang="ts">
|
|
import { LazyLinkModal, LazyConfirmModal, LazyRenameFileModal } from "#components";
|
|
|
|
definePageMeta({ middleware: "auth" });
|
|
|
|
const toast = useToast();
|
|
const overlay = useOverlay();
|
|
const fileInput = ref<HTMLInputElement | null>(null);
|
|
|
|
const { fetch: fetchSession } = useUserSession();
|
|
|
|
const signOut = async () => {
|
|
await $fetch("/api/auth/sign-out", { method: "POST" });
|
|
await fetchSession();
|
|
await navigateTo("/auth/sign-in");
|
|
};
|
|
|
|
const { data: links, status: linksStatus, refresh: refreshLinks } = useLazyFetch("/api/links", { key: "links", server: false });
|
|
const { data: files, status: filesStatus, refresh: refreshFiles } = useLazyFetch("/api/files", { key: "files", server: false });
|
|
|
|
const route = useRoute();
|
|
const section = computed(() => route.query.section === "files" ? "files" : "links");
|
|
const category = computed(() => {
|
|
if (section.value !== "links") return "all";
|
|
if (route.query.filter === "active") return "active";
|
|
if (route.query.filter === "disabled") return "disabled";
|
|
return "all";
|
|
});
|
|
|
|
const categoryTitle = computed(() => {
|
|
if (category.value === "active") return "Active links";
|
|
if (category.value === "disabled") return "Disabled links";
|
|
return "All links";
|
|
});
|
|
|
|
const pageTitle = computed(() => section.value === "files" ? "Files" : categoryTitle.value);
|
|
useHead({ title: pageTitle });
|
|
|
|
const filteredLinks = computed(() => {
|
|
if (!links.value) return [];
|
|
if (category.value === "active") return links.value.filter((l) => !l.disabled);
|
|
if (category.value === "disabled") return links.value.filter((l) => l.disabled);
|
|
return links.value;
|
|
});
|
|
|
|
// Link actions
|
|
const openModal = async (link: Link | null) => {
|
|
const modal = overlay.create(LazyLinkModal, { destroyOnClose: true });
|
|
const instance = modal.open({ link });
|
|
if (await instance.result) await refreshLinks();
|
|
};
|
|
|
|
const deleteLink = async (link: Link) => {
|
|
const modal = overlay.create(LazyConfirmModal, { destroyOnClose: true });
|
|
const instance = modal.open({ title: "Delete link", description: `Are you sure you want to delete "${link.name}"?` });
|
|
if (!await instance.result) return;
|
|
|
|
try {
|
|
await $fetch(`/api/links/${link.id}`, { method: "DELETE" });
|
|
toast.add({ title: "Link deleted", color: "success" });
|
|
await refreshLinks();
|
|
} catch (error) {
|
|
toast.add({ title: getApiError(error), color: "error" });
|
|
}
|
|
};
|
|
|
|
// File actions
|
|
const uploading = ref(false);
|
|
|
|
const handleUpload = async (event: Event) => {
|
|
const input = event.target as HTMLInputElement;
|
|
if (!input.files || input.files.length === 0) return;
|
|
|
|
uploading.value = true;
|
|
try {
|
|
for (const file of Array.from(input.files)) {
|
|
const form = new FormData();
|
|
form.append("file", file);
|
|
await $fetch("/api/files", { method: "POST", body: form });
|
|
}
|
|
toast.add({ title: `${input.files.length === 1 ? "File" : "Files"} uploaded`, color: "success" });
|
|
await refreshFiles();
|
|
} catch (error) {
|
|
toast.add({ title: getApiError(error), color: "error" });
|
|
} finally {
|
|
uploading.value = false;
|
|
input.value = "";
|
|
}
|
|
};
|
|
|
|
const renameFile = async (file: FileEntry) => {
|
|
const modal = overlay.create(LazyRenameFileModal, { destroyOnClose: true });
|
|
const instance = modal.open({ filename: file.name });
|
|
if (await instance.result) await refreshFiles();
|
|
};
|
|
|
|
const deleteFile = async (file: FileEntry) => {
|
|
const modal = overlay.create(LazyConfirmModal, { destroyOnClose: true });
|
|
const instance = modal.open({ title: "Delete file", description: `Are you sure you want to delete "${file.name}"?` });
|
|
if (!await instance.result) return;
|
|
|
|
try {
|
|
await $fetch(`/api/files/${encodeURIComponent(file.name)}`, { method: "DELETE" });
|
|
toast.add({ title: "File deleted", color: "success" });
|
|
await refreshFiles();
|
|
} catch (error) {
|
|
toast.add({ title: getApiError(error), color: "error" });
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<UDashboardGroup>
|
|
<UDashboardSidebar :toggle="false" :ui="{ header: 'border-b border-default' }">
|
|
<template #header>
|
|
<span class="font-semibold text-highlighted text-lg">pihka.al</span>
|
|
</template>
|
|
|
|
<div class="px-2 space-y-2">
|
|
<UNavigationMenu
|
|
orientation="vertical"
|
|
highlight
|
|
:items="[
|
|
{
|
|
label: 'All',
|
|
icon: 'i-lucide-link',
|
|
badge: links?.length ?? 0,
|
|
to: '/dashboard',
|
|
active: section === 'links' && category === 'all',
|
|
},
|
|
{
|
|
label: 'Active',
|
|
icon: 'i-lucide-link-2',
|
|
badge: links?.filter((l) => !l.disabled).length ?? 0,
|
|
to: '/dashboard?filter=active',
|
|
active: section === 'links' && category === 'active',
|
|
},
|
|
{
|
|
label: 'Disabled',
|
|
icon: 'i-lucide-link-2-off',
|
|
badge: links?.filter((l) => l.disabled).length ?? 0,
|
|
to: '/dashboard?filter=disabled',
|
|
active: section === 'links' && category === 'disabled',
|
|
},
|
|
]"
|
|
/>
|
|
|
|
<USeparator />
|
|
|
|
<UNavigationMenu
|
|
orientation="vertical"
|
|
highlight
|
|
:items="[
|
|
{
|
|
label: 'Files',
|
|
icon: 'i-lucide-folder',
|
|
badge: files?.length ?? 0,
|
|
to: '/dashboard?section=files',
|
|
active: section === 'files',
|
|
},
|
|
]"
|
|
/>
|
|
</div>
|
|
|
|
<template #footer>
|
|
<UButton variant="link" icon="i-lucide-log-out" block @click="signOut">Sign out</UButton>
|
|
</template>
|
|
</UDashboardSidebar>
|
|
|
|
<UDashboardPanel v-if="section === 'links'">
|
|
<template #header>
|
|
<UDashboardNavbar :title="categoryTitle">
|
|
<template #right>
|
|
<UButton icon="i-lucide-plus" variant="subtle" @click="openModal(null)">New link</UButton>
|
|
</template>
|
|
</UDashboardNavbar>
|
|
</template>
|
|
|
|
<template #body>
|
|
<LinksTable
|
|
:data="filteredLinks"
|
|
:status="linksStatus"
|
|
@edit="openModal"
|
|
@delete="deleteLink"
|
|
/>
|
|
</template>
|
|
</UDashboardPanel>
|
|
|
|
<UDashboardPanel v-else>
|
|
<template #header>
|
|
<UDashboardNavbar title="Files">
|
|
<template #right>
|
|
<input ref="fileInput" type="file" class="hidden" multiple @change="handleUpload" />
|
|
<UButton icon="i-lucide-upload" variant="subtle" :loading="uploading" @click="(fileInput as HTMLInputElement)?.click()">Upload</UButton>
|
|
</template>
|
|
</UDashboardNavbar>
|
|
</template>
|
|
|
|
<template #body>
|
|
<FilesTable
|
|
:data="files ?? []"
|
|
:status="filesStatus"
|
|
@rename="renameFile"
|
|
@delete="deleteFile"
|
|
/>
|
|
</template>
|
|
</UDashboardPanel>
|
|
</UDashboardGroup>
|
|
</template>
|