list warrens + explore nested folders

This commit is contained in:
2025-07-12 06:39:43 +02:00
parent f9f55895ed
commit 4d0765c53b
38 changed files with 1877 additions and 93 deletions

3
frontend/.env.dev Normal file
View File

@@ -0,0 +1,3 @@
# this file is ignored when the app is built since we're using SSG
NUXT_PUBLIC_API_BASE="http://127.0.0.1:8080/api"

1
frontend/.gitignore vendored
View File

@@ -22,3 +22,4 @@ logs
.env
.env.*
!.env.example
!.env.dev

View File

@@ -1,3 +1,11 @@
<script setup lang="ts">
import { getWarrens } from './lib/api/warrens';
const store = useWarrenStore();
store.warrens = await getWarrens();
</script>
<template>
<NuxtRouteAnnouncer />
<NuxtLoadingIndicator />

View File

@@ -9,6 +9,7 @@
"@nuxt/icon": "1.15.0",
"@nuxt/image": "1.10.0",
"@nuxt/test-utils": "3.19.2",
"@pinia/nuxt": "^0.11.1",
"@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^13.5.0",
"class-variance-authority": "^0.7.1",
@@ -16,6 +17,7 @@
"eslint": "^9.0.0",
"lucide-vue-next": "^0.525.0",
"nuxt": "^3.17.6",
"pinia": "^3.0.3",
"reka-ui": "^2.3.2",
"shadcn-nuxt": "2.2.0",
"tailwind-merge": "^3.3.1",
@@ -369,6 +371,8 @@
"@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.1", "", { "os": "win32", "cpu": "x64" }, "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA=="],
"@pinia/nuxt": ["@pinia/nuxt@0.11.1", "", { "dependencies": { "@nuxt/kit": "^3.9.0" }, "peerDependencies": { "pinia": "^3.0.3" } }, "sha512-tCD8ioWhhIHKwm8Y9VvyhBAV/kK4W5uGBIYbI5iM4N1t7duOqK6ECBUavrMxMolELayqqMLb9+evegrh3S7s2A=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
@@ -547,7 +551,7 @@
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.17", "", { "dependencies": { "@vue/compiler-dom": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ=="],
"@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@vue/devtools-api": ["@vue/devtools-api@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7" } }, "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg=="],
"@vue/devtools-core": ["@vue/devtools-core@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7", "@vue/devtools-shared": "^7.7.7", "mitt": "^3.0.1", "nanoid": "^5.1.0", "pathe": "^2.0.3", "vite-hot-client": "^2.0.4" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ=="],
@@ -1445,6 +1449,8 @@
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"pinia": ["pinia@3.0.3", "", { "dependencies": { "@vue/devtools-api": "^7.7.2" }, "peerDependencies": { "typescript": ">=4.4.4", "vue": "^2.7.0 || ^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA=="],
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
@@ -2199,6 +2205,8 @@
"vite-plugin-vue-tracer/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"winston/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"winston/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],

View File

@@ -14,18 +14,7 @@ import {
const route = useRoute();
const warrens = [
{
title: 'Thyr',
url: '/warrens/Thyr',
icon: h(Icon, { name: 'lucide:folder-root' }),
},
{
title: 'Serc',
url: '/warrens/Serc',
icon: h(Icon, { name: 'lucide:folder-root' }),
},
];
const store = useWarrenStore();
</script>
<template>
@@ -57,22 +46,24 @@ const warrens = [
<CollapsibleContent>
<SidebarMenuSub>
<SidebarMenuSubItem
v-for="warren in warrens"
:key="warren.title"
v-for="(warren, uuid) in store.warrens"
:key="uuid"
>
<SidebarMenuSubButton
as-child
:tooltip="warren.title"
:tooltip="warren.name"
:is-active="
warren.url === route.path
route.path.startsWith(
`/warrens/${uuid}`
)
"
class="transition-all"
>
<NuxtLink :to="warren.url">
<component
:is="warren.icon"
></component>
<span>{{ warren.title }}</span>
<NuxtLink :to="`/warrens/${uuid}`">
<Icon
name="lucide:folder-root"
/>
<span>{{ warren.name }}</span>
</NuxtLink>
</SidebarMenuSubButton>
</SidebarMenuSubItem>

View File

@@ -1,24 +1,30 @@
<script setup lang="ts">
import type { DirectoryEntryType } from '~/types';
import type { FileType } from '~/types';
const route = useRoute();
const { name, entryType } = defineProps<{
const { name, entryType, disabled } = defineProps<{
name: string;
entryType: DirectoryEntryType;
entryType: FileType;
disabled: boolean,
}>();
const iconName = entryType === 'file' ? 'lucide:file' : 'lucide:folder';
</script>
<template>
<Button class="w-36 h-12" variant="outline" size="lg">
<NuxtLink
class="flex flex-row items-center gap-1.5"
:to="joinPaths(route.path, name)"
<NuxtLink
:to="joinPaths(route.path, name)"
:class="['select-none', { 'pointer-events-none': disabled }]"
>
<Button
class="w-44 h-12"
variant="outline"
size="lg"
:disabled="disabled"
>
<Icon :name="iconName" />
<span>{{ name }}</span>
</NuxtLink>
</Button>
<span class="truncate">{{ name }}</span>
</Button>
</NuxtLink>
</template>

View File

@@ -1,34 +1,22 @@
<script setup lang="ts">
import { ScrollArea } from '@/components/ui/scroll-area';
import type { DirectoryEntryType } from '~/types';
const items: Array<{ name: string; entryType: DirectoryEntryType }> = [
/* {
name: 'File A',
entryType: 'file',
},
{
name: 'File B',
entryType: 'file',
}, */
{
name: 'Directory A',
entryType: 'directory',
},
{
name: 'Directory B',
entryType: 'directory',
},
];
import type { DirectoryEntry } from '~/types';
const { entries } = defineProps<{
entries: DirectoryEntry[];
}>();
const { isLoading } = useLoadingIndicator();
</script>
<template>
<ScrollArea class="w-full h-full">
<div class="flex flex-row gap-2">
<DirectoryEntry
v-for="item in items"
:key="item.name"
:name="item.name"
:entry-type="item.entryType"
v-for="entry in entries"
:key="entry.name"
:name="entry.name"
:entry-type="entry.fileType"
:disabled="isLoading"
/>
</div>
</ScrollArea>

View File

@@ -0,0 +1,29 @@
import type { DirectoryEntry } from '~/types';
import type { Warren } from '~/types/warrens';
export async function getWarrens(): Promise<Record<string, Warren>> {
const arr: Warren[] = await $fetch(getApiUrl('warrens'), {
method: 'GET',
});
const warrens: Record<string, Warren> = {};
for (const warren of arr) {
warrens[warren.id] = warren;
}
return warrens;
}
export async function getWarrenDirectory(
path: string
): Promise<DirectoryEntry[]> {
const entries: DirectoryEntry[] = await $fetch(
getApiUrl(`warrens/${path}`),
{
method: 'GET',
}
);
return entries;
}

View File

@@ -13,6 +13,7 @@ export default defineNuxtConfig({
'@nuxt/test-utils',
'shadcn-nuxt',
'@nuxtjs/color-mode',
'@pinia/nuxt',
],
css: ['~/assets/css/tailwind.css'],
@@ -53,4 +54,10 @@ export default defineNuxtConfig({
},
ssr: false,
runtimeConfig: {
public: {
apiBase: '/api',
},
},
});

View File

@@ -4,7 +4,7 @@
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"dev": "nuxt dev --dotenv .env.dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
@@ -16,6 +16,7 @@
"@nuxt/icon": "1.15.0",
"@nuxt/image": "1.10.0",
"@nuxt/test-utils": "3.19.2",
"@pinia/nuxt": "^0.11.1",
"@tailwindcss/vite": "^4.1.11",
"@vueuse/core": "^13.5.0",
"class-variance-authority": "^0.7.1",
@@ -23,6 +24,7 @@
"eslint": "^9.0.0",
"lucide-vue-next": "^0.525.0",
"nuxt": "^3.17.6",
"pinia": "^3.0.3",
"reka-ui": "^2.3.2",
"shadcn-nuxt": "2.2.0",
"tailwind-merge": "^3.3.1",

View File

@@ -1,3 +1,9 @@
<script setup lang="ts">
import { getWarrenDirectory } from '~/lib/api/warrens';
const route = useRoute();
const entries = await getWarrenDirectory(route.path.split('/warrens/')[1]);
</script>
<template>
<DirectoryList />
<DirectoryList :entries="entries" />
</template>

View File

@@ -1,28 +1,22 @@
<script setup lang="ts">
import { ScrollArea } from '@/components/ui/scroll-area';
const warrens = ['Thyr', 'Serc'];
const store = useWarrenStore();
</script>
<template>
<ScrollArea class="w-full h-full">
<div class="flex flex-row gap-2">
<Button
v-for="(warren, i) in warrens"
:key="i"
class="w-36 h-12"
variant="outline"
size="lg"
as-child
<NuxtLink
v-for="(warren, uuid) in store.warrens"
:key="uuid"
:to="`/warrens/${uuid}`"
>
<NuxtLink
class="flex flex-row items-center gap-1.5"
:to="`/warrens/${warren}`"
>
<Button class="w-44 h-12" variant="outline" size="lg">
<Icon name="lucide:folder-root" />
{{ warren }}
</NuxtLink>
</Button>
<span clas="truncate">{{ warren.name }}</span>
</Button>
</NuxtLink>
</div>
</ScrollArea>
</template>

8
frontend/stores/index.ts Normal file
View File

@@ -0,0 +1,8 @@
import { defineStore } from 'pinia';
import type { Warren } from '~/types/warrens';
export const useWarrenStore = defineStore('warrens', {
state: () => ({
warrens: {} as Record<string, Warren>,
}),
});

View File

@@ -1,6 +1,11 @@
export type DirectoryEntryType = 'file' | 'directory';
export type FileType = 'file' | 'directory';
export type BreadcrumbData = {
name: string;
href: string;
};
export type DirectoryEntry = {
name: string;
fileType: FileType;
};

View File

@@ -0,0 +1,4 @@
export type Warren = {
id: string,
name: string,
};

5
frontend/utils/api.ts Normal file
View File

@@ -0,0 +1,5 @@
export function getApiUrl(path: string): string {
const API_BASE_URL = useRuntimeConfig().public.apiBase;
console.log(API_BASE_URL);
return `${API_BASE_URL}/${path}`;
}

View File

@@ -1,6 +1,8 @@
import type { BreadcrumbData } from '~/types';
export function getBreadcrumbs(path: string): BreadcrumbData[] {
const { warrens } = useWarrenStore();
const crumbs = path
.split('/')
.filter((v) => v.length > 0)
@@ -21,6 +23,14 @@ export function getBreadcrumbs(path: string): BreadcrumbData[] {
.join('/');
}
if (
crumbs.length >= 3 &&
crumbs[1].href === '/warrens' &&
crumbs[2].name in warrens
) {
crumbs[2].name = warrens[crumbs[2].name].name;
}
return crumbs;
}