This commit is contained in:
30
.gitea/workflows/deploy.yml
Normal file
30
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Build & Deploy Nuxt (Docker)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: [self-hosted, linux, docker]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Define image tag
|
||||||
|
run: |
|
||||||
|
echo "IMAGE=local/nautilusDesk:${{ gitea.sha }}" >> $GITEA_ENV
|
||||||
|
|
||||||
|
- name: Build image
|
||||||
|
run: |
|
||||||
|
docker build -t "$IMAGE" .
|
||||||
|
|
||||||
|
- name: Deploy via compose
|
||||||
|
run: |
|
||||||
|
cd deploy
|
||||||
|
IMAGE="$IMAGE" docker compose up -d --remove-orphans
|
||||||
|
|
||||||
|
- name: Cleanup dangling images (optional)
|
||||||
|
run: |
|
||||||
|
docker image prune -f || true
|
||||||
17
Dockerfile
Normal file
17
Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:20-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-alpine
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
COPY --from=build /app/.output ./.output
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", ".output/server/index.mjs"]
|
||||||
24
NautilusDesk/.gitignore
vendored
Normal file
24
NautilusDesk/.gitignore
vendored
Normal 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
75
NautilusDesk/README.md
Normal 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.
|
||||||
10
NautilusDesk/app/app.config.ts
Normal file
10
NautilusDesk/app/app.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
theme: {
|
||||||
|
primaryColor: '#fff',
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
neutral: 'zinc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
13
NautilusDesk/app/app.vue
Normal file
13
NautilusDesk/app/app.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<UApp>
|
||||||
|
<UMain>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</UMain>
|
||||||
|
</UApp>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
2
NautilusDesk/app/assets/css/main.css
Normal file
2
NautilusDesk/app/assets/css/main.css
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "@nuxt/ui";
|
||||||
28
NautilusDesk/app/components/Header.vue
Normal file
28
NautilusDesk/app/components/Header.vue
Normal 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>
|
||||||
5
NautilusDesk/app/components/Logo.vue
Normal file
5
NautilusDesk/app/components/Logo.vue
Normal 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>
|
||||||
45
NautilusDesk/app/components/Menus/TeamsMenu.vue
Normal file
45
NautilusDesk/app/components/Menus/TeamsMenu.vue
Normal 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>
|
||||||
87
NautilusDesk/app/components/Menus/UserMenu.vue
Normal file
87
NautilusDesk/app/components/Menus/UserMenu.vue
Normal 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>
|
||||||
34
NautilusDesk/app/components/NotificationsSlideover.vue
Normal file
34
NautilusDesk/app/components/NotificationsSlideover.vue
Normal 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>
|
||||||
44
NautilusDesk/app/components/customers/AddModal.vue
Normal file
44
NautilusDesk/app/components/customers/AddModal.vue
Normal 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>
|
||||||
28
NautilusDesk/app/components/customers/DeleteModal.vue
Normal file
28
NautilusDesk/app/components/customers/DeleteModal.vue
Normal 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>
|
||||||
3
NautilusDesk/app/composables/useAppHead.ts
Normal file
3
NautilusDesk/app/composables/useAppHead.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function useAppHead() {
|
||||||
|
useHead({ titleTemplate: 'NautilusDesk · Dive School' })
|
||||||
|
}
|
||||||
25
NautilusDesk/app/composables/useDashboard.ts
Normal file
25
NautilusDesk/app/composables/useDashboard.ts
Normal 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)
|
||||||
13
NautilusDesk/app/error.vue
Normal file
13
NautilusDesk/app/error.vue
Normal 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>
|
||||||
139
NautilusDesk/app/layouts/default.vue
Normal file
139
NautilusDesk/app/layouts/default.vue
Normal 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>
|
||||||
3
NautilusDesk/app/middleware/analytics.global.ts
Normal file
3
NautilusDesk/app/middleware/analytics.global.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
// runs on every navigation
|
||||||
|
})
|
||||||
5
NautilusDesk/app/middleware/auth.ts
Normal file
5
NautilusDesk/app/middleware/auth.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
3
NautilusDesk/app/middleware/setups.global.ts
Normal file
3
NautilusDesk/app/middleware/setups.global.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
// runs on every navigation
|
||||||
|
})
|
||||||
324
NautilusDesk/app/pages/Customers.vue
Normal file
324
NautilusDesk/app/pages/Customers.vue
Normal 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>
|
||||||
47
NautilusDesk/app/pages/index.vue
Normal file
47
NautilusDesk/app/pages/index.vue
Normal 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
60
NautilusDesk/app/types/index.d.ts
vendored
Normal 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
|
||||||
|
}
|
||||||
29
NautilusDesk/nuxt.config.ts
Normal file
29
NautilusDesk/nuxt.config.ts
Normal 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
16573
NautilusDesk/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
NautilusDesk/package.json
Normal file
24
NautilusDesk/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
NautilusDesk/public/favicon.ico
Normal file
BIN
NautilusDesk/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
BIN
NautilusDesk/public/icon.png
Normal file
BIN
NautilusDesk/public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
2
NautilusDesk/public/robots.txt
Normal file
2
NautilusDesk/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow:
|
||||||
18
NautilusDesk/tsconfig.json
Normal file
18
NautilusDesk/tsconfig.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
11
deploy/docker-compose.yml
Normal file
11
deploy/docker-compose.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
services:
|
||||||
|
nuxt-app:
|
||||||
|
image: ${IMAGE}
|
||||||
|
container_name: nautilusDesk
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
Reference in New Issue
Block a user