init
Some checks failed
Build & Deploy Nuxt (Docker) / deploy (push) Failing after 6s

This commit is contained in:
Tom Trappmann
2025-12-22 21:30:18 +01:00
commit c0b06c1927
32 changed files with 17721 additions and 0 deletions

24
NautilusDesk/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example

75
NautilusDesk/README.md Normal file
View File

@@ -0,0 +1,75 @@
# Nuxt Minimal Starter
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install dependencies:
```bash
# npm
npm install
# pnpm
pnpm install
# yarn
yarn install
# bun
bun install
```
## Development Server
Start the development server on `http://localhost:3000`:
```bash
# npm
npm run dev
# pnpm
pnpm dev
# yarn
yarn dev
# bun
bun run dev
```
## Production
Build the application for production:
```bash
# npm
npm run build
# pnpm
pnpm build
# yarn
yarn build
# bun
bun run build
```
Locally preview production build:
```bash
# npm
npm run preview
# pnpm
pnpm preview
# yarn
yarn preview
# bun
bun run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

View File

@@ -0,0 +1,10 @@
export default defineAppConfig({
theme: {
primaryColor: '#fff',
},
ui: {
colors: {
neutral: 'zinc'
}
}
})

13
NautilusDesk/app/app.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<UApp>
<UMain>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</UMain>
</UApp>
</template>
<script setup lang="ts">
</script>

View File

@@ -0,0 +1,2 @@
@import "tailwindcss";
@import "@nuxt/ui";

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'
const route = useRoute()
const items = computed<NavigationMenuItem[]>(() => [
{
label: 'Docs',
to: '/docs/getting-started',
active: route.path.startsWith('/docs/getting-started')
},
])
</script>
<template>
<UHeader>
<template #title>
<Logo class="h-6 w-auto" />
<h3>NautilusDesk</h3>
</template>
<UNavigationMenu :items="items" />
<template #right>
<UColorModeButton />
</template>
</UHeader>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<NuxtLink to="/" class="flex items-center gap-2">
<img src="/icon.png" alt="Logo" class="h-full w-auto" />
</NuxtLink>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
defineProps<{
collapsed?: boolean
}>()
const teams = ref([{
label: 'VTM-Dive',
avatar: {
src: '',
alt: 'VTM-Dive'
}
},])
const selectedTeam = ref(teams.value[0])
const items = computed<DropdownMenuItem[][]>(() => {
return [teams.value.map(team => ({
...team,
onSelect() {
selectedTeam.value = team
}
})), [{
label: 'Create team',
icon: 'i-lucide-circle-plus'
}, {
label: 'Manage teams',
icon: 'i-lucide-cog'
}]]
})
</script>
<template>
<UDropdownMenu :items="items" :content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: collapsed ? 'w-40' : 'w-(--reka-dropdown-menu-trigger-width)' }">
<UButton v-bind="{
...selectedTeam,
label: collapsed ? undefined : selectedTeam?.label,
trailingIcon: collapsed ? undefined : 'i-lucide-chevrons-up-down'
}" color="neutral" variant="ghost" block :square="collapsed" class="data-[state=open]:bg-elevated"
:class="[!collapsed && 'py-2']" :ui="{
trailingIcon: 'text-dimmed'
}" />
</UDropdownMenu>
</template>

View File

@@ -0,0 +1,87 @@
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
defineProps<{
collapsed?: boolean
}>()
const colorMode = useColorMode()
const appConfig = useAppConfig()
const user = ref({
name: 'Tom Trappmann',
avatar: {
src: '',
alt: 'Tom Trappmann'
}
})
const items = computed<DropdownMenuItem[][]>(() => ([[{
type: 'label',
label: user.value.name,
avatar: user.value.avatar
}], [{
label: 'Profile',
icon: 'i-lucide-user'
}, {
label: 'Billing',
icon: 'i-lucide-credit-card'
}, {
label: 'Settings',
icon: 'i-lucide-settings',
to: '/settings'
}], [{
label: 'Appearance',
icon: 'i-lucide-sun-moon',
children: [{
label: 'Light',
icon: 'i-lucide-sun',
type: 'checkbox',
checked: colorMode.value === 'light',
onSelect(e: Event) {
e.preventDefault()
colorMode.preference = 'light'
}
}, {
label: 'Dark',
icon: 'i-lucide-moon',
type: 'checkbox',
checked: colorMode.value === 'dark',
onUpdateChecked(checked: boolean) {
if (checked) {
colorMode.preference = 'dark'
}
},
onSelect(e: Event) {
e.preventDefault()
}
}]
}], [{
label: 'Log out',
icon: 'i-lucide-log-out'
}]]))
</script>
<template>
<UDropdownMenu :items="items" :content="{ align: 'center', collisionPadding: 12 }"
:ui="{ content: collapsed ? 'w-48' : 'w-(--reka-dropdown-menu-trigger-width)' }">
<UButton v-bind="{
...user,
label: collapsed ? undefined : user?.name,
trailingIcon: collapsed ? undefined : 'i-lucide-chevrons-up-down'
}" color="neutral" variant="ghost" block :square="collapsed" class="data-[state=open]:bg-elevated" :ui="{
trailingIcon: 'text-dimmed'
}" />
<template #chip-leading="{ item }">
<div class="inline-flex items-center justify-center shrink-0 size-5">
<span class="rounded-full ring ring-bg bg-(--chip-light) dark:bg-(--chip-dark) size-2" :style="{
'--chip-light': `var(--color-${(item as any).chip}-500)`,
'--chip-dark': `var(--color-${(item as any).chip}-400)`
}" />
</div>
</template>
</UDropdownMenu>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { formatTimeAgo } from '@vueuse/core'
import type { Notification } from '~/types'
const { isNotificationsSlideoverOpen } = useDashboard()
const { data: notifications } = await useFetch<Notification[]>('/api/notifications')
</script>
<template>
<USlideover v-model:open="isNotificationsSlideoverOpen" title="Notifications">
<template #body>
<NuxtLink v-for="notification in notifications" :key="notification.id" :to="`/inbox?id=${notification.id}`"
class="px-3 py-2.5 rounded-md hover:bg-elevated/50 flex items-center gap-3 relative -mx-3 first:-mt-3 last:-mb-3">
<UChip color="error" :show="!!notification.unread" inset>
<UAvatar v-bind="notification.sender.avatar" :alt="notification.sender.name" size="md" />
</UChip>
<div class="text-sm flex-1">
<p class="flex items-center justify-between">
<span class="text-highlighted font-medium">{{ notification.sender.name }}</span>
<time :datetime="notification.date" class="text-muted text-xs"
v-text="formatTimeAgo(new Date(notification.date))" />
</p>
<p class="text-dimmed">
{{ notification.body }}
</p>
</div>
</NuxtLink>
</template>
</USlideover>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
name: z.string().min(2, 'Too short'),
email: z.string().email('Invalid email')
})
const open = ref(false)
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({
name: undefined,
email: undefined
})
const toast = useToast()
async function onSubmit(event: FormSubmitEvent<Schema>) {
toast.add({ title: 'Success', description: `New customer ${event.data.name} added`, color: 'success' })
open.value = false
}
</script>
<template>
<UModal v-model:open="open" title="New customer" description="Add a new customer to the database">
<UButton label="New customer" icon="i-lucide-plus" />
<template #body>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField label="Name" placeholder="John Doe" name="name">
<UInput v-model="state.name" class="w-full" />
</UFormField>
<UFormField label="Email" placeholder="john.doe@example.com" name="email">
<UInput v-model="state.email" class="w-full" />
</UFormField>
<div class="flex justify-end gap-2">
<UButton label="Cancel" color="neutral" variant="subtle" @click="open = false" />
<UButton label="Create" color="primary" variant="solid" type="submit" />
</div>
</UForm>
</template>
</UModal>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
withDefaults(defineProps<{
count?: number
}>(), {
count: 0
})
const open = ref(false)
async function onSubmit() {
await new Promise(resolve => setTimeout(resolve, 1000))
open.value = false
}
</script>
<template>
<UModal v-model:open="open" :title="`Delete ${count} customer${count > 1 ? 's' : ''}`"
:description="`Are you sure, this action cannot be undone.`">
<slot />
<template #body>
<div class="flex justify-end gap-2">
<UButton label="Cancel" color="neutral" variant="subtle" @click="open = false" />
<UButton label="Delete" color="error" variant="solid" loading-auto @click="onSubmit" />
</div>
</template>
</UModal>
</template>

View File

@@ -0,0 +1,3 @@
export function useAppHead() {
useHead({ titleTemplate: 'NautilusDesk · Dive School' })
}

View File

@@ -0,0 +1,25 @@
import { createSharedComposable } from '@vueuse/core'
const _useDashboard = () => {
const route = useRoute()
const router = useRouter()
const isNotificationsSlideoverOpen = ref(false)
defineShortcuts({
'g-h': () => router.push('/'),
'g-i': () => router.push('/inbox'),
'g-c': () => router.push('/customers'),
'g-s': () => router.push('/settings'),
'n': () => isNotificationsSlideoverOpen.value = !isNotificationsSlideoverOpen.value
})
watch(() => route.fullPath, () => {
isNotificationsSlideoverOpen.value = false
})
return {
isNotificationsSlideoverOpen
}
}
export const useDashboard = createSharedComposable(_useDashboard)

View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
const props = defineProps<{ error: NuxtError }>()
</script>
<template>
<UError :error="{
statusCode: 404,
statusMessage: 'Page not found',
message: 'The page you are looking for does not exist.'
}" />
</template>

View File

@@ -0,0 +1,139 @@
<script setup lang="ts">
import type { NavigationMenuItem } from '@nuxt/ui'
import TeamsMenu from '~/components/Menus/TeamsMenu.vue'
import UserMenu from '~/components/Menus/UserMenu.vue'
const route = useRoute()
const toast = useToast()
const open = ref(false)
const links = [[{
label: 'Home',
icon: 'i-lucide-house',
to: '/',
onSelect: () => {
open.value = false
}
}, {
label: 'Customers',
icon: 'i-lucide-users',
to: '/customers',
onSelect: () => {
open.value = false
}
}, {
label: 'Settings',
to: '/settings',
icon: 'i-lucide-settings',
defaultOpen: true,
type: 'trigger',
children: [{
label: 'General',
to: '/settings',
exact: true,
onSelect: () => {
open.value = false
}
}, {
label: 'Members',
to: '/settings/members',
onSelect: () => {
open.value = false
}
}, {
label: 'Notifications',
to: '/settings/notifications',
onSelect: () => {
open.value = false
}
}, {
label: 'Security',
to: '/settings/security',
onSelect: () => {
open.value = false
}
}]
}], [{
label: 'Feedback',
icon: 'i-lucide-message-circle',
to: 'https://github.com/nuxt-ui-templates/dashboard',
target: '_blank'
}, {
label: 'Help & Support',
icon: 'i-lucide-info',
to: 'https://github.com/nuxt-ui-templates/dashboard',
target: '_blank'
}]] satisfies NavigationMenuItem[][]
const groups = computed(() => [{
id: 'links',
label: 'Go to',
items: links.flat()
}, {
id: 'code',
label: 'Code',
items: [{
id: 'source',
label: 'View page source',
icon: 'i-simple-icons-github',
to: `https://github.com/nuxt-ui-templates/dashboard/blob/main/app/pages${route.path === '/' ? '/index' : route.path}.vue`,
target: '_blank'
}]
}])
onMounted(async () => {
const cookie = useCookie('cookie-consent')
if (cookie.value === 'accepted') {
return
}
toast.add({
title: 'We use first-party cookies to enhance your experience on our website.',
duration: 0,
close: false,
actions: [{
label: 'Accept',
color: 'neutral',
variant: 'outline',
onClick: () => {
cookie.value = 'accepted'
}
}, {
label: 'Opt out',
color: 'neutral',
variant: 'ghost'
}]
})
})
</script>
<template>
<UDashboardGroup unit="rem">
<UDashboardSidebar id="default" v-model:open="open" collapsible resizable class="bg-elevated/25"
:ui="{ footer: 'lg:border-t lg:border-default' }">
<template #header="{ collapsed }">
<TeamsMenu :collapsed="collapsed" />
</template>
<template #default="{ collapsed }">
<UDashboardSearchButton :collapsed="collapsed" class="bg-transparent ring-default" />
<UNavigationMenu :collapsed="collapsed" :items="links[0]" orientation="vertical" tooltip popover />
<UNavigationMenu :collapsed="collapsed" :items="links[1]" orientation="vertical" tooltip
class="mt-auto" />
</template>
<template #footer="{ collapsed }">
<UserMenu :collapsed="collapsed" />
</template>
</UDashboardSidebar>
<UDashboardSearch :groups="groups" />
<slot />
<NotificationsSlideover />
</UDashboardGroup>
</template>

View File

@@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware(() => {
// runs on every navigation
})

View File

@@ -0,0 +1,5 @@
export default defineNuxtRouteMiddleware(() => {
})

View File

@@ -0,0 +1,3 @@
export default defineNuxtRouteMiddleware(() => {
// runs on every navigation
})

View File

@@ -0,0 +1,324 @@
<script setup lang="ts">
import type { TableColumn } from '@nuxt/ui'
import { upperFirst } from 'scule'
import { getPaginationRowModel } from '@tanstack/table-core'
import type { Row } from '@tanstack/table-core'
import type { User } from '~/types'
const UAvatar = resolveComponent('UAvatar')
const UButton = resolveComponent('UButton')
const UBadge = resolveComponent('UBadge')
const UDropdownMenu = resolveComponent('UDropdownMenu')
const UCheckbox = resolveComponent('UCheckbox')
const toast = useToast()
const table = useTemplateRef('table')
const columnFilters = ref([{
id: 'email',
value: ''
}])
const columnVisibility = ref()
const rowSelection = ref({ 1: true })
const { data, status } = await useFetch<User[]>('/api/customers', {
lazy: true
})
function getRowItems(row: Row<User>) {
return [
{
type: 'label',
label: 'Actions'
},
{
label: 'View customer details',
icon: 'i-lucide-list'
},
{
type: 'separator'
},
{
label: 'Delete customer',
icon: 'i-lucide-trash',
color: 'error',
onSelect() {
toast.add({
title: 'Customer deleted',
description: 'The customer has been deleted.'
})
}
}
]
}
const columns: TableColumn<User>[] = [
{
id: 'select',
header: ({ table }) =>
h(UCheckbox, {
'modelValue': table.getIsSomePageRowsSelected()
? 'indeterminate'
: table.getIsAllPageRowsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') =>
table.toggleAllPageRowsSelected(!!value),
'ariaLabel': 'Select all'
}),
cell: ({ row }) =>
h(UCheckbox, {
'modelValue': row.getIsSelected(),
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
'ariaLabel': 'Select row'
})
},
{
accessorKey: 'id',
header: ({ column }) => {
const isSorted = column.getIsSorted()
return h(UButton, {
color: 'neutral',
variant: 'ghost',
label: 'ID',
icon: isSorted
? isSorted === 'asc'
? 'i-lucide-arrow-up-narrow-wide'
: 'i-lucide-arrow-down-wide-narrow'
: 'i-lucide-arrow-up-down',
class: '-mx-2.5',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
})
},
},
{
accessorKey: 'name',
header: ({ column }) => {
const isSorted = column.getIsSorted()
return h(UButton, {
color: 'neutral',
variant: 'ghost',
label: 'Name',
icon: isSorted
? isSorted === 'asc'
? 'i-lucide-arrow-up-narrow-wide'
: 'i-lucide-arrow-down-wide-narrow'
: 'i-lucide-arrow-up-down',
class: '-mx-2.5',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
})
},
cell: ({ row }) => {
return h('div', { class: 'flex items-center gap-3' }, [
h(UAvatar, {
...row.original.avatar,
size: 'lg'
}),
h('div', undefined, [
h('p', { class: 'font-medium text-highlighted' }, row.original.name),
h('p', { class: '' }, `@${row.original.name}`)
]),
])
}
},
{
accessorKey: 'email',
header: ({ column }) => {
const isSorted = column.getIsSorted()
return h(UButton, {
color: 'neutral',
variant: 'ghost',
label: 'Email',
icon: isSorted
? isSorted === 'asc'
? 'i-lucide-arrow-up-narrow-wide'
: 'i-lucide-arrow-down-wide-narrow'
: 'i-lucide-arrow-up-down',
class: '-mx-2.5',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
})
}
},
{
accessorKey: 'location',
header: ({ column }) => {
const isSorted = column.getIsSorted()
return h(UButton, {
color: 'neutral',
variant: 'ghost',
label: 'Location',
icon: isSorted
? isSorted === 'asc'
? 'i-lucide-arrow-up-narrow-wide'
: 'i-lucide-arrow-down-wide-narrow'
: 'i-lucide-arrow-up-down',
class: '-mx-2.5',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
})
},
cell: ({ row }) => row.original.location
},
{
accessorKey: 'status',
header: ({ column }) => {
const isSorted = column.getIsSorted()
return h(UButton, {
color: 'neutral',
variant: 'ghost',
label: 'Status',
icon: isSorted
? isSorted === 'asc'
? 'i-lucide-arrow-up-narrow-wide'
: 'i-lucide-arrow-down-wide-narrow'
: 'i-lucide-arrow-up-down',
class: '-mx-2.5',
onClick: () => column.toggleSorting(column.getIsSorted() === 'asc')
})
},
filterFn: 'equals',
cell: ({ row }) => {
const color = {
subscribed: 'success' as const,
unsubscribed: 'error' as const,
bounced: 'warning' as const
}[row.original.status]
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
row.original.status
)
}
},
{
id: 'actions',
cell: ({ row }) => {
return h(
'div',
{ class: 'text-right' },
h(
UDropdownMenu,
{
content: {
align: 'end'
},
items: getRowItems(row)
},
() =>
h(UButton, {
icon: 'i-lucide-ellipsis-vertical',
color: 'neutral',
variant: 'ghost',
class: 'ml-auto'
})
)
)
}
}
]
const statusFilter = ref('all')
watch(() => statusFilter.value, (newVal) => {
if (!table?.value?.tableApi) return
const statusColumn = table.value.tableApi.getColumn('status')
if (!statusColumn) return
if (newVal === 'all') {
statusColumn.setFilterValue(undefined)
} else {
statusColumn.setFilterValue(newVal)
}
})
const email = computed({
get: (): string => {
return (table.value?.tableApi?.getColumn('email')?.getFilterValue() as string) || ''
},
set: (value: string) => {
table.value?.tableApi?.getColumn('email')?.setFilterValue(value || undefined)
}
})
const pagination = ref({
pageIndex: 0,
pageSize: 10
})
</script>
<template>
<UDashboardPanel id="customers">
<template #header>
<UDashboardNavbar title="Customers">
<template #leading>
<UDashboardSidebarCollapse />
</template>
<template #right>
<CustomersAddModal />
</template>
</UDashboardNavbar>
</template>
<template #body>
<div class="flex flex-wrap items-center justify-between gap-1.5">
<UInput v-model="email" class="max-w-sm" icon="i-lucide-search" placeholder="Filter emails..." />
<div class="flex flex-wrap items-center gap-1.5">
<CustomersDeleteModal :count="table?.tableApi?.getFilteredSelectedRowModel().rows.length">
<UButton v-if="table?.tableApi?.getFilteredSelectedRowModel().rows.length" label="Delete"
color="error" variant="subtle" icon="i-lucide-trash">
<template #trailing>
<UKbd>
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length }}
</UKbd>
</template>
</UButton>
</CustomersDeleteModal>
<UDropdownMenu :items="table?.tableApi
?.getAllColumns()
.filter((column: any) => column.getCanHide())
.map((column: any) => ({
label: upperFirst(column.id),
type: 'checkbox' as const,
checked: column.getIsVisible(),
onUpdateChecked(checked: boolean) {
table?.tableApi?.getColumn(column.id)?.toggleVisibility(!!checked)
},
onSelect(e?: Event) {
e?.preventDefault()
}
}))
" :content="{ align: 'end' }">
<UButton label="Display" color="neutral" variant="outline"
trailing-icon="i-lucide-settings-2" />
</UDropdownMenu>
</div>
</div>
<UTable ref="table" v-model:column-filters="columnFilters" v-model:column-visibility="columnVisibility"
v-model:row-selection="rowSelection" v-model:pagination="pagination" :pagination-options="{
getPaginationRowModel: getPaginationRowModel()
}" class="shrink-0" :data="data" :columns="columns" :loading="status === 'pending'" :ui="{
base: 'table-fixed border-separate border-spacing-0',
thead: '[&>tr]:bg-elevated/50 [&>tr]:after:content-none',
tbody: '[&>tr]:last:[&>td]:border-b-0',
th: 'py-2 first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
td: 'border-b border-default',
separator: 'h-0'
}" />
<div class="flex items-center justify-between gap-3 border-t border-default pt-4 mt-auto">
<div class="text-sm text-muted">
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
</div>
<div class="flex items-center gap-1.5">
<UPagination :default-page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"
:items-per-page="table?.tableApi?.getState().pagination.pageSize"
:total="table?.tableApi?.getFilteredRowModel().rows.length"
@update:page="(p: number) => table?.tableApi?.setPageIndex(p - 1)" />
</div>
</div>
</template>
</UDashboardPanel>
</template>

View File

@@ -0,0 +1,47 @@
<script setup lang="ts">
import type { DropdownMenuItem } from '@nuxt/ui'
const { isNotificationsSlideoverOpen } = useDashboard()
const items = [[{
label: 'New mail',
icon: 'i-lucide-send',
to: '/inbox'
}, {
label: 'New customer',
icon: 'i-lucide-user-plus',
to: '/customers'
}]] satisfies DropdownMenuItem[][]
</script>
<template>
<UDashboardPanel id="home">
<template #header>
<UDashboardNavbar title="Home" :ui="{ right: 'gap-3' }">
<template #leading>
<UDashboardSidebarCollapse />
</template>
<template #right>
<UTooltip text="Notifications" :shortcuts="['N']">
<UButton color="neutral" variant="ghost" square @click="isNotificationsSlideoverOpen = true">
<UChip color="error" inset>
<UIcon name="i-lucide-bell" class="size-5 shrink-0" />
</UChip>
</UButton>
</UTooltip>
<UDropdownMenu :items="items">
<UButton icon="i-lucide-plus" size="md" class="rounded-full" />
</UDropdownMenu>
</template>
</UDashboardNavbar>
<UDashboardToolbar>
</UDashboardToolbar>
</template>
</UDashboardPanel>
</template>

60
NautilusDesk/app/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,60 @@
import type { AvatarProps } from '@nuxt/ui'
export type UserStatus = 'subscribed' | 'unsubscribed' | 'bounced'
export type SaleStatus = 'paid' | 'failed' | 'refunded'
export interface User {
id: number
name: string
email: string
avatar?: AvatarProps
status: UserStatus
location: string
}
export interface Mail {
id: number
unread?: boolean
from: User
subject: string
body: string
date: string
}
export interface Member {
name: string
username: string
role: 'member' | 'owner'
avatar: AvatarProps
}
export interface Stat {
title: string
icon: string
value: number | string
variation: number
formatter?: (value: number) => string
}
export interface Sale {
id: string
date: string
status: SaleStatus
email: string
amount: number
}
export interface Notification {
id: number
unread?: boolean
sender: User
body: string
date: string
}
export type Period = 'daily' | 'weekly' | 'monthly'
export interface Range {
start: Date
end: Date
}

View File

@@ -0,0 +1,29 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: {
enabled: true,
timeline: {
enabled: true
}
},
imports: {
presets: [
{
from: 'vue-i18n',
imports: ['useI18n'],
},
],
},
modules: ['@nuxtjs/i18n', '@pinia/nuxt', '@nuxt/ui'],
css: ['~/assets/css/main.css'],
i18n: {
locales: [
{ code: 'en', language: 'en-US' },
{ code: 'de', language: 'de-DE' }
],
defaultLocale: 'en',
}
})

16573
NautilusDesk/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
NautilusDesk/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "NautilusDesk",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/ui": "^4.3.0",
"@nuxtjs/i18n": "^10.2.1",
"@pinia/nuxt": "^0.11.3",
"date-fns": "^4.1.0",
"nuxt": "^4.2.2",
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-i18n": "^11.2.7",
"vue-router": "^4.6.4",
"zod": "^4.2.1"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow:

View File

@@ -0,0 +1,18 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"files": [],
"references": [
{
"path": "./.nuxt/tsconfig.app.json"
},
{
"path": "./.nuxt/tsconfig.server.json"
},
{
"path": "./.nuxt/tsconfig.shared.json"
},
{
"path": "./.nuxt/tsconfig.node.json"
}
]
}