First
This commit is contained in:
commit
a1c2fac4b8
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Production license for @nuxt/ui-pro, get one at https://ui.nuxt.com/pro/purchase
|
||||||
|
NUXT_UI_PRO_LICENSE=
|
||||||
|
# Public URL, used for OG Image when running nuxt generate
|
||||||
|
NUXT_PUBLIC_SITE_URL=
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
# VSC
|
||||||
|
.history
|
||||||
63
README.md
Normal file
63
README.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Nuxt Dashboard Template
|
||||||
|
|
||||||
|
[](https://ui.nuxt.com/pro)
|
||||||
|
[](https://hub.nuxt.com/new?repo=nuxt-ui-pro/dashboard)
|
||||||
|
|
||||||
|
Get started with the Nuxt dashboard template with multiple pages, collapsible sidebar, keyboard shortcuts, light & dark more, command palette and more, powered by [Nuxt UI Pro](https://ui.nuxt.com/pro).
|
||||||
|
|
||||||
|
- [Live demo](https://dashboard-template.nuxt.dev/)
|
||||||
|
- [Documentation](https://ui.nuxt.com/getting-started/installation/pro/nuxt)
|
||||||
|
|
||||||
|
<a href="https://dashboard-template.nuxt.dev/" target="_blank">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2Rhc2hib2FyZC10ZW1wbGF0ZS5udXh0LmRldiIsImlhdCI6MTczOTQ2MzU2N30._VElt4uvLjvAMdnTLytCInOajMElzWDKbmvOaMZhZUI.jpg?theme=dark">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2Rhc2hib2FyZC10ZW1wbGF0ZS5udXh0LmRldiIsImlhdCI6MTczOTQ2MzU2N30._VElt4uvLjvAMdnTLytCInOajMElzWDKbmvOaMZhZUI.jpg?theme=light">
|
||||||
|
<img alt="Nuxt Dashboard Template" src="https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2Rhc2hib2FyZC10ZW1wbGF0ZS5udXh0LmRldiIsImlhdCI6MTczOTQ2MzU2N30._VElt4uvLjvAMdnTLytCInOajMElzWDKbmvOaMZhZUI.jpg">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Vue Dashboard Template
|
||||||
|
|
||||||
|
The dashboard template for Vue is on https://github.com/nuxt-ui-pro/dashboard-vue.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash [Terminal]
|
||||||
|
npx nuxi@latest init -t github:nuxt-ui-pro/dashboard
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
|
|
||||||
|
## Renovate integration
|
||||||
|
|
||||||
|
Install [Renovate GitHub app](https://github.com/apps/renovate/installations/select_target) on your repository and you are good to go.
|
||||||
8
app/app.config.ts
Normal file
8
app/app.config.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
primary: 'green',
|
||||||
|
neutral: 'zinc'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
42
app/app.vue
Normal file
42
app/app.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const color = computed(() => colorMode.value === 'dark' ? '#1b1718' : 'white')
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
meta: [
|
||||||
|
{ charset: 'utf-8' },
|
||||||
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
|
{ key: 'theme-color', name: 'theme-color', content: color }
|
||||||
|
],
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', href: '/favicon.ico' }
|
||||||
|
],
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: 'en'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const title = 'Nuxt Dashboard Template'
|
||||||
|
const description = 'A professional dashboard template built with Nuxt UI Pro, featuring multiple pages, data visualization, and comprehensive management capabilities for creating powerful admin interfaces.'
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
ogTitle: title,
|
||||||
|
ogDescription: description,
|
||||||
|
ogImage: 'https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2Rhc2hib2FyZC10ZW1wbGF0ZS5udXh0LmRldiIsImlhdCI6MTczOTQ2MzU2N30._VElt4uvLjvAMdnTLytCInOajMElzWDKbmvOaMZhZUI.jpg?theme=light',
|
||||||
|
twitterImage: 'https://assets.hub.nuxt.com/eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1cmwiOiJodHRwczovL2Rhc2hib2FyZC10ZW1wbGF0ZS5udXh0LmRldiIsImlhdCI6MTczOTQ2MzU2N30._VElt4uvLjvAMdnTLytCInOajMElzWDKbmvOaMZhZUI.jpg?theme=light',
|
||||||
|
twitterCard: 'summary_large_image'
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UApp>
|
||||||
|
<NuxtLoadingIndicator />
|
||||||
|
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
</UApp>
|
||||||
|
</template>
|
||||||
18
app/assets/css/main.css
Normal file
18
app/assets/css/main.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
@import "tailwindcss" theme(static);
|
||||||
|
@import "@nuxt/ui-pro";
|
||||||
|
|
||||||
|
@theme static {
|
||||||
|
--font-sans: 'Public Sans', sans-serif;
|
||||||
|
|
||||||
|
--color-green-50: #EFFDF5;
|
||||||
|
--color-green-100: #D9FBE8;
|
||||||
|
--color-green-200: #B3F5D1;
|
||||||
|
--color-green-300: #75EDAE;
|
||||||
|
--color-green-400: #00DC82;
|
||||||
|
--color-green-500: #00C16A;
|
||||||
|
--color-green-600: #00A155;
|
||||||
|
--color-green-700: #007F45;
|
||||||
|
--color-green-800: #016538;
|
||||||
|
--color-green-900: #0A5331;
|
||||||
|
--color-green-950: #052E16;
|
||||||
|
}
|
||||||
52
app/components/NotificationsSlideover.vue
Normal file
52
app/components/NotificationsSlideover.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<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>
|
||||||
68
app/components/TeamsMenu.vue
Normal file
68
app/components/TeamsMenu.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
collapsed?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const teams = ref([{
|
||||||
|
label: 'Nuxt',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/nuxt.png',
|
||||||
|
alt: 'Nuxt'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'NuxtHub',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/nuxt-hub.png',
|
||||||
|
alt: 'NuxtHub'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'NuxtLabs',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/nuxtlabs.png',
|
||||||
|
alt: 'NuxtLabs'
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
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>
|
||||||
184
app/components/UserMenu.vue
Normal file
184
app/components/UserMenu.vue
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
collapsed?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
const colors = ['red', 'orange', 'amber', 'yellow', 'lime', 'green', 'emerald', 'teal', 'cyan', 'sky', 'blue', 'indigo', 'violet', 'purple', 'fuchsia', 'pink', 'rose']
|
||||||
|
const neutrals = ['slate', 'gray', 'zinc', 'neutral', 'stone']
|
||||||
|
|
||||||
|
const user = ref({
|
||||||
|
name: 'Benjamin Canac',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://github.com/benjamincanac.png',
|
||||||
|
alt: 'Benjamin Canac'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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: 'Theme',
|
||||||
|
icon: 'i-lucide-palette',
|
||||||
|
children: [{
|
||||||
|
label: 'Primary',
|
||||||
|
slot: 'chip',
|
||||||
|
chip: appConfig.ui.colors.primary,
|
||||||
|
content: {
|
||||||
|
align: 'center',
|
||||||
|
collisionPadding: 16
|
||||||
|
},
|
||||||
|
children: colors.map(color => ({
|
||||||
|
label: color,
|
||||||
|
chip: color,
|
||||||
|
slot: 'chip',
|
||||||
|
checked: appConfig.ui.colors.primary === color,
|
||||||
|
type: 'checkbox',
|
||||||
|
onSelect: (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
appConfig.ui.colors.primary = color
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}, {
|
||||||
|
label: 'Neutral',
|
||||||
|
slot: 'chip',
|
||||||
|
chip: appConfig.ui.colors.neutral === 'neutral' ? 'old-neutral' : appConfig.ui.colors.neutral,
|
||||||
|
content: {
|
||||||
|
align: 'end',
|
||||||
|
collisionPadding: 16
|
||||||
|
},
|
||||||
|
children: neutrals.map(color => ({
|
||||||
|
label: color,
|
||||||
|
chip: color === 'neutral' ? 'old-neutral' : color,
|
||||||
|
slot: 'chip',
|
||||||
|
type: 'checkbox',
|
||||||
|
checked: appConfig.ui.colors.neutral === color,
|
||||||
|
onSelect: (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
appConfig.ui.colors.neutral = color
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
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: 'Templates',
|
||||||
|
icon: 'i-lucide-layout-template',
|
||||||
|
children: [{
|
||||||
|
label: 'Starter',
|
||||||
|
to: 'https://ui-pro-starter.nuxt.dev/'
|
||||||
|
}, {
|
||||||
|
label: 'Landing',
|
||||||
|
to: 'https://landing-template.nuxt.dev/'
|
||||||
|
}, {
|
||||||
|
label: 'Docs',
|
||||||
|
to: 'https://docs-template.nuxt.dev/'
|
||||||
|
}, {
|
||||||
|
label: 'SaaS',
|
||||||
|
to: 'https://saas-template.nuxt.dev/'
|
||||||
|
}, {
|
||||||
|
label: 'Dashboard',
|
||||||
|
to: 'https://dashboard-template.nuxt.dev/',
|
||||||
|
checked: true,
|
||||||
|
type: 'checkbox'
|
||||||
|
}, {
|
||||||
|
label: 'Chat',
|
||||||
|
to: 'https://chat-template.nuxt.dev/'
|
||||||
|
}]
|
||||||
|
}], [{
|
||||||
|
label: 'Documentation',
|
||||||
|
icon: 'i-lucide-book-open',
|
||||||
|
to: 'https://ui.nuxt.com/getting-started/installation/pro/nuxt',
|
||||||
|
target: '_blank'
|
||||||
|
}, {
|
||||||
|
label: 'GitHub repository',
|
||||||
|
icon: 'i-simple-icons-github',
|
||||||
|
to: 'https://github.com/nuxt-ui-pro/dashboard',
|
||||||
|
target: '_blank'
|
||||||
|
}, {
|
||||||
|
label: 'Upgrade to Pro',
|
||||||
|
icon: 'i-lucide-rocket',
|
||||||
|
to: 'https://ui.nuxt.com/pro/purchase',
|
||||||
|
target: '_blank'
|
||||||
|
}], [{
|
||||||
|
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 }">
|
||||||
|
<span
|
||||||
|
:style="{
|
||||||
|
'--chip-light': `var(--color-${(item as any).chip}-500)`,
|
||||||
|
'--chip-dark': `var(--color-${(item as any).chip}-400)`
|
||||||
|
}"
|
||||||
|
class="ms-0.5 size-2 rounded-full bg-(--chip-light) dark:bg-(--chip-dark)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</UDropdownMenu>
|
||||||
|
</template>
|
||||||
59
app/components/customers/AddModal.vue
Normal file
59
app/components/customers/AddModal.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<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>
|
||||||
42
app/components/customers/DeleteModal.vue
Normal file
42
app/components/customers/DeleteModal.vue
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<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>
|
||||||
123
app/components/home/HomeChart.client.vue
Normal file
123
app/components/home/HomeChart.client.vue
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { eachDayOfInterval, eachWeekOfInterval, eachMonthOfInterval, format } from 'date-fns'
|
||||||
|
import { VisXYContainer, VisLine, VisAxis, VisArea, VisCrosshair, VisTooltip } from '@unovis/vue'
|
||||||
|
import type { Period, Range } from '~/types'
|
||||||
|
|
||||||
|
const cardRef = useTemplateRef<HTMLElement | null>('cardRef')
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
period: Period
|
||||||
|
range: Range
|
||||||
|
}>()
|
||||||
|
|
||||||
|
type DataRecord = {
|
||||||
|
date: Date
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width } = useElementSize(cardRef)
|
||||||
|
|
||||||
|
// We use `useAsyncData` here to have same random data on the client and server
|
||||||
|
const { data } = await useAsyncData<DataRecord[]>(async () => {
|
||||||
|
const dates = ({
|
||||||
|
daily: eachDayOfInterval,
|
||||||
|
weekly: eachWeekOfInterval,
|
||||||
|
monthly: eachMonthOfInterval
|
||||||
|
} as Record<Period, typeof eachDayOfInterval>)[props.period](props.range)
|
||||||
|
|
||||||
|
const min = 1000
|
||||||
|
const max = 10000
|
||||||
|
|
||||||
|
return dates.map(date => ({ date, amount: Math.floor(Math.random() * (max - min + 1)) + min }))
|
||||||
|
}, {
|
||||||
|
watch: [() => props.period, () => props.range],
|
||||||
|
default: () => []
|
||||||
|
})
|
||||||
|
|
||||||
|
const x = (_: DataRecord, i: number) => i
|
||||||
|
const y = (d: DataRecord) => d.amount
|
||||||
|
|
||||||
|
const total = computed(() => data.value.reduce((acc: number, { amount }) => acc + amount, 0))
|
||||||
|
|
||||||
|
const formatNumber = new Intl.NumberFormat('en', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format
|
||||||
|
|
||||||
|
const formatDate = (date: Date): string => {
|
||||||
|
return ({
|
||||||
|
daily: format(date, 'd MMM'),
|
||||||
|
weekly: format(date, 'd MMM'),
|
||||||
|
monthly: format(date, 'MMM yyy')
|
||||||
|
})[props.period]
|
||||||
|
}
|
||||||
|
|
||||||
|
const xTicks = (i: number) => {
|
||||||
|
if (i === 0 || i === data.value.length - 1 || !data.value[i]) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return formatDate(data.value[i].date)
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = (d: DataRecord) => `${formatDate(d.date)}: ${formatNumber(d.amount)}`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UCard ref="cardRef" :ui="{ body: '!px-0 !pt-0 !pb-3' }">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-muted uppercase mb-1.5">
|
||||||
|
Revenue
|
||||||
|
</p>
|
||||||
|
<p class="text-3xl text-highlighted font-semibold">
|
||||||
|
{{ formatNumber(total) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<VisXYContainer
|
||||||
|
:data="data"
|
||||||
|
:padding="{ top: 40 }"
|
||||||
|
class="h-96"
|
||||||
|
:width="width"
|
||||||
|
>
|
||||||
|
<VisLine
|
||||||
|
:x="x"
|
||||||
|
:y="y"
|
||||||
|
color="var(--ui-primary)"
|
||||||
|
/>
|
||||||
|
<VisArea
|
||||||
|
:x="x"
|
||||||
|
:y="y"
|
||||||
|
color="var(--ui-primary)"
|
||||||
|
:opacity="0.1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisAxis
|
||||||
|
type="x"
|
||||||
|
:x="x"
|
||||||
|
:tick-format="xTicks"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisCrosshair
|
||||||
|
color="var(--ui-primary)"
|
||||||
|
:template="template"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<VisTooltip />
|
||||||
|
</VisXYContainer>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.unovis-xy-container {
|
||||||
|
--vis-crosshair-line-stroke-color: var(--ui-primary);
|
||||||
|
--vis-crosshair-circle-stroke-color: var(--ui-bg);
|
||||||
|
|
||||||
|
--vis-axis-grid-color: var(--ui-border);
|
||||||
|
--vis-axis-tick-color: var(--ui-border);
|
||||||
|
--vis-axis-tick-label-color: var(--ui-text-dimmed);
|
||||||
|
|
||||||
|
--vis-tooltip-background-color: var(--ui-bg);
|
||||||
|
--vis-tooltip-border-color: var(--ui-border);
|
||||||
|
--vis-tooltip-text-color: var(--ui-text-highlighted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
app/components/home/HomeChart.server.vue
Normal file
16
app/components/home/HomeChart.server.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<UCard :ui="{ body: '!px-0 !pt-0 !pb-3' }">
|
||||||
|
<template #header>
|
||||||
|
<div>
|
||||||
|
<p class="text-xs text-muted uppercase mb-1.5">
|
||||||
|
Revenue
|
||||||
|
</p>
|
||||||
|
<p class="text-3xl text-highlighted font-semibold">
|
||||||
|
---
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="h-96" />
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
132
app/components/home/HomeDateRangePicker.vue
Normal file
132
app/components/home/HomeDateRangePicker.vue
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DateFormatter, getLocalTimeZone, CalendarDate, today } from '@internationalized/date'
|
||||||
|
import type { Range } from '~/types'
|
||||||
|
|
||||||
|
const df = new DateFormatter('en-US', {
|
||||||
|
dateStyle: 'medium'
|
||||||
|
})
|
||||||
|
|
||||||
|
const selected = defineModel<Range>({ required: true })
|
||||||
|
|
||||||
|
const ranges = [
|
||||||
|
{ label: 'Last 7 days', days: 7 },
|
||||||
|
{ label: 'Last 14 days', days: 14 },
|
||||||
|
{ label: 'Last 30 days', days: 30 },
|
||||||
|
{ label: 'Last 3 months', months: 3 },
|
||||||
|
{ label: 'Last 6 months', months: 6 },
|
||||||
|
{ label: 'Last year', years: 1 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const toCalendarDate = (date: Date) => {
|
||||||
|
return new CalendarDate(
|
||||||
|
date.getFullYear(),
|
||||||
|
date.getMonth() + 1,
|
||||||
|
date.getDate()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const calendarRange = computed({
|
||||||
|
get: () => ({
|
||||||
|
start: selected.value.start ? toCalendarDate(selected.value.start) : undefined,
|
||||||
|
end: selected.value.end ? toCalendarDate(selected.value.end) : undefined
|
||||||
|
}),
|
||||||
|
set: (newValue: { start: CalendarDate | null, end: CalendarDate | null }) => {
|
||||||
|
selected.value = {
|
||||||
|
start: newValue.start ? newValue.start.toDate(getLocalTimeZone()) : new Date(),
|
||||||
|
end: newValue.end ? newValue.end.toDate(getLocalTimeZone()) : new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const isRangeSelected = (range: { days?: number, months?: number, years?: number }) => {
|
||||||
|
if (!selected.value.start || !selected.value.end) return false
|
||||||
|
|
||||||
|
const currentDate = today(getLocalTimeZone())
|
||||||
|
let startDate = currentDate.copy()
|
||||||
|
|
||||||
|
if (range.days) {
|
||||||
|
startDate = startDate.subtract({ days: range.days })
|
||||||
|
} else if (range.months) {
|
||||||
|
startDate = startDate.subtract({ months: range.months })
|
||||||
|
} else if (range.years) {
|
||||||
|
startDate = startDate.subtract({ years: range.years })
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedStart = toCalendarDate(selected.value.start)
|
||||||
|
const selectedEnd = toCalendarDate(selected.value.end)
|
||||||
|
|
||||||
|
return selectedStart.compare(startDate) === 0 && selectedEnd.compare(currentDate) === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectRange = (range: { days?: number, months?: number, years?: number }) => {
|
||||||
|
const endDate = today(getLocalTimeZone())
|
||||||
|
let startDate = endDate.copy()
|
||||||
|
|
||||||
|
if (range.days) {
|
||||||
|
startDate = startDate.subtract({ days: range.days })
|
||||||
|
} else if (range.months) {
|
||||||
|
startDate = startDate.subtract({ months: range.months })
|
||||||
|
} else if (range.years) {
|
||||||
|
startDate = startDate.subtract({ years: range.years })
|
||||||
|
}
|
||||||
|
|
||||||
|
selected.value = {
|
||||||
|
start: startDate.toDate(getLocalTimeZone()),
|
||||||
|
end: endDate.toDate(getLocalTimeZone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPopover :content="{ align: 'start' }" :modal="true">
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-lucide-calendar"
|
||||||
|
class="data-[state=open]:bg-elevated group"
|
||||||
|
>
|
||||||
|
<span class="truncate">
|
||||||
|
<template v-if="selected.start">
|
||||||
|
<template v-if="selected.end">
|
||||||
|
{{ df.format(selected.start) }} - {{ df.format(selected.end) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ df.format(selected.start) }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Pick a date
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<template #trailing>
|
||||||
|
<UIcon name="i-lucide-chevron-down" class="shrink-0 text-dimmed size-5 group-data-[state=open]:rotate-180 transition-transform duration-200" />
|
||||||
|
</template>
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-stretch sm:divide-x divide-default">
|
||||||
|
<div class="hidden sm:flex flex-col justify-center">
|
||||||
|
<UButton
|
||||||
|
v-for="(range, index) in ranges"
|
||||||
|
:key="index"
|
||||||
|
:label="range.label"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
class="rounded-none px-4"
|
||||||
|
:class="[isRangeSelected(range) ? 'bg-elevated' : 'hover:bg-elevated/50']"
|
||||||
|
truncate
|
||||||
|
@click="selectRange(range)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UCalendar
|
||||||
|
v-model="calendarRange"
|
||||||
|
class="p-2"
|
||||||
|
:number-of-months="2"
|
||||||
|
range
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UPopover>
|
||||||
|
</template>
|
||||||
49
app/components/home/HomePeriodSelect.vue
Normal file
49
app/components/home/HomePeriodSelect.vue
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { eachDayOfInterval } from 'date-fns'
|
||||||
|
import type { Period, Range } from '~/types'
|
||||||
|
|
||||||
|
const model = defineModel<Period>({ required: true })
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
range: Range
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const days = computed(() => eachDayOfInterval(props.range))
|
||||||
|
|
||||||
|
const periods = computed<Period[]>(() => {
|
||||||
|
if (days.value.length <= 8) {
|
||||||
|
return [
|
||||||
|
'daily'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (days.value.length <= 31) {
|
||||||
|
return [
|
||||||
|
'daily',
|
||||||
|
'weekly'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'weekly',
|
||||||
|
'monthly'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ensure the model value is always a valid period
|
||||||
|
watch(periods, () => {
|
||||||
|
if (!periods.value.includes(model.value)) {
|
||||||
|
model.value = periods.value[0]!
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<USelect
|
||||||
|
v-model="model"
|
||||||
|
:items="periods"
|
||||||
|
variant="ghost"
|
||||||
|
class="data-[state=open]:bg-elevated"
|
||||||
|
:ui="{ value: 'capitalize', itemLabel: 'capitalize', trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
112
app/components/home/HomeSales.vue
Normal file
112
app/components/home/HomeSales.vue
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { h, resolveComponent } from 'vue'
|
||||||
|
import type { TableColumn } from '@nuxt/ui'
|
||||||
|
import type { Period, Range, Sale } from '~/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
period: Period
|
||||||
|
range: Range
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const UBadge = resolveComponent('UBadge')
|
||||||
|
|
||||||
|
const sampleEmails = [
|
||||||
|
'james.anderson@example.com',
|
||||||
|
'mia.white@example.com',
|
||||||
|
'william.brown@example.com',
|
||||||
|
'emma.davis@example.com',
|
||||||
|
'ethan.harris@example.com'
|
||||||
|
]
|
||||||
|
|
||||||
|
const { data } = await useAsyncData('sales', async () => {
|
||||||
|
const sales: Sale[] = []
|
||||||
|
const currentDate = new Date()
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const hoursAgo = randomInt(0, 48)
|
||||||
|
const date = new Date(currentDate.getTime() - hoursAgo * 3600000)
|
||||||
|
|
||||||
|
sales.push({
|
||||||
|
id: (4600 - i).toString(),
|
||||||
|
date: date.toISOString(),
|
||||||
|
status: randomFrom(['paid', 'failed', 'refunded']),
|
||||||
|
email: randomFrom(sampleEmails),
|
||||||
|
amount: randomInt(100, 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return sales.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
|
}, {
|
||||||
|
watch: [() => props.period, () => props.range],
|
||||||
|
default: () => []
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns: TableColumn<Sale>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: 'ID',
|
||||||
|
cell: ({ row }) => `#${row.getValue('id')}`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'date',
|
||||||
|
header: 'Date',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return new Date(row.getValue('date')).toLocaleString('en-US', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const color = {
|
||||||
|
paid: 'success' as const,
|
||||||
|
failed: 'error' as const,
|
||||||
|
refunded: 'neutral' as const
|
||||||
|
}[row.getValue('status') as string]
|
||||||
|
|
||||||
|
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () =>
|
||||||
|
row.getValue('status')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'email',
|
||||||
|
header: 'Email'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'amount',
|
||||||
|
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const amount = Number.parseFloat(row.getValue('amount'))
|
||||||
|
|
||||||
|
const formatted = new Intl.NumberFormat('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'EUR'
|
||||||
|
}).format(amount)
|
||||||
|
|
||||||
|
return h('div', { class: 'text-right font-medium' }, formatted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTable
|
||||||
|
:data="data"
|
||||||
|
:columns="columns"
|
||||||
|
class="shrink-0"
|
||||||
|
: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: 'first:rounded-l-lg last:rounded-r-lg border-y border-default first:border-l last:border-r',
|
||||||
|
td: 'border-b border-default'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
98
app/components/home/HomeStats.vue
Normal file
98
app/components/home/HomeStats.vue
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Period, Range, Stat } from '~/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
period: Period
|
||||||
|
range: Range
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function formatCurrency(value: number): string {
|
||||||
|
return value.toLocaleString('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
maximumFractionDigits: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseStats = [{
|
||||||
|
title: 'Customers',
|
||||||
|
icon: 'i-lucide-users',
|
||||||
|
minValue: 400,
|
||||||
|
maxValue: 1000,
|
||||||
|
minVariation: -15,
|
||||||
|
maxVariation: 25
|
||||||
|
}, {
|
||||||
|
title: 'Conversions',
|
||||||
|
icon: 'i-lucide-chart-pie',
|
||||||
|
minValue: 1000,
|
||||||
|
maxValue: 2000,
|
||||||
|
minVariation: -10,
|
||||||
|
maxVariation: 20
|
||||||
|
}, {
|
||||||
|
title: 'Revenue',
|
||||||
|
icon: 'i-lucide-circle-dollar-sign',
|
||||||
|
minValue: 200000,
|
||||||
|
maxValue: 500000,
|
||||||
|
minVariation: -20,
|
||||||
|
maxVariation: 30,
|
||||||
|
formatter: formatCurrency
|
||||||
|
}, {
|
||||||
|
title: 'Orders',
|
||||||
|
icon: 'i-lucide-shopping-cart',
|
||||||
|
minValue: 100,
|
||||||
|
maxValue: 300,
|
||||||
|
minVariation: -5,
|
||||||
|
maxVariation: 15
|
||||||
|
}]
|
||||||
|
|
||||||
|
const { data: stats } = await useAsyncData<Stat[]>('stats', async () => {
|
||||||
|
return baseStats.map((stat) => {
|
||||||
|
const value = randomInt(stat.minValue, stat.maxValue)
|
||||||
|
const variation = randomInt(stat.minVariation, stat.maxVariation)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: stat.title,
|
||||||
|
icon: stat.icon,
|
||||||
|
value: stat.formatter ? stat.formatter(value) : value,
|
||||||
|
variation
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, {
|
||||||
|
watch: [() => props.period, () => props.range],
|
||||||
|
default: () => []
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPageGrid class="lg:grid-cols-4 gap-4 sm:gap-6 lg:gap-px">
|
||||||
|
<UPageCard
|
||||||
|
v-for="(stat, index) in stats"
|
||||||
|
:key="index"
|
||||||
|
:icon="stat.icon"
|
||||||
|
:title="stat.title"
|
||||||
|
to="/customers"
|
||||||
|
variant="subtle"
|
||||||
|
:ui="{
|
||||||
|
container: 'gap-y-1.5',
|
||||||
|
wrapper: 'items-start',
|
||||||
|
leading: 'p-2.5 rounded-full bg-primary/10 ring ring-inset ring-primary/25 flex-col',
|
||||||
|
title: 'font-normal text-muted text-xs uppercase'
|
||||||
|
}"
|
||||||
|
class="lg:rounded-none first:rounded-l-lg last:rounded-r-lg hover:z-1"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-2xl font-semibold text-highlighted">
|
||||||
|
{{ stat.value }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<UBadge
|
||||||
|
:color="stat.variation > 0 ? 'success' : 'error'"
|
||||||
|
variant="subtle"
|
||||||
|
class="text-xs"
|
||||||
|
>
|
||||||
|
{{ stat.variation > 0 ? '+' : '' }}{{ stat.variation }}%
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</UPageCard>
|
||||||
|
</UPageGrid>
|
||||||
|
</template>
|
||||||
78
app/components/inbox/InboxList.vue
Normal file
78
app/components/inbox/InboxList.vue
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { format, isToday } from 'date-fns'
|
||||||
|
import type { Mail } from '~/types'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
mails: Mail[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const mailsRefs = ref<Element[]>([])
|
||||||
|
|
||||||
|
const selectedMail = defineModel<Mail | null>()
|
||||||
|
|
||||||
|
watch(selectedMail, () => {
|
||||||
|
if (!selectedMail.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const ref = mailsRefs.value[selectedMail.value.id]
|
||||||
|
if (ref) {
|
||||||
|
ref.scrollIntoView({ block: 'nearest' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
defineShortcuts({
|
||||||
|
arrowdown: () => {
|
||||||
|
const index = props.mails.findIndex(mail => mail.id === selectedMail.value?.id)
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
selectedMail.value = props.mails[0]
|
||||||
|
} else if (index < props.mails.length - 1) {
|
||||||
|
selectedMail.value = props.mails[index + 1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
arrowup: () => {
|
||||||
|
const index = props.mails.findIndex(mail => mail.id === selectedMail.value?.id)
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
selectedMail.value = props.mails[props.mails.length - 1]
|
||||||
|
} else if (index > 0) {
|
||||||
|
selectedMail.value = props.mails[index - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="overflow-y-auto divide-y divide-default">
|
||||||
|
<div
|
||||||
|
v-for="(mail, index) in mails"
|
||||||
|
:key="index"
|
||||||
|
:ref="el => { mailsRefs[mail.id] = el as Element }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="p-4 sm:px-6 text-sm cursor-pointer border-l-2 transition-colors"
|
||||||
|
:class="[
|
||||||
|
mail.unread ? 'text-highlighted' : 'text-toned)',
|
||||||
|
selectedMail && selectedMail.id === mail.id ? 'border-primary bg-primary/10' : 'border-(--ui-bg) hover:border-primary hover:bg-primary/5'
|
||||||
|
]"
|
||||||
|
@click="selectedMail = mail"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between" :class="[mail.unread && 'font-semibold']">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{{ mail.from.name }}
|
||||||
|
|
||||||
|
<UChip v-if="mail.unread" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span>{{ isToday(new Date(mail.date)) ? format(new Date(mail.date), 'HH:mm') : format(new Date(mail.date), 'dd MMM') }}</span>
|
||||||
|
</div>
|
||||||
|
<p class="truncate" :class="[mail.unread && 'font-semibold']">
|
||||||
|
{{ mail.subject }}
|
||||||
|
</p>
|
||||||
|
<p class="text-dimmed line-clamp-1">
|
||||||
|
{{ mail.body }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
165
app/components/inbox/InboxMail.vue
Normal file
165
app/components/inbox/InboxMail.vue
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { format } from 'date-fns'
|
||||||
|
import type { Mail } from '~/types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
mail: Mail
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emits = defineEmits(['close'])
|
||||||
|
|
||||||
|
const dropdownItems = [[{
|
||||||
|
label: 'Mark as unread',
|
||||||
|
icon: 'i-lucide-check-circle'
|
||||||
|
}, {
|
||||||
|
label: 'Mark as important',
|
||||||
|
icon: 'i-lucide-triangle-alert'
|
||||||
|
}], [{
|
||||||
|
label: 'Star thread',
|
||||||
|
icon: 'i-lucide-star'
|
||||||
|
}, {
|
||||||
|
label: 'Mute thread',
|
||||||
|
icon: 'i-lucide-circle-pause'
|
||||||
|
}]]
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const reply = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
reply.value = ''
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: 'Email sent',
|
||||||
|
description: 'Your email has been sent successfully',
|
||||||
|
icon: 'i-lucide-check-circle',
|
||||||
|
color: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="inbox-2">
|
||||||
|
<UDashboardNavbar :title="mail.subject" :toggle="false">
|
||||||
|
<template #leading>
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-x"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
class="-ms-1.5"
|
||||||
|
@click="emits('close')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UTooltip text="Archive">
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-inbox"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
|
<UTooltip text="Reply">
|
||||||
|
<UButton icon="i-lucide-reply" color="neutral" variant="ghost" />
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
|
<UDropdownMenu :items="dropdownItems">
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-ellipsis-vertical"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</UDropdownMenu>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between gap-1 p-4 sm:px-6 border-b border-default">
|
||||||
|
<div class="flex items-start gap-4 sm:my-1.5">
|
||||||
|
<UAvatar
|
||||||
|
v-bind="mail.from.avatar"
|
||||||
|
:alt="mail.from.name"
|
||||||
|
size="3xl"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="font-semibold text-highlighted">
|
||||||
|
{{ mail.from.name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-muted">
|
||||||
|
{{ mail.from.email }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="max-sm:pl-16 text-muted text-sm sm:mt-2">
|
||||||
|
{{ format(new Date(mail.date), 'dd MMM HH:mm') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 p-4 sm:p-6 overflow-y-auto">
|
||||||
|
<p class="whitespace-pre-wrap">
|
||||||
|
{{ mail.body }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pb-4 px-4 sm:px-6 shrink-0">
|
||||||
|
<UCard variant="subtle" class="mt-auto" :ui="{ header: 'flex items-center gap-1.5 text-dimmed' }">
|
||||||
|
<template #header>
|
||||||
|
<UIcon name="i-lucide-reply" class="size-5" />
|
||||||
|
|
||||||
|
<span class="text-sm truncate">
|
||||||
|
Reply to {{ mail.from.name }} ({{ mail.from.email }})
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<form @submit.prevent="onSubmit">
|
||||||
|
<UTextarea
|
||||||
|
v-model="reply"
|
||||||
|
color="neutral"
|
||||||
|
variant="none"
|
||||||
|
required
|
||||||
|
autoresize
|
||||||
|
placeholder="Write your reply..."
|
||||||
|
:rows="4"
|
||||||
|
:disabled="loading"
|
||||||
|
class="w-full"
|
||||||
|
:ui="{ base: 'p-0 resize-none' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<UTooltip text="Attach file">
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
icon="i-lucide-paperclip"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-2">
|
||||||
|
<UButton
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
label="Save draft"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
color="neutral"
|
||||||
|
:loading="loading"
|
||||||
|
label="Send"
|
||||||
|
icon="i-lucide-send"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
60
app/components/settings/MembersList.vue
Normal file
60
app/components/settings/MembersList.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
import type { Member } from '~/types'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
members: Member[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const items = [{
|
||||||
|
label: 'Edit member',
|
||||||
|
onSelect: () => console.log('Edit member')
|
||||||
|
}, {
|
||||||
|
label: 'Remove member',
|
||||||
|
color: 'error' as const,
|
||||||
|
onSelect: () => console.log('Remove member')
|
||||||
|
}] satisfies DropdownMenuItem[]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul role="list" class="divide-y divide-default">
|
||||||
|
<li
|
||||||
|
v-for="(member, index) in members"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center justify-between gap-3 py-3 px-4 sm:px-6"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
|
<UAvatar
|
||||||
|
v-bind="member.avatar"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="text-sm min-w-0">
|
||||||
|
<p class="text-highlighted font-medium truncate">
|
||||||
|
{{ member.name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-muted truncate">
|
||||||
|
{{ member.username }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<USelect
|
||||||
|
:model-value="member.role"
|
||||||
|
:items="['member', 'owner']"
|
||||||
|
color="neutral"
|
||||||
|
:ui="{ value: 'capitalize', item: 'capitalize' }"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UDropdownMenu :items="items" :content="{ align: 'end' }">
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-ellipsis-vertical"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
</UDropdownMenu>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
25
app/composables/useDashboard.ts
Normal file
25
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)
|
||||||
24
app/error.vue
Normal file
24
app/error.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NuxtError } from '#app'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
error: NuxtError
|
||||||
|
}>()
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Page not found',
|
||||||
|
description: 'We are sorry but this page could not be found.'
|
||||||
|
})
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
htmlAttrs: {
|
||||||
|
lang: 'en'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UApp>
|
||||||
|
<UError :error="error" />
|
||||||
|
</UApp>
|
||||||
|
</template>
|
||||||
272
app/layouts/default.vue
Normal file
272
app/layouts/default.vue
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const open = ref(false)
|
||||||
|
|
||||||
|
const links = [[{
|
||||||
|
label: 'Accueil',
|
||||||
|
icon: 'i-lucide-house',
|
||||||
|
to: '/',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Connect',
|
||||||
|
icon: 'i-lucide-plug',
|
||||||
|
to: '/connect',
|
||||||
|
type: 'trigger',
|
||||||
|
children: [{
|
||||||
|
label: 'Clouds',
|
||||||
|
to: '/connect/clouds',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'SaaS',
|
||||||
|
to: '/connect/saas',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Écosystème',
|
||||||
|
to: '/connect/ecosystem',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Data Centres',
|
||||||
|
to: '/connect/data-centres',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Internet Exchanges',
|
||||||
|
to: '/connect/internet-exchanges',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
label: 'Services',
|
||||||
|
icon: 'i-lucide-server',
|
||||||
|
to: '/services',
|
||||||
|
type: 'trigger',
|
||||||
|
children: [{
|
||||||
|
label: 'Layer 2',
|
||||||
|
to: '/services/layer2',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Layer 3',
|
||||||
|
to: '/services/layer3',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Access Ports',
|
||||||
|
to: '/services/access-ports',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Internet On Demand',
|
||||||
|
to: '/services/internet-on-demand',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Mobilité',
|
||||||
|
to: '/services/mobility',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Marketplace',
|
||||||
|
to: '/services/marketplace',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'API',
|
||||||
|
to: '/services/api',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'DDoS Protection',
|
||||||
|
to: '/services/ddos',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Services Managés',
|
||||||
|
to: '/services/managed',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
label: 'Ressources',
|
||||||
|
icon: 'i-lucide-book',
|
||||||
|
to: '/resources',
|
||||||
|
type: 'trigger',
|
||||||
|
children: [{
|
||||||
|
label: 'Documentation API',
|
||||||
|
to: '/resources/api-docs',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Blog',
|
||||||
|
to: '/resources/blog',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
label: 'À Propos',
|
||||||
|
icon: 'i-lucide-info',
|
||||||
|
to: '/about',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}], [{
|
||||||
|
label: 'Paramètres',
|
||||||
|
to: '/settings',
|
||||||
|
icon: 'i-lucide-settings',
|
||||||
|
defaultOpen: true,
|
||||||
|
type: 'trigger',
|
||||||
|
children: [{
|
||||||
|
label: 'Général',
|
||||||
|
to: '/settings',
|
||||||
|
exact: true,
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Membres',
|
||||||
|
to: '/settings/members',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Notifications',
|
||||||
|
to: '/settings/notifications',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Sécurité',
|
||||||
|
to: '/settings/security',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
label: 'Connexion',
|
||||||
|
icon: 'i-lucide-log-in',
|
||||||
|
to: '/login',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
label: 'Inscription',
|
||||||
|
icon: 'i-lucide-user-plus',
|
||||||
|
to: '/signup',
|
||||||
|
onSelect: () => {
|
||||||
|
open.value = false
|
||||||
|
}
|
||||||
|
}]] 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-pro/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>
|
||||||
249
app/pages/about.vue
Normal file
249
app/pages/about.vue
Normal file
@ -0,0 +1,249 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'À Propos - Wibx Tour Layer 2',
|
||||||
|
description: 'Découvrez Wibx Tour, la plateforme de connectivité réseau Layer 2 nouvelle génération'
|
||||||
|
})
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ label: 'Data Centers', value: '150+', icon: 'i-lucide-building' },
|
||||||
|
{ label: 'Pays Couverts', value: '45+', icon: 'i-lucide-globe' },
|
||||||
|
{ label: 'Connexions Actives', value: '10K+', icon: 'i-lucide-link' },
|
||||||
|
{ label: 'Uptime', value: '99.99%', icon: 'i-lucide-shield-check' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const team = [
|
||||||
|
{
|
||||||
|
name: 'Marie Dubois',
|
||||||
|
role: 'CEO & Co-fondatrice',
|
||||||
|
bio: '15 ans d\'expérience dans les télécommunications et les réseaux d\'entreprise.',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b31ad8b6?w=150&h=150&fit=crop&crop=face'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Thomas Martin',
|
||||||
|
role: 'CTO & Co-fondateur',
|
||||||
|
bio: 'Expert en architecture réseau et développement d\'infrastructures cloud.',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Sophie Bernard',
|
||||||
|
role: 'VP Engineering',
|
||||||
|
bio: 'Spécialiste en automatisation réseau et APIs de nouvelle génération.',
|
||||||
|
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150&h=150&fit=crop&crop=face'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
{
|
||||||
|
title: 'Innovation',
|
||||||
|
description: 'Nous repoussons constamment les limites de la technologie réseau pour offrir des solutions avant-gardistes.',
|
||||||
|
icon: 'i-lucide-lightbulb'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Fiabilité',
|
||||||
|
description: 'Notre infrastructure robuste garantit une disponibilité maximale pour vos connexions critiques.',
|
||||||
|
icon: 'i-lucide-shield'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Simplicité',
|
||||||
|
description: 'Nous rendons simple ce qui est complexe, avec des interfaces intuitives et une automatisation poussée.',
|
||||||
|
icon: 'i-lucide-circle-check'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Partenariat',
|
||||||
|
description: 'Nous travaillons en étroite collaboration avec nos clients pour réussir ensemble.',
|
||||||
|
icon: 'i-lucide-handshake'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="about">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="À Propos">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton to="/signup" size="sm">
|
||||||
|
Nous Rejoindre
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
Wibx Tour - Connectivité Réseau Nouvelle Génération
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<UHero>
|
||||||
|
<template #title>
|
||||||
|
Révolutionner la <span class="text-primary">Connectivité</span> Réseau
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #description>
|
||||||
|
Wibx Tour est né de la vision de simplifier et d'automatiser la connectivité réseau Layer 2.
|
||||||
|
Nous aidons les entreprises à se connecter instantanément aux clouds, data centers et services
|
||||||
|
avec une expérience utilisateur exceptionnelle.
|
||||||
|
</template>
|
||||||
|
</UHero>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="stat in stats"
|
||||||
|
:key="stat.label"
|
||||||
|
class="text-center p-6 bg-gray-50 dark:bg-gray-900/50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon :name="stat.icon" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold text-primary mb-1">{{ stat.value }}</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">{{ stat.label }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Notre Mission -->
|
||||||
|
<div class="bg-gradient-to-r from-primary/5 to-blue-500/5 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">Notre Mission</h2>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-300 mb-6">
|
||||||
|
Démocratiser l'accès aux connexions réseau haut débit en rendant simple et abordable
|
||||||
|
ce qui était auparavant complexe et coûteux.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
Nous croyons que chaque entreprise, quelle que soit sa taille, devrait pouvoir bénéficier
|
||||||
|
d'une connectivité réseau de niveau entreprise. C'est pourquoi nous avons développé une
|
||||||
|
plateforme qui automatise l'ensemble du processus, de la commande au déploiement.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nos Valeurs -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">Nos Valeurs</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="value in values"
|
||||||
|
:key="value.title"
|
||||||
|
class="p-6 border border-gray-200 dark:border-gray-700 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<div class="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center mr-4">
|
||||||
|
<UIcon :name="value.icon" size="20" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold">{{ value.title }}</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">{{ value.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Équipe -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">Notre Équipe</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<UCard
|
||||||
|
v-for="member in team"
|
||||||
|
:key="member.name"
|
||||||
|
class="text-center"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<img
|
||||||
|
:src="member.avatar"
|
||||||
|
:alt="member.name"
|
||||||
|
class="w-24 h-24 rounded-full object-cover"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<h3 class="text-lg font-semibold mb-1">{{ member.name }}</h3>
|
||||||
|
<p class="text-primary font-medium mb-3">{{ member.role }}</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">{{ member.bio }}</p>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Histoire -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-6">Notre Histoire</h2>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center shrink-0">
|
||||||
|
<span class="text-primary font-bold">2020</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-2">Fondation</h4>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
Création de Wibx Tour par une équipe d'experts en télécommunications
|
||||||
|
frustrés par la complexité des solutions existantes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center shrink-0">
|
||||||
|
<span class="text-primary font-bold">2021</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-2">Première Plateforme</h4>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
Lancement de la première version de notre plateforme d'automatisation
|
||||||
|
des connexions Layer 2.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center shrink-0">
|
||||||
|
<span class="text-primary font-bold">2023</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-2">Expansion Internationale</h4>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
Extension de notre réseau à plus de 45 pays et 150 data centers
|
||||||
|
dans le monde entier.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center shrink-0">
|
||||||
|
<span class="text-primary font-bold">2024</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="font-semibold mb-2">Innovation Continue</h4>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400">
|
||||||
|
Lancement de nouvelles fonctionnalités incluant l'IA prédictive
|
||||||
|
et l'orchestration multi-cloud avancée.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Call to Action -->
|
||||||
|
<div class="bg-primary text-white rounded-lg p-8 text-center">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">Rejoignez-nous dans cette Aventure</h2>
|
||||||
|
<p class="text-lg mb-8 opacity-90">
|
||||||
|
Découvrez comment Wibx Tour peut transformer votre connectivité réseau
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<UButton to="/signup" size="lg" variant="outline" color="white">
|
||||||
|
Essayer Gratuitement
|
||||||
|
</UButton>
|
||||||
|
<UButton to="/services/layer2" size="lg" variant="ghost" color="white">
|
||||||
|
Nos Solutions
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
391
app/pages/connect/clouds.vue
Normal file
391
app/pages/connect/clouds.vue
Normal file
@ -0,0 +1,391 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Connect Clouds - Connexions Cloud Directes | Wibx Tour',
|
||||||
|
description: 'Connectez-vous directement aux plus grands fournisseurs cloud avec des liens Layer 2 privés et sécurisés'
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedProvider = ref('all')
|
||||||
|
|
||||||
|
const cloudProviders = [
|
||||||
|
{
|
||||||
|
id: 'aws',
|
||||||
|
name: 'Amazon Web Services',
|
||||||
|
logo: 'i-simple-icons-amazonaws',
|
||||||
|
status: 'Direct Connect',
|
||||||
|
statusColor: 'green',
|
||||||
|
regions: ['EU-West-1', 'EU-Central-1', 'US-East-1', 'US-West-2', 'AP-Southeast-1'],
|
||||||
|
services: ['EC2', 'S3', 'RDS', 'Lambda', 'VPC'],
|
||||||
|
bandwidth: '50 Mbps - 100 Gbps',
|
||||||
|
latency: '< 2ms',
|
||||||
|
price: 'À partir de €149/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'azure',
|
||||||
|
name: 'Microsoft Azure',
|
||||||
|
logo: 'i-simple-icons-microsoftazure',
|
||||||
|
status: 'Partner',
|
||||||
|
statusColor: 'blue',
|
||||||
|
regions: ['West Europe', 'North Europe', 'East US', 'West US 2'],
|
||||||
|
services: ['Virtual Machines', 'Storage', 'SQL Database', 'App Service'],
|
||||||
|
bandwidth: '50 Mbps - 10 Gbps',
|
||||||
|
latency: '< 3ms',
|
||||||
|
price: 'À partir de €129/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gcp',
|
||||||
|
name: 'Google Cloud Platform',
|
||||||
|
logo: 'i-simple-icons-googlecloud',
|
||||||
|
status: 'Direct Connect',
|
||||||
|
statusColor: 'green',
|
||||||
|
regions: ['europe-west1', 'europe-west3', 'us-central1', 'us-east1'],
|
||||||
|
services: ['Compute Engine', 'Cloud Storage', 'BigQuery', 'Kubernetes Engine'],
|
||||||
|
bandwidth: '50 Mbps - 100 Gbps',
|
||||||
|
latency: '< 2ms',
|
||||||
|
price: 'À partir de €159/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'oracle',
|
||||||
|
name: 'Oracle Cloud Infrastructure',
|
||||||
|
logo: 'i-simple-icons-oracle',
|
||||||
|
status: 'New',
|
||||||
|
statusColor: 'orange',
|
||||||
|
regions: ['eu-frankfurt-1', 'eu-amsterdam-1', 'us-phoenix-1'],
|
||||||
|
services: ['Compute', 'Object Storage', 'Database', 'Networking'],
|
||||||
|
bandwidth: '1 Gbps - 10 Gbps',
|
||||||
|
latency: '< 5ms',
|
||||||
|
price: 'À partir de €99/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ibm',
|
||||||
|
name: 'IBM Cloud',
|
||||||
|
logo: 'i-simple-icons-ibm',
|
||||||
|
status: 'Partner',
|
||||||
|
statusColor: 'blue',
|
||||||
|
regions: ['eu-de', 'eu-gb', 'us-south', 'us-east'],
|
||||||
|
services: ['Virtual Servers', 'Object Storage', 'Watson', 'Kubernetes'],
|
||||||
|
bandwidth: '100 Mbps - 1 Gbps',
|
||||||
|
latency: '< 4ms',
|
||||||
|
price: 'À partir de €89/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'alibaba',
|
||||||
|
name: 'Alibaba Cloud',
|
||||||
|
logo: 'i-simple-icons-alibabacloud',
|
||||||
|
status: 'New',
|
||||||
|
statusColor: 'orange',
|
||||||
|
regions: ['eu-central-1', 'eu-west-1', 'us-west-1', 'ap-southeast-1'],
|
||||||
|
services: ['ECS', 'OSS', 'RDS', 'Container Service'],
|
||||||
|
bandwidth: '100 Mbps - 1 Gbps',
|
||||||
|
latency: '< 6ms',
|
||||||
|
price: 'À partir de €79/mois'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ value: 'all', label: 'Tous les Clouds' },
|
||||||
|
{ value: 'aws', label: 'AWS' },
|
||||||
|
{ value: 'azure', label: 'Azure' },
|
||||||
|
{ value: 'gcp', label: 'Google Cloud' },
|
||||||
|
{ value: 'oracle', label: 'Oracle' },
|
||||||
|
{ value: 'ibm', label: 'IBM' },
|
||||||
|
{ value: 'alibaba', label: 'Alibaba' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredProviders = computed(() => {
|
||||||
|
if (selectedProvider.value === 'all') {
|
||||||
|
return cloudProviders
|
||||||
|
}
|
||||||
|
return cloudProviders.filter(provider => provider.id === selectedProvider.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getStatusVariant = (color: string) => {
|
||||||
|
switch (color) {
|
||||||
|
case 'green': return 'solid'
|
||||||
|
case 'orange': return 'outline'
|
||||||
|
case 'blue': return 'subtle'
|
||||||
|
default: return 'subtle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="connect-clouds">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Connect Clouds">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton size="sm" class="mr-2">
|
||||||
|
Demander un Devis
|
||||||
|
</UButton>
|
||||||
|
<UButton to="/services/layer2" variant="outline" size="sm">
|
||||||
|
En Savoir Plus
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
Connexions Cloud Directes - Layer 2 Privés et Sécurisés
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="bg-primary/10 rounded-lg p-8 text-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-4">
|
||||||
|
Connexions <span class="text-primary">Cloud</span> Directes
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||||
|
Connectez-vous aux plus grands fournisseurs cloud avec des liens Layer 2 privés, sécurisés et performants
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiques -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 bg-primary/20 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<UIcon name="i-lucide-cloud" class="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-3xl font-bold text-primary mb-1">
|
||||||
|
{{ cloudProviders.length }}+
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Fournisseurs cloud
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 bg-primary/20 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<UIcon name="i-lucide-globe" class="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-3xl font-bold text-primary mb-1">
|
||||||
|
50+
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Régions mondiales
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 bg-primary/20 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<UIcon name="i-lucide-zap" class="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-3xl font-bold text-primary mb-1">
|
||||||
|
< 2ms
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Latence ultra-faible
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center space-y-4">
|
||||||
|
<div class="w-16 h-16 bg-primary/20 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<UIcon name="i-lucide-shield-check" class="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-3xl font-bold text-primary mb-1">
|
||||||
|
100%
|
||||||
|
</div>
|
||||||
|
<div class="text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||||
|
Connexions privées
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div>
|
||||||
|
<UTabs v-model="selectedProvider" :items="tabs" class="w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cloud Providers Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="provider in filteredProviders"
|
||||||
|
:key="provider.id"
|
||||||
|
class="hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<UIcon :name="provider.logo" size="48" />
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold">
|
||||||
|
{{ provider.name }}
|
||||||
|
</h3>
|
||||||
|
<UBadge
|
||||||
|
:variant="getStatusVariant(provider.statusColor)"
|
||||||
|
:color="provider.statusColor"
|
||||||
|
size="sm"
|
||||||
|
class="mt-1"
|
||||||
|
>
|
||||||
|
{{ provider.status }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Régions -->
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
Régions Disponibles
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<UBadge
|
||||||
|
v-for="region in provider.regions"
|
||||||
|
:key="region"
|
||||||
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ region }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Services -->
|
||||||
|
<div>
|
||||||
|
<h4 class="font-medium text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||||
|
Services Principaux
|
||||||
|
</h4>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<UBadge
|
||||||
|
v-for="service in provider.services"
|
||||||
|
:key="service"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ service }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Spécifications -->
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">Bande Passante:</span>
|
||||||
|
<p class="font-medium">
|
||||||
|
{{ provider.bandwidth }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-600 dark:text-gray-400">Latence:</span>
|
||||||
|
<p class="font-medium">
|
||||||
|
{{ provider.latency }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prix -->
|
||||||
|
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<p class="text-lg font-semibold text-primary">
|
||||||
|
{{ provider.price }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton block>
|
||||||
|
Configurer
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="outline" square>
|
||||||
|
<UIcon name="i-lucide-info" />
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avantages -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">
|
||||||
|
Pourquoi Choisir Nos Connexions Cloud ?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-globe" size="24" class="text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Couverture Mondiale
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Accédez à 50+ régions cloud depuis nos data centers
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-dollar-sign" size="24" class="text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Réduction des Coûts
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Jusqu'à 70% d'économies sur les frais de transfert
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-purple-100 dark:bg-purple-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-gauge" size="24" class="text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Performances Optimisées
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Latence réduite et bande passante garantie
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-orange-100 dark:bg-orange-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-shield-check" size="24" class="text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Sécurité Renforcée
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Connexions privées isolées d'Internet
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Call to Action -->
|
||||||
|
<div class="text-center bg-primary text-white rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">
|
||||||
|
Prêt à Connecter Votre Cloud ?
|
||||||
|
</h2>
|
||||||
|
<p class="text-lg mb-8 opacity-90">
|
||||||
|
Configurez votre première connexion cloud en quelques minutes
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<UButton size="lg" variant="outline" color="white">
|
||||||
|
Commencer Maintenant
|
||||||
|
</UButton>
|
||||||
|
<UButton to="/resources/api-docs" size="lg" variant="ghost" color="white">
|
||||||
|
Documentation API
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
488
app/pages/connect/data-centres.vue
Normal file
488
app/pages/connect/data-centres.vue
Normal file
@ -0,0 +1,488 @@
|
|||||||
|
<template>
|
||||||
|
<UDashboardPanel id="data-centres">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Data Centres">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton to="/connect/clouds" size="sm">
|
||||||
|
Connecter Cloud
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
{{ dataCentres.length }} centres mondiaux
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UBadge variant="subtle" class="text-xs">
|
||||||
|
{{ dataCentres.filter(dc => dc.status === 'active').length }} actifs
|
||||||
|
</UBadge>
|
||||||
|
<UBadge variant="subtle" class="text-xs">
|
||||||
|
99.9% uptime
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-4">
|
||||||
|
Réseau Global de Data Centres
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||||
|
Connectez-vous à notre infrastructure mondiale avec plus de {{ dataCentres.length }} data centres dans {{ regions.length }} régions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiques -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-server" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
{{ dataCentres.length }}+
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Data Centres
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-globe" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
{{ regions.length }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Régions
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-shield-check" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
99.9%
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Uptime SLA
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-leaf" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
100%
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Énergie verte
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carte Interactive -->
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
Carte Interactive des Data Centres
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<!-- Filtres -->
|
||||||
|
<div>
|
||||||
|
<UTabs v-model="selectedRegion" :items="tabs" class="w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Carte Leaflet -->
|
||||||
|
<div id="map" class="h-[500px] w-full rounded-lg" />
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<!-- Liste des Data Centres -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8">
|
||||||
|
Nos Data Centres
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="dc in filteredDataCentres"
|
||||||
|
:key="dc.id"
|
||||||
|
class="hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
{{ dc.name }}
|
||||||
|
</h3>
|
||||||
|
<UBadge
|
||||||
|
:variant="dc.status === 'active' ? 'solid' : 'outline'"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{{ dc.status === 'active' ? 'Actif' : 'En construction' }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-map-pin" class="w-4 h-4" />
|
||||||
|
{{ dc.city }}, {{ dc.country }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-building" class="w-4 h-4" />
|
||||||
|
{{ dc.floors }} étages, {{ dc.racks }} racks
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-zap" class="w-4 h-4" />
|
||||||
|
{{ dc.power }} MW de puissance
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-users" class="w-4 h-4" />
|
||||||
|
{{ dc.carriers }}+ opérateurs
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-1 pt-2">
|
||||||
|
<UBadge
|
||||||
|
v-for="certification in dc.certifications"
|
||||||
|
:key="certification"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{{ certification }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton variant="outline" block>
|
||||||
|
Demander un devis
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avantages -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">
|
||||||
|
Pourquoi choisir nos Data Centres ?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-shield" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Sécurité
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Sécurité physique 24/7, biométrie, vidéosurveillance
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-activity" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Fiabilité
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
99.9% de disponibilité, alimentation redondante
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-network" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Connectivité
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Accès à 500+ opérateurs, peering direct
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-leaf" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Éco-responsable
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
100% énergie renouvelable, PUE < 1.3
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Imports côté client uniquement
|
||||||
|
let L: any = null
|
||||||
|
if (process.client) {
|
||||||
|
L = await import('leaflet')
|
||||||
|
await import('leaflet/dist/leaflet.css')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Métadonnées de la page
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Data Centres - Wibx Tour Layer 2',
|
||||||
|
description: 'Découvrez notre réseau mondial de data centres sécurisés et connectés. Plus de 50 centres dans 25 régions avec 99.9% de disponibilité.'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Données des data centres
|
||||||
|
const dataCentres = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Paris-Aubervilliers',
|
||||||
|
city: 'Paris',
|
||||||
|
country: 'France',
|
||||||
|
region: 'Europe',
|
||||||
|
lat: 48.8566,
|
||||||
|
lng: 2.3522,
|
||||||
|
status: 'active',
|
||||||
|
floors: 4,
|
||||||
|
racks: 2000,
|
||||||
|
power: 12,
|
||||||
|
certifications: ['ISO 27001', 'SOC 2', 'Tier III'],
|
||||||
|
carriers: 150,
|
||||||
|
latency: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'London-Docklands',
|
||||||
|
city: 'Londres',
|
||||||
|
country: 'Royaume-Uni',
|
||||||
|
region: 'Europe',
|
||||||
|
lat: 51.5074,
|
||||||
|
lng: -0.1278,
|
||||||
|
status: 'active',
|
||||||
|
floors: 6,
|
||||||
|
racks: 3000,
|
||||||
|
power: 18,
|
||||||
|
certifications: ['ISO 27001', 'SOC 2', 'Tier IV'],
|
||||||
|
carriers: 200,
|
||||||
|
latency: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Frankfurt-Main',
|
||||||
|
city: 'Francfort',
|
||||||
|
country: 'Allemagne',
|
||||||
|
region: 'Europe',
|
||||||
|
lat: 50.1109,
|
||||||
|
lng: 8.6821,
|
||||||
|
status: 'active',
|
||||||
|
floors: 5,
|
||||||
|
racks: 2500,
|
||||||
|
power: 15,
|
||||||
|
certifications: ['ISO 27001', 'SOC 2', 'Tier III'],
|
||||||
|
carriers: 180,
|
||||||
|
latency: 1.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'New York-Manhattan',
|
||||||
|
city: 'New York',
|
||||||
|
country: 'États-Unis',
|
||||||
|
region: 'Amérique du Nord',
|
||||||
|
lat: 40.7128,
|
||||||
|
lng: -74.0060,
|
||||||
|
status: 'active',
|
||||||
|
floors: 8,
|
||||||
|
racks: 4000,
|
||||||
|
power: 25,
|
||||||
|
certifications: ['ISO 27001', 'SOC 2', 'Tier IV'],
|
||||||
|
carriers: 300,
|
||||||
|
latency: 0.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Tokyo-Shibuya',
|
||||||
|
city: 'Tokyo',
|
||||||
|
country: 'Japon',
|
||||||
|
region: 'Asie-Pacifique',
|
||||||
|
lat: 35.6762,
|
||||||
|
lng: 139.6503,
|
||||||
|
status: 'active',
|
||||||
|
floors: 10,
|
||||||
|
racks: 3500,
|
||||||
|
power: 22,
|
||||||
|
certifications: ['ISO 27001', 'SOC 2', 'Tier IV'],
|
||||||
|
carriers: 250,
|
||||||
|
latency: 0.8
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Filtres
|
||||||
|
const selectedRegion = ref('all')
|
||||||
|
const regions = computed(() => [...new Set(dataCentres.value.map(dc => dc.region))])
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{ value: 'all', label: 'Toutes les régions' },
|
||||||
|
...regions.value.map(region => ({ value: region, label: region }))
|
||||||
|
])
|
||||||
|
|
||||||
|
const filteredDataCentres = computed(() => {
|
||||||
|
if (selectedRegion.value === 'all') {
|
||||||
|
return dataCentres.value
|
||||||
|
}
|
||||||
|
return dataCentres.value.filter(dc => dc.region === selectedRegion.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialisation de la carte
|
||||||
|
let map: L.Map | null = null
|
||||||
|
let markers: L.Marker[] = []
|
||||||
|
let currentTileLayer: L.TileLayer | null = null
|
||||||
|
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const initMap = async () => {
|
||||||
|
if (!process.client || !L) return
|
||||||
|
|
||||||
|
// Charger Leaflet si pas encore fait
|
||||||
|
if (!L.map) {
|
||||||
|
const leafletModule = await import('leaflet')
|
||||||
|
L = leafletModule.default
|
||||||
|
await import('leaflet/dist/leaflet.css')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corriger les icônes Leaflet par défaut
|
||||||
|
delete (L.Icon.Default.prototype as any)._getIconUrl
|
||||||
|
L.Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png',
|
||||||
|
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialiser la carte
|
||||||
|
map = L.map('map', {
|
||||||
|
center: [20, 0],
|
||||||
|
zoom: 2
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ajouter les tuiles selon le mode
|
||||||
|
updateTileLayer()
|
||||||
|
|
||||||
|
// Ajouter les marqueurs
|
||||||
|
updateMarkers()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTileLayer = () => {
|
||||||
|
if (!map || !L || !process.client) return
|
||||||
|
|
||||||
|
// Supprimer l'ancienne couche de tuiles
|
||||||
|
if (currentTileLayer) {
|
||||||
|
map.removeLayer(currentTileLayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter la nouvelle couche selon le mode
|
||||||
|
if (colorMode.value === 'dark') {
|
||||||
|
// Mode sombre - utiliser CartoDB Dark Matter
|
||||||
|
currentTileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||||
|
attribution: '© OpenStreetMap © CartoDB',
|
||||||
|
subdomains: 'abcd',
|
||||||
|
maxZoom: 19
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Mode clair - utiliser OpenStreetMap
|
||||||
|
currentTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
maxZoom: 19
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTileLayer.addTo(map)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMarkers = () => {
|
||||||
|
if (!map || !L || !process.client) return
|
||||||
|
|
||||||
|
// Supprimer les anciens marqueurs
|
||||||
|
markers.forEach(marker => map?.removeLayer(marker))
|
||||||
|
markers = []
|
||||||
|
|
||||||
|
// Ajouter les nouveaux marqueurs
|
||||||
|
filteredDataCentres.value.forEach(dc => {
|
||||||
|
const marker = L.marker([dc.lat, dc.lng])
|
||||||
|
.bindPopup(`
|
||||||
|
<div class="p-2">
|
||||||
|
<h3 class="font-bold text-sm">${dc.name}</h3>
|
||||||
|
<p class="text-xs text-gray-600">${dc.city}, ${dc.country}</p>
|
||||||
|
<p class="text-xs mt-1">
|
||||||
|
<span class="inline-block w-2 h-2 rounded-full ${dc.status === 'active' ? 'bg-green-500' : 'bg-orange-500'} mr-1"></span>
|
||||||
|
${dc.status === 'active' ? 'Actif' : 'En construction'}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mt-1">${dc.carriers}+ opérateurs</p>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
|
||||||
|
if (map) {
|
||||||
|
marker.addTo(map)
|
||||||
|
markers.push(marker)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mise à jour de la carte quand les filtres changent
|
||||||
|
watch(filteredDataCentres, updateMarkers, { deep: true })
|
||||||
|
|
||||||
|
// Mise à jour des tuiles quand le mode dark change
|
||||||
|
watch(() => colorMode.value, updateTileLayer)
|
||||||
|
|
||||||
|
// Initialisation côté client
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
initMap()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Nettoyage
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (map) {
|
||||||
|
map.remove()
|
||||||
|
map = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Fix pour les icônes Leaflet */
|
||||||
|
.leaflet-default-icon-path {
|
||||||
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbXNfN1+3bBZFvbOkg9vJLJsJU6vKKUHVQmCQNkzQyGpHJfVgZcwKjGhgqCGHYRiRPUHBDOtklmZmcpjPXJBL8/PdFqZBHxhASNQAyBfOaOWIhAMCCGHaB0hAKIRBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5AYhBSCAABYBBEAKARAGCYhBBbGGQBkKCCGg5A');
|
||||||
|
}
|
||||||
|
</style>
|
||||||
477
app/pages/connect/ecosystem.vue
Normal file
477
app/pages/connect/ecosystem.vue
Normal file
@ -0,0 +1,477 @@
|
|||||||
|
<template>
|
||||||
|
<UDashboardPanel id="ecosystem">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Écosystème">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton to="/connect/saas" size="sm">
|
||||||
|
SaaS
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
{{ partners.length }} partenaires
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UBadge variant="subtle" class="text-xs">
|
||||||
|
{{ certifiedPartners.length }} certifiés
|
||||||
|
</UBadge>
|
||||||
|
<UBadge variant="subtle" class="text-xs">
|
||||||
|
API ouverte
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-4">
|
||||||
|
Écosystème de Partenaires
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||||
|
Connectez-vous à un écosystème riche de {{ partners.length }} partenaires technologiques et fournisseurs de services
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiques -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-users" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
{{ partners.length }}+
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Partenaires
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-award" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
{{ certifiedPartners.length }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Certifiés
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-layers" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
{{ categories.length }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Catégories
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-code" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
API
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Ouverte
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Partenaires Stratégiques -->
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
Partenaires Stratégiques
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="partner in strategicPartners"
|
||||||
|
:key="partner.id"
|
||||||
|
class="flex flex-col items-center p-4 border rounded-lg hover:shadow-md transition-shadow cursor-pointer"
|
||||||
|
>
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-lg flex items-center justify-center mb-2">
|
||||||
|
<UIcon :name="partner.icon" class="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold text-sm text-center">
|
||||||
|
{{ partner.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-300 text-center mt-1">
|
||||||
|
{{ partner.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<!-- Filtres -->
|
||||||
|
<div>
|
||||||
|
<UTabs v-model="selectedCategory" :items="tabs" class="w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Partenaires -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8">
|
||||||
|
Nos partenaires technologiques
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="partner in filteredPartners"
|
||||||
|
:key="partner.id"
|
||||||
|
class="hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||||
|
<UIcon :name="partner.icon" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
{{ partner.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-300">
|
||||||
|
{{ partner.category }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UBadge
|
||||||
|
v-if="partner.certified"
|
||||||
|
variant="solid"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Certifié
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
{{ partner.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-map-pin" class="w-4 h-4" />
|
||||||
|
{{ partner.location }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-users" class="w-4 h-4" />
|
||||||
|
{{ partner.clients }}+ clients
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-1 pt-2">
|
||||||
|
<UBadge
|
||||||
|
v-for="service in partner.services"
|
||||||
|
:key="service"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{{ service }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-2 border-t">
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
Intégration
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold text-primary">
|
||||||
|
{{ partner.integration }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton variant="outline" block>
|
||||||
|
Se connecter
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Programme Partenaires -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">
|
||||||
|
Programme Partenaires
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div class="text-center p-6 border rounded-lg">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-handshake" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2 text-lg">
|
||||||
|
Partenaire Technologique
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Intégrez vos solutions avec notre plateforme
|
||||||
|
</p>
|
||||||
|
<ul class="text-sm text-gray-600 dark:text-gray-300 space-y-1 mb-4">
|
||||||
|
<li>• API complète</li>
|
||||||
|
<li>• Documentation technique</li>
|
||||||
|
<li>• Support dédié</li>
|
||||||
|
<li>• Co-marketing</li>
|
||||||
|
</ul>
|
||||||
|
<UButton variant="outline" size="sm">
|
||||||
|
Devenir partenaire
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-6 border rounded-lg">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-award" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2 text-lg">
|
||||||
|
Partenaire Certifié
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Certification officielle et avantages premium
|
||||||
|
</p>
|
||||||
|
<ul class="text-sm text-gray-600 dark:text-gray-300 space-y-1 mb-4">
|
||||||
|
<li>• Formation certifiante</li>
|
||||||
|
<li>• Support prioritaire</li>
|
||||||
|
<li>• Leads qualifiés</li>
|
||||||
|
<li>• Badge officiel</li>
|
||||||
|
</ul>
|
||||||
|
<UButton variant="solid" size="sm">
|
||||||
|
Se certifier
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-6 border rounded-lg">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-star" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2 text-lg">
|
||||||
|
Partenaire Premier
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Niveau d'excellence et d'engagement maximum
|
||||||
|
</p>
|
||||||
|
<ul class="text-sm text-gray-600 dark:text-gray-300 space-y-1 mb-4">
|
||||||
|
<li>• Roadmap partagée</li>
|
||||||
|
<li>• Co-innovation</li>
|
||||||
|
<li>• Événements exclusifs</li>
|
||||||
|
<li>• Revenue share</li>
|
||||||
|
</ul>
|
||||||
|
<UButton variant="solid" size="sm">
|
||||||
|
Programme Premier
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API & Développeurs -->
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
API & Développeurs
|
||||||
|
</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold mb-3">
|
||||||
|
API RESTful
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Intégrez facilement nos services dans vos applications avec notre API moderne et bien documentée.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4 mb-4">
|
||||||
|
<pre class="text-sm overflow-x-auto"><code>curl -X GET \
|
||||||
|
https://api.wibx.com/v1/connections \
|
||||||
|
-H "Authorization: Bearer {token}" \
|
||||||
|
-H "Content-Type: application/json"</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton variant="solid" size="sm">
|
||||||
|
Documentation API
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="outline" size="sm">
|
||||||
|
Tester l'API
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold mb-3">
|
||||||
|
SDKs Disponibles
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Utilisez nos SDKs officiels pour une intégration rapide.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-code" class="w-4 h-4 text-primary" />
|
||||||
|
JavaScript/Node.js
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-code" class="w-4 h-4 text-primary" />
|
||||||
|
Python
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-code" class="w-4 h-4 text-primary" />
|
||||||
|
PHP
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-code" class="w-4 h-4 text-primary" />
|
||||||
|
Go
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton variant="solid" size="sm">
|
||||||
|
Télécharger SDKs
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="outline" size="sm">
|
||||||
|
GitHub
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Métadonnées de la page
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Écosystème - Wibx Tour Layer 2',
|
||||||
|
description: 'Découvrez notre écosystème de partenaires technologiques. API ouverte, intégrations natives, programme de certification.'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Partenaires stratégiques
|
||||||
|
const strategicPartners = ref([
|
||||||
|
{ id: 1, name: 'AWS', description: 'Cloud Computing', icon: 'i-lucide-cloud' },
|
||||||
|
{ id: 2, name: 'Microsoft', description: 'Azure & Office 365', icon: 'i-lucide-building' },
|
||||||
|
{ id: 3, name: 'Google', description: 'Google Cloud', icon: 'i-lucide-search' },
|
||||||
|
{ id: 4, name: 'Salesforce', description: 'CRM Platform', icon: 'i-lucide-user-circle' },
|
||||||
|
{ id: 5, name: 'Oracle', description: 'Database & Cloud', icon: 'i-lucide-database' },
|
||||||
|
{ id: 6, name: 'SAP', description: 'Enterprise Software', icon: 'i-lucide-briefcase' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// Données des partenaires
|
||||||
|
const partners = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Cloudflare',
|
||||||
|
category: 'Sécurité',
|
||||||
|
description: 'Protection DDoS et CDN global pour une sécurité et performance optimales',
|
||||||
|
icon: 'i-lucide-shield',
|
||||||
|
location: 'Global',
|
||||||
|
clients: '26M',
|
||||||
|
services: ['DDoS Protection', 'CDN', 'WAF', 'DNS'],
|
||||||
|
integration: 'API native',
|
||||||
|
certified: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Equinix',
|
||||||
|
category: 'Infrastructure',
|
||||||
|
description: 'Leader mondial des centres de données et de l\'interconnexion',
|
||||||
|
icon: 'i-lucide-server',
|
||||||
|
location: 'Worldwide',
|
||||||
|
clients: '10K',
|
||||||
|
services: ['Colocation', 'Interconnexion', 'Edge Computing'],
|
||||||
|
integration: 'Connexion directe',
|
||||||
|
certified: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Fortinet',
|
||||||
|
category: 'Sécurité',
|
||||||
|
description: 'Solutions de cybersécurité et pare-feu nouvelle génération',
|
||||||
|
icon: 'i-lucide-lock',
|
||||||
|
location: 'Global',
|
||||||
|
clients: '540K',
|
||||||
|
services: ['Firewall', 'SD-WAN', 'Endpoint Protection'],
|
||||||
|
integration: 'API REST',
|
||||||
|
certified: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Cisco',
|
||||||
|
category: 'Réseau',
|
||||||
|
description: 'Équipements réseau et solutions de connectivité enterprise',
|
||||||
|
icon: 'i-lucide-network',
|
||||||
|
location: 'Worldwide',
|
||||||
|
clients: '1M',
|
||||||
|
services: ['Switching', 'Routing', 'SD-WAN', 'Wireless'],
|
||||||
|
integration: 'SNMP/API',
|
||||||
|
certified: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'VMware',
|
||||||
|
category: 'Virtualisation',
|
||||||
|
description: 'Solutions de virtualisation et cloud computing',
|
||||||
|
icon: 'i-lucide-layers',
|
||||||
|
location: 'Global',
|
||||||
|
clients: '500K',
|
||||||
|
services: ['vSphere', 'NSX', 'vSAN', 'Cloud'],
|
||||||
|
integration: 'vCenter API',
|
||||||
|
certified: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Okta',
|
||||||
|
category: 'Identité',
|
||||||
|
description: 'Gestion d\'identité et accès cloud',
|
||||||
|
icon: 'i-lucide-user-check',
|
||||||
|
location: 'Cloud',
|
||||||
|
clients: '15K',
|
||||||
|
services: ['SSO', 'MFA', 'Identity Management'],
|
||||||
|
integration: 'REST API',
|
||||||
|
certified: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Filtres
|
||||||
|
const selectedCategory = ref('all')
|
||||||
|
const categories = computed(() => [...new Set(partners.value.map(partner => partner.category))])
|
||||||
|
const certifiedPartners = computed(() => partners.value.filter(partner => partner.certified))
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{ value: 'all', label: 'Tous les partenaires' },
|
||||||
|
...categories.value.map(category => ({ value: category, label: category }))
|
||||||
|
])
|
||||||
|
|
||||||
|
const filteredPartners = computed(() => {
|
||||||
|
if (selectedCategory.value === 'all') {
|
||||||
|
return partners.value
|
||||||
|
}
|
||||||
|
return partners.value.filter(partner => partner.category === selectedCategory.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
306
app/pages/connect/internet-exchanges.vue
Normal file
306
app/pages/connect/internet-exchanges.vue
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
// Métadonnées de la page
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Internet Exchanges - Wibx Tour Layer 2',
|
||||||
|
description: 'Connectez-vous aux plus grands points d\'échange Internet (IXP) pour optimiser votre peering et réduire la latence.'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Données des Internet Exchanges
|
||||||
|
const exchanges = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'DE-CIX Frankfurt',
|
||||||
|
location: 'Francfort, Allemagne',
|
||||||
|
region: 'Europe',
|
||||||
|
tier: 1,
|
||||||
|
description: 'Le plus grand point d\'échange Internet au monde',
|
||||||
|
members: 950,
|
||||||
|
traffic: 11.2,
|
||||||
|
pricing: 'À partir de 150€/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'AMS-IX Amsterdam',
|
||||||
|
location: 'Amsterdam, Pays-Bas',
|
||||||
|
region: 'Europe',
|
||||||
|
tier: 1,
|
||||||
|
description: 'Point d\'échange historique en Europe',
|
||||||
|
members: 880,
|
||||||
|
traffic: 8.5,
|
||||||
|
pricing: 'À partir de 180€/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'France-IX Paris',
|
||||||
|
location: 'Paris, France',
|
||||||
|
region: 'Europe',
|
||||||
|
tier: 1,
|
||||||
|
description: 'Principal point d\'échange français',
|
||||||
|
members: 520,
|
||||||
|
traffic: 4.8,
|
||||||
|
pricing: 'À partir de 120€/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Equinix NY',
|
||||||
|
location: 'New York, États-Unis',
|
||||||
|
region: 'Amérique du Nord',
|
||||||
|
tier: 1,
|
||||||
|
description: 'Hub majeur en Amérique du Nord',
|
||||||
|
members: 1200,
|
||||||
|
traffic: 6.8,
|
||||||
|
pricing: 'À partir de 300€/mois'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
const selectedRegion = ref('all')
|
||||||
|
const regions = computed(() => [...new Set(exchanges.value.map(exchange => exchange.region))])
|
||||||
|
const totalPeers = computed(() => exchanges.value.reduce((sum, exchange) => sum + exchange.members, 0))
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{ value: 'all', label: 'Toutes les régions' },
|
||||||
|
...regions.value.map(region => ({ value: region, label: region }))
|
||||||
|
])
|
||||||
|
|
||||||
|
const filteredExchanges = computed(() => {
|
||||||
|
if (selectedRegion.value === 'all') {
|
||||||
|
return exchanges.value
|
||||||
|
}
|
||||||
|
return exchanges.value.filter(exchange => exchange.region === selectedRegion.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="internet-exchanges">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Internet Exchanges">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton to="/connect/data-centres" size="sm">
|
||||||
|
Data Centres
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
{{ exchanges.length }} points d'échange
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UBadge variant="subtle" class="text-xs">
|
||||||
|
{{ totalPeers }}+ membres
|
||||||
|
</UBadge>
|
||||||
|
<UBadge variant="subtle" class="text-xs">
|
||||||
|
Peering multi-latéral
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-4">
|
||||||
|
Internet Exchanges (IXP)
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||||
|
Connectez-vous aux plus grands points d'échange Internet pour un peering optimal
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiques -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-network" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
{{ exchanges.length }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Points d'échange
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-users" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
{{ totalPeers }}+
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Membres total
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-activity" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
100+
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Tbps total
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-timer" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
< 5ms
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Latence
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtres -->
|
||||||
|
<div>
|
||||||
|
<UTabs v-model="selectedRegion" :items="tabs" class="w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liste des Exchanges -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8">
|
||||||
|
Points d'échange disponibles
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="exchange in filteredExchanges"
|
||||||
|
:key="exchange.id"
|
||||||
|
class="hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
{{ exchange.name }}
|
||||||
|
</h3>
|
||||||
|
<UBadge
|
||||||
|
:variant="exchange.tier === 1 ? 'solid' : 'outline'"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Tier {{ exchange.tier }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
{{ exchange.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-map-pin" class="w-4 h-4" />
|
||||||
|
{{ exchange.location }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-users" class="w-4 h-4" />
|
||||||
|
{{ exchange.members }} membres
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-activity" class="w-4 h-4" />
|
||||||
|
{{ exchange.traffic }} Tbps
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-2 border-t">
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
Tarif mensuel
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold text-primary">
|
||||||
|
{{ exchange.pricing }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton variant="outline" block>
|
||||||
|
Se connecter
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avantages -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">
|
||||||
|
Avantages du peering IXP
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-trending-down" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Réduction des coûts
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Diminuez vos coûts de transit
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-timer" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Latence optimisée
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Chemins plus courts et directs
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-shield" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Redondance
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Résilience de l'infrastructure
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-network" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Écosystème riche
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Large réseau de partenaires
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
387
app/pages/connect/saas.vue
Normal file
387
app/pages/connect/saas.vue
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
<template>
|
||||||
|
<UDashboardPanel id="connect-saas">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Connect SaaS">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton to="/connect/ecosystem" size="sm">
|
||||||
|
Écosystème
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
{{ saasServices.length }} services SaaS
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UBadge variant="subtle" class="text-xs">
|
||||||
|
{{ categories.length }} catégories
|
||||||
|
</UBadge>
|
||||||
|
<UBadge variant="subtle" class="text-xs">
|
||||||
|
Intégration 1-clic
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-4">
|
||||||
|
Applications SaaS
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||||
|
Intégrez facilement vos services SaaS favoris avec notre plateforme de connectivité unifiée
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistiques -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-grid-3x3" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
{{ saasServices.length }}+
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Services SaaS
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-zap" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
1-clic
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Intégration
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-activity" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
99.9%
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Disponibilité
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center mx-auto mb-3">
|
||||||
|
<UIcon name="i-lucide-shield-check" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl font-bold mb-1">
|
||||||
|
SOC 2
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Certifié
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtres -->
|
||||||
|
<div>
|
||||||
|
<UTabs v-model="selectedCategory" :items="tabs" class="w-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Services SaaS -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8">
|
||||||
|
Services disponibles
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="service in filteredServices"
|
||||||
|
:key="service.id"
|
||||||
|
class="hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||||
|
<UIcon :name="service.icon" class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">
|
||||||
|
{{ service.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-gray-600 dark:text-gray-300">
|
||||||
|
{{ service.category }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
{{ service.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-users" class="w-4 h-4" />
|
||||||
|
{{ service.users }}+ utilisateurs
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<UIcon name="i-lucide-star" class="w-4 h-4" />
|
||||||
|
{{ service.rating }}/5 ({{ service.reviews }} avis)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-1 pt-2">
|
||||||
|
<UBadge
|
||||||
|
v-for="feature in service.features"
|
||||||
|
:key="feature"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{{ feature }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-2 border-t">
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
Tarification
|
||||||
|
</span>
|
||||||
|
<span class="text-sm font-semibold text-primary">
|
||||||
|
{{ service.pricing }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton variant="outline" block>
|
||||||
|
Connecter
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avantages -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">
|
||||||
|
Pourquoi choisir notre plateforme SaaS ?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-zap" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Intégration Rapide
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Connectez vos applications en 1-clic
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-shield-check" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Sécurité
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Chiffrement end-to-end et conformité
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-trending-up" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Évolutivité
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Adaptation automatique à vos besoins
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-headphones" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Support 24/7
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Assistance technique permanente
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processus d'intégration -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">
|
||||||
|
Comment ça marche ?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<span class="text-2xl font-bold text-primary">1</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Sélectionnez
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Choisissez les services SaaS à connecter
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<span class="text-2xl font-bold text-primary">2</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Configurez
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Paramètres de connectivité en quelques clics
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<span class="text-2xl font-bold text-primary">3</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">
|
||||||
|
Utilisez
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Applications connectées instantanément
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
// Métadonnées de la page
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Connect SaaS - Wibx Tour Layer 2',
|
||||||
|
description: 'Connectez facilement vos applications SaaS favorites avec notre plateforme unifiée. Intégration 1-clic, sécurité enterprise, support 24/7.'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Données des services SaaS
|
||||||
|
const saasServices = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Salesforce',
|
||||||
|
category: 'CRM',
|
||||||
|
description: 'Plateforme CRM leader mondial pour la gestion des relations clients',
|
||||||
|
icon: 'i-lucide-user-circle',
|
||||||
|
users: '150K',
|
||||||
|
rating: 4.8,
|
||||||
|
reviews: 5420,
|
||||||
|
features: ['CRM', 'Analytics', 'AI', 'Mobile'],
|
||||||
|
pricing: 'À partir de 25€/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Microsoft 365',
|
||||||
|
category: 'Productivité',
|
||||||
|
description: 'Suite bureautique complète avec Office, Teams et OneDrive',
|
||||||
|
icon: 'i-lucide-briefcase',
|
||||||
|
users: '300K',
|
||||||
|
rating: 4.7,
|
||||||
|
reviews: 8930,
|
||||||
|
features: ['Office', 'Teams', 'OneDrive', 'SharePoint'],
|
||||||
|
pricing: 'À partir de 12€/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Slack',
|
||||||
|
category: 'Communication',
|
||||||
|
description: 'Plateforme de communication et collaboration en équipe',
|
||||||
|
icon: 'i-lucide-message-circle',
|
||||||
|
users: '120K',
|
||||||
|
rating: 4.6,
|
||||||
|
reviews: 3210,
|
||||||
|
features: ['Chat', 'Channels', 'Video', 'Integrations'],
|
||||||
|
pricing: 'À partir de 6€/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Zoom',
|
||||||
|
category: 'Communication',
|
||||||
|
description: 'Solution de visioconférence et webinaires professionnels',
|
||||||
|
icon: 'i-lucide-video',
|
||||||
|
users: '200K',
|
||||||
|
rating: 4.5,
|
||||||
|
reviews: 6780,
|
||||||
|
features: ['Video', 'Webinaires', 'Recording', 'Screen Share'],
|
||||||
|
pricing: 'À partir de 14€/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'HubSpot',
|
||||||
|
category: 'Marketing',
|
||||||
|
description: 'Plateforme marketing automation et inbound marketing',
|
||||||
|
icon: 'i-lucide-megaphone',
|
||||||
|
users: '90K',
|
||||||
|
rating: 4.7,
|
||||||
|
reviews: 2340,
|
||||||
|
features: ['Marketing', 'Automation', 'Analytics', 'CRM'],
|
||||||
|
pricing: 'À partir de 45€/mois'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Notion',
|
||||||
|
category: 'Productivité',
|
||||||
|
description: 'Workspace tout-en-un pour notes, tâches et collaboration',
|
||||||
|
icon: 'i-lucide-file-text',
|
||||||
|
users: '60K',
|
||||||
|
rating: 4.8,
|
||||||
|
reviews: 2890,
|
||||||
|
features: ['Notes', 'Databases', 'Tasks', 'Templates'],
|
||||||
|
pricing: 'À partir de 8€/mois'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Filtres
|
||||||
|
const selectedCategory = ref('all')
|
||||||
|
const categories = computed(() => [...new Set(saasServices.value.map(service => service.category))])
|
||||||
|
|
||||||
|
const tabs = computed(() => [
|
||||||
|
{ value: 'all', label: 'Tous les services' },
|
||||||
|
...categories.value.map(category => ({ value: category, label: category }))
|
||||||
|
])
|
||||||
|
|
||||||
|
const filteredServices = computed(() => {
|
||||||
|
if (selectedCategory.value === 'all') {
|
||||||
|
return saasServices.value
|
||||||
|
}
|
||||||
|
return saasServices.value.filter(service => service.category === selectedCategory.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
256
app/pages/index.vue
Normal file
256
app/pages/index.vue
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { sub } from 'date-fns'
|
||||||
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
|
import type { Period, Range } from '~/types'
|
||||||
|
|
||||||
|
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[][]
|
||||||
|
|
||||||
|
const range = shallowRef<Range>({
|
||||||
|
start: sub(new Date(), { days: 14 }),
|
||||||
|
end: new Date()
|
||||||
|
})
|
||||||
|
const period = ref<Period>('daily')
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Wibx Tour Layer 2 - Connectivité Cloud et Réseau',
|
||||||
|
description: 'Plateforme de connectivité réseau Layer 2 pour cloud, data centers et services managés'
|
||||||
|
})
|
||||||
|
|
||||||
|
const cloudProviders = [
|
||||||
|
{ name: 'AWS', logo: 'i-simple-icons-amazonaws', status: 'Direct Connect' },
|
||||||
|
{ name: 'Azure', logo: 'i-simple-icons-microsoftazure', status: 'Partner' },
|
||||||
|
{ name: 'Google Cloud', logo: 'i-simple-icons-googlecloud', status: 'Direct Connect' },
|
||||||
|
{ name: 'Oracle Cloud', logo: 'i-simple-icons-oracle', status: 'New' },
|
||||||
|
{ name: 'IBM Cloud', logo: 'i-simple-icons-ibm', status: 'Partner' },
|
||||||
|
{ name: 'Alibaba Cloud', logo: 'i-simple-icons-alibabacloud', status: 'New' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const mainServices = [
|
||||||
|
{
|
||||||
|
title: 'Connect Cloud',
|
||||||
|
description: 'Connectez-vous directement aux plus grands fournisseurs cloud avec des liens Layer 2 privés.',
|
||||||
|
icon: 'i-lucide-cloud',
|
||||||
|
to: '/connect/clouds',
|
||||||
|
features: ['AWS Direct Connect', 'Azure ExpressRoute', 'Google Cloud Interconnect']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Marketplace',
|
||||||
|
description: 'Accédez à notre écosystème de partenaires et services via notre marketplace intégré.',
|
||||||
|
icon: 'i-lucide-store',
|
||||||
|
to: '/services/marketplace',
|
||||||
|
features: ['500+ Partenaires', 'APIs Intégrées', 'Facturation Unifiée']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Instant Layer 2',
|
||||||
|
description: 'Provisionnement instantané de liens Layer 2 avec SLA garanti et monitoring en temps réel.',
|
||||||
|
icon: 'i-lucide-zap',
|
||||||
|
to: '/services/layer2',
|
||||||
|
features: ['Déploiement < 5min', 'SLA 99.9%', 'Monitoring 24/7']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="home">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Accueil">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton to="/services/layer2" size="sm">
|
||||||
|
Créer une Connexion
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
Plateforme Wibx Tour Layer 2
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<UHero>
|
||||||
|
<template #title>
|
||||||
|
Connectivité <span class="text-primary">Layer 2</span> Instantanée et Sécurisée
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #description>
|
||||||
|
Wibx Tour Layer 2 vous permet de connecter instantanément vos infrastructures aux clouds,
|
||||||
|
data centers et services managés avec des performances garanties et une sécurité de niveau entreprise.
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #links>
|
||||||
|
<UButton to="/services/layer2" size="lg" class="mr-4">
|
||||||
|
Commencer Maintenant
|
||||||
|
</UButton>
|
||||||
|
<UButton to="/resources/api-docs" variant="outline" size="lg">
|
||||||
|
Voir la Documentation
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UHero>
|
||||||
|
|
||||||
|
<!-- Services principaux -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Services Principaux</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Découvrez nos solutions de connectivité réseau pour tous vos besoins
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="service in mainServices"
|
||||||
|
:key="service.title"
|
||||||
|
class="hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<UIcon :name="service.icon" size="32" class="text-primary" />
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold">{{ service.title }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
{{ service.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="space-y-2 mb-4">
|
||||||
|
<div
|
||||||
|
v-for="feature in service.features"
|
||||||
|
:key="feature"
|
||||||
|
class="flex items-center gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-check" class="text-green-500" />
|
||||||
|
<span>{{ feature }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton :to="service.to" variant="outline" block>
|
||||||
|
En savoir plus
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Partenaires Cloud -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Partenaires Cloud</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Connectez-vous aux plus grands fournisseurs cloud avec des liens directs
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
||||||
|
<div
|
||||||
|
v-for="provider in cloudProviders"
|
||||||
|
:key="provider.name"
|
||||||
|
class="flex flex-col items-center text-center p-4 bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<UIcon :name="provider.logo" size="48" class="mb-3 text-gray-600 dark:text-gray-300" />
|
||||||
|
<h4 class="font-medium mb-2">{{ provider.name }}</h4>
|
||||||
|
<UBadge
|
||||||
|
:variant="provider.status === 'Direct Connect' ? 'solid' : provider.status === 'New' ? 'outline' : 'subtle'"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ provider.status }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mt-8">
|
||||||
|
<UButton to="/connect/clouds" size="lg">
|
||||||
|
Voir Tous les Clouds
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avantages -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">
|
||||||
|
Pourquoi Choisir Wibx Tour ?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-zap" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Déploiement Rapide</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Provisionnement en moins de 5 minutes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-shield-check" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Sécurité Garantie</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Connexions privées et chiffrées
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-activity" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Monitoring 24/7</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Surveillance continue de vos liens
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-dollar-sign" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Tarification Transparente</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Pas de frais cachés, facturation au réel
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Call to Action -->
|
||||||
|
<div class="bg-primary text-white rounded-lg p-8 text-center">
|
||||||
|
<h2 class="text-3xl font-bold mb-4">
|
||||||
|
Prêt à Commencer ?
|
||||||
|
</h2>
|
||||||
|
<p class="text-xl mb-8 opacity-90">
|
||||||
|
Créez votre premier lien Layer 2 en quelques minutes
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<UButton to="/signup" size="lg" variant="outline" color="white">
|
||||||
|
Créer un Compte
|
||||||
|
</UButton>
|
||||||
|
<UButton to="/services/api" size="lg" variant="ghost" color="white">
|
||||||
|
Explorer les APIs
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
204
app/pages/login.vue
Normal file
204
app/pages/login.vue
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Connexion - Wibx Tour Layer 2',
|
||||||
|
description: 'Connectez-vous à votre compte Wibx Tour pour gérer vos connexions réseau Layer 2'
|
||||||
|
})
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
email: z.string().email('Email invalide'),
|
||||||
|
password: z.string().min(8, 'Le mot de passe doit contenir au moins 8 caractères'),
|
||||||
|
rememberMe: z.boolean().optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.output<typeof schema>
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
rememberMe: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const pending = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
async function onSubmit(event: Schema) {
|
||||||
|
try {
|
||||||
|
pending.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
// Simulation de l'authentification
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
|
|
||||||
|
// Redirection vers le tableau de bord
|
||||||
|
await navigateTo('/')
|
||||||
|
} catch (err) {
|
||||||
|
error.value = 'Email ou mot de passe incorrect'
|
||||||
|
} finally {
|
||||||
|
pending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex">
|
||||||
|
<!-- Left side - Form -->
|
||||||
|
<div class="flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-3xl font-bold tracking-tight">
|
||||||
|
Connexion
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
||||||
|
Accédez à votre compte Wibx Tour
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="error"
|
||||||
|
icon="i-lucide-alert-circle"
|
||||||
|
color="red"
|
||||||
|
variant="soft"
|
||||||
|
:title="error"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UForm
|
||||||
|
:schema="schema"
|
||||||
|
:state="state"
|
||||||
|
class="space-y-6"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<UFormGroup label="Email" name="email" required>
|
||||||
|
<UInput
|
||||||
|
v-model="state.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="votre@email.com"
|
||||||
|
icon="i-lucide-mail"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Mot de passe" name="password" required>
|
||||||
|
<UInput
|
||||||
|
v-model="state.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
icon="i-lucide-lock"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<UCheckbox
|
||||||
|
v-model="state.rememberMe"
|
||||||
|
name="rememberMe"
|
||||||
|
label="Se souvenir de moi"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ULink
|
||||||
|
to="/forgot-password"
|
||||||
|
class="text-sm text-primary hover:text-primary-600"
|
||||||
|
>
|
||||||
|
Mot de passe oublié ?
|
||||||
|
</ULink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
:loading="pending"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Se connecter
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Pas encore de compte ?
|
||||||
|
<ULink
|
||||||
|
to="/signup"
|
||||||
|
class="font-medium text-primary hover:text-primary-600"
|
||||||
|
>
|
||||||
|
Créer un compte
|
||||||
|
</ULink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social Login (Optional) -->
|
||||||
|
<div class="mt-6">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 flex items-center">
|
||||||
|
<div class="w-full border-t border-gray-300 dark:border-gray-600" />
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center text-sm">
|
||||||
|
<span class="px-2 bg-white dark:bg-gray-900 text-gray-500">
|
||||||
|
Ou continuer avec
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-2 gap-3">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
<UIcon name="i-simple-icons-google" class="mr-2" />
|
||||||
|
Google
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
<UIcon name="i-simple-icons-github" class="mr-2" />
|
||||||
|
GitHub
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side - Image/Branding -->
|
||||||
|
<div class="hidden lg:block relative flex-1">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-primary-500 to-primary-700">
|
||||||
|
<div class="flex items-center justify-center h-full p-12">
|
||||||
|
<div class="text-center text-white">
|
||||||
|
<div class="mb-8">
|
||||||
|
<UIcon name="i-lucide-network" size="80" class="mx-auto mb-4" />
|
||||||
|
<h2 class="text-4xl font-bold mb-4">
|
||||||
|
Wibx Tour Layer 2
|
||||||
|
</h2>
|
||||||
|
<p class="text-xl opacity-90">
|
||||||
|
La plateforme de connectivité réseau nouvelle génération
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-4 text-left max-w-md">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<UIcon name="i-lucide-check" class="mr-3 text-green-300" />
|
||||||
|
<span>Connexions Layer 2 instantanées</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<UIcon name="i-lucide-check" class="mr-3 text-green-300" />
|
||||||
|
<span>150+ data centers mondiaux</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<UIcon name="i-lucide-check" class="mr-3 text-green-300" />
|
||||||
|
<span>APIs complètes pour développeurs</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<UIcon name="i-lucide-check" class="mr-3 text-green-300" />
|
||||||
|
<span>Support 24/7 par des experts</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
340
app/pages/services/api.vue
Normal file
340
app/pages/services/api.vue
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'API Services - Intégration Programmable | Wibx Tour',
|
||||||
|
description: 'Automatisez vos connexions réseau avec nos APIs REST complètes. Documentation, SDK et exemples de code inclus.'
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiEndpoints = [
|
||||||
|
{
|
||||||
|
title: 'Locations API',
|
||||||
|
description: 'Récupérez la liste des data centers et points de présence disponibles',
|
||||||
|
endpoint: 'GET /api/v1/locations',
|
||||||
|
icon: 'i-lucide-map-pin',
|
||||||
|
category: 'Infrastructure'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Cloud Providers API',
|
||||||
|
description: 'Accédez aux informations des fournisseurs cloud et leurs régions',
|
||||||
|
endpoint: 'GET /api/v1/cloud-providers',
|
||||||
|
icon: 'i-lucide-cloud',
|
||||||
|
category: 'Cloud'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Layer 2 Links API',
|
||||||
|
description: 'Créez, modifiez et gérez vos connexions Layer 2',
|
||||||
|
endpoint: 'POST /api/v1/l2-links',
|
||||||
|
icon: 'i-lucide-link',
|
||||||
|
category: 'Connectivity'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Access Ports API',
|
||||||
|
description: 'Demandez et gérez vos ports d\'accès dans les data centers',
|
||||||
|
endpoint: 'POST /api/v1/access-ports',
|
||||||
|
icon: 'i-lucide-plug',
|
||||||
|
category: 'Infrastructure'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Quotes API',
|
||||||
|
description: 'Obtenez des devis instantanés pour vos connexions',
|
||||||
|
endpoint: 'POST /api/v1/quotes',
|
||||||
|
icon: 'i-lucide-calculator',
|
||||||
|
category: 'Billing'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Monitoring API',
|
||||||
|
description: 'Surveillez vos connexions et récupérez les métriques',
|
||||||
|
endpoint: 'GET /api/v1/monitoring',
|
||||||
|
icon: 'i-lucide-activity',
|
||||||
|
category: 'Monitoring'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const rateInformation = [
|
||||||
|
{
|
||||||
|
title: 'Limites de Taux',
|
||||||
|
content: 'Les APIs Wibx Tour sont limitées à 1000 requêtes par heure par clé API pour les comptes gratuits, et 10000 requêtes par heure pour les comptes premium. Les en-têtes de réponse incluent les informations de limite et de consommation.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Unités de Facturation',
|
||||||
|
content: 'Les appels API sont facturés par tranche de 1000 requêtes. Les premiers 10000 appels par mois sont gratuits. Au-delà, le tarif est de €0.01 par tranche de 1000 requêtes pour les endpoints de consultation et €0.05 pour les endpoints de provisionnement.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Authentification',
|
||||||
|
content: 'Toutes les requêtes API doivent inclure une clé API valide dans l\'en-tête Authorization: Bearer YOUR_API_KEY. Les clés API peuvent être générées depuis votre tableau de bord utilisateur.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Versioning',
|
||||||
|
content: 'L\'API utilise un versioning basé sur l\'URL (/api/v1/). Les versions majeures sont maintenues pendant au moins 12 mois après la sortie d\'une nouvelle version. Les changements non-breaking sont déployés sans notification.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const exampleCode = `// Exemple d'utilisation de l'API Wibx Tour
|
||||||
|
const wibxApi = new WibxTourAPI({
|
||||||
|
apiKey: 'your_api_key_here',
|
||||||
|
baseURL: 'https://api.wibx-tour.com/v1'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupérer les locations disponibles
|
||||||
|
const locations = await wibxApi.locations.list();
|
||||||
|
console.log('Locations disponibles:', locations);
|
||||||
|
|
||||||
|
// Créer une connexion Layer 2
|
||||||
|
const l2Link = await wibxApi.l2Links.create({
|
||||||
|
name: 'Ma première connexion',
|
||||||
|
sourceLocation: 'paris-dc1',
|
||||||
|
destinationLocation: 'london-dc2',
|
||||||
|
bandwidth: '1000', // 1 Gbps
|
||||||
|
vlan: 100
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Connexion créée:', l2Link);
|
||||||
|
|
||||||
|
// Surveiller la connexion
|
||||||
|
const metrics = await wibxApi.monitoring.getMetrics(l2Link.id, {
|
||||||
|
period: '1h',
|
||||||
|
metrics: ['bandwidth', 'latency', 'packet_loss']
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Métriques:', metrics);`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="api-services">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="API Services">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton size="sm" class="mr-2">
|
||||||
|
Obtenir une Clé API
|
||||||
|
</UButton>
|
||||||
|
<UButton to="#documentation" variant="outline" size="sm">
|
||||||
|
Voir la Documentation
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
APIs REST Complètes - Pour Développeurs
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Hero Description -->
|
||||||
|
<div>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
Automatisez la gestion de vos connexions réseau avec nos APIs REST puissantes et faciles à utiliser.
|
||||||
|
SDK, documentation complète et exemples de code inclus.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Endpoints Grid -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Endpoints Principaux</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Explorez nos APIs pour intégrer Wibx Tour dans vos applications
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="api in apiEndpoints"
|
||||||
|
:key="api.title"
|
||||||
|
class="hover:shadow-lg transition-shadow duration-300"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div class="w-12 h-12 bg-primary/10 rounded-lg flex items-center justify-center">
|
||||||
|
<UIcon :name="api.icon" size="24" class="text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<UBadge variant="soft" size="sm" class="mb-1">
|
||||||
|
{{ api.category }}
|
||||||
|
</UBadge>
|
||||||
|
<h3 class="font-semibold">{{ api.title }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<p class="text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
{{ api.description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-gray-100 dark:bg-gray-800 rounded-lg p-3 text-sm font-mono">
|
||||||
|
{{ api.endpoint }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton variant="outline" block class="mt-4">
|
||||||
|
Voir la Doc
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Code Example -->
|
||||||
|
<div id="documentation" class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Exemple d'Utilisation</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Commencez rapidement avec notre SDK JavaScript
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-gray-900 rounded-lg p-6 overflow-x-auto">
|
||||||
|
<pre class="text-green-400 text-sm"><code>{{ exampleCode }}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-8">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<UButton variant="outline">
|
||||||
|
<UIcon name="i-simple-icons-npm" class="mr-2" />
|
||||||
|
npm install wibx-tour-sdk
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="outline">
|
||||||
|
<UIcon name="i-simple-icons-github" class="mr-2" />
|
||||||
|
Voir sur GitHub
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rate Limits and Billing -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Tarification et Limites</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Informations sur les limites d'utilisation et la facturation des APIs
|
||||||
|
</p>
|
||||||
|
<UAccordion :items="rateInformation" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SDK and Tools -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">SDKs et Outils</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Bibliothèques officielles pour accélérer votre développement
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
|
<UCard class="text-center">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UIcon name="i-simple-icons-javascript" size="48" class="text-yellow-500" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<h3 class="font-semibold mb-2">JavaScript/Node.js</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
SDK officiel pour Node.js et navigateurs
|
||||||
|
</p>
|
||||||
|
<UButton variant="outline" size="sm" block>
|
||||||
|
Installer
|
||||||
|
</UButton>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UIcon name="i-simple-icons-python" size="48" class="text-blue-500" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<h3 class="font-semibold mb-2">Python</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Bibliothèque Python pour intégration facile
|
||||||
|
</p>
|
||||||
|
<UButton variant="outline" size="sm" block>
|
||||||
|
Installer
|
||||||
|
</UButton>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UIcon name="i-simple-icons-go" size="48" class="text-cyan-500" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<h3 class="font-semibold mb-2">Go</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Module Go pour applications haute performance
|
||||||
|
</p>
|
||||||
|
<UButton variant="outline" size="sm" block>
|
||||||
|
Installer
|
||||||
|
</UButton>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<UIcon name="i-lucide-terminal" size="48" class="text-gray-500" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<h3 class="font-semibold mb-2">CLI Tool</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Outil en ligne de commande pour administration
|
||||||
|
</p>
|
||||||
|
<UButton variant="outline" size="sm" block>
|
||||||
|
Télécharger
|
||||||
|
</UButton>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Support -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">Support Développeurs</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<UCard class="text-center">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<UIcon name="i-lucide-book" size="24" class="text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<h3 class="font-semibold mb-2">Documentation</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Documentation complète avec exemples et guides
|
||||||
|
</p>
|
||||||
|
<UButton variant="outline" block>
|
||||||
|
Consulter
|
||||||
|
</UButton>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<UIcon name="i-lucide-message-circle" size="24" class="text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<h3 class="font-semibold mb-2">Support Technique</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Équipe dédiée pour les intégrations complexes
|
||||||
|
</p>
|
||||||
|
<UButton variant="outline" block>
|
||||||
|
Contacter
|
||||||
|
</UButton>
|
||||||
|
</UCard>
|
||||||
|
|
||||||
|
<UCard class="text-center">
|
||||||
|
<template #header>
|
||||||
|
<div class="w-16 h-16 bg-purple-100 dark:bg-purple-900/20 rounded-full flex items-center justify-center mx-auto">
|
||||||
|
<UIcon name="i-lucide-users" size="24" class="text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<h3 class="font-semibold mb-2">Communauté</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300 mb-4">
|
||||||
|
Rejoignez notre communauté de développeurs
|
||||||
|
</p>
|
||||||
|
<UButton variant="outline" block>
|
||||||
|
Rejoindre
|
||||||
|
</UButton>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
295
app/pages/services/layer2.vue
Normal file
295
app/pages/services/layer2.vue
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Services Layer 2 - Connexions Privées Sécurisées | Wibx Tour',
|
||||||
|
description: 'Déployez des connexions Layer 2 privées en quelques minutes avec SLA garanti et monitoring 24/7'
|
||||||
|
})
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
title: 'Sélectionner les Points de Connexion',
|
||||||
|
description: 'Choisissez vos points de départ et d\'arrivée parmi nos 150+ data centers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Configurer la Bande Passante',
|
||||||
|
description: 'Définissez votre bande passante de 1 Mbps à 100 Gbps'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Validation et Déploiement',
|
||||||
|
description: 'Validation automatique et provisionnement en moins de 5 minutes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Monitoring et Gestion',
|
||||||
|
description: 'Surveillance continue avec alertes et reporting en temps réel'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const technicalSpecs = [
|
||||||
|
{
|
||||||
|
title: 'VLAN Configuration',
|
||||||
|
content: 'Configurez vos VLANs avec une granularité complète. Support des VLAN ID de 2 à 4094, avec possibilité de Q-in-Q pour les architectures complexes.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Access Ports Requis',
|
||||||
|
content: 'Chaque connexion Layer 2 nécessite un Access Port dans chaque data center. Ports disponibles : 1GE, 10GE, 40GE, 100GE avec connecteurs optiques et cuivre.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Redondance et Résilience',
|
||||||
|
content: 'Options de redondance active/passive ou active/active. Basculement automatique en moins de 50ms avec BGP ou LACP.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Qualité de Service (QoS)',
|
||||||
|
content: 'Gestion avancée de la QoS avec classification du trafic, limitation de bande passante et priorisation des paquets.'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const pricingPlans = [
|
||||||
|
{
|
||||||
|
name: 'Starter',
|
||||||
|
bandwidth: '1-100 Mbps',
|
||||||
|
price: '€29',
|
||||||
|
features: [
|
||||||
|
'Connexion Layer 2 basique',
|
||||||
|
'Monitoring standard',
|
||||||
|
'Support par email',
|
||||||
|
'SLA 99.5%'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Professional',
|
||||||
|
bandwidth: '100 Mbps - 1 Gbps',
|
||||||
|
price: '€199',
|
||||||
|
features: [
|
||||||
|
'Connexion Layer 2 avancée',
|
||||||
|
'Monitoring en temps réel',
|
||||||
|
'Support prioritaire 24/7',
|
||||||
|
'SLA 99.9%',
|
||||||
|
'Redondance optionnelle'
|
||||||
|
],
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Enterprise',
|
||||||
|
bandwidth: '1-100 Gbps',
|
||||||
|
price: 'Sur mesure',
|
||||||
|
features: [
|
||||||
|
'Connexions Layer 2 multiples',
|
||||||
|
'Monitoring avancé + alertes',
|
||||||
|
'Support dédié',
|
||||||
|
'SLA 99.99%',
|
||||||
|
'Redondance incluse',
|
||||||
|
'QoS avancée'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="layer2">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Services Layer 2">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton size="sm" class="mr-2">
|
||||||
|
Créer une Connexion
|
||||||
|
</UButton>
|
||||||
|
<UButton to="#demo" variant="outline" size="sm">
|
||||||
|
Voir la Démo
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
Connexions Layer 2 Privées - Déploiement Instantané
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-12">
|
||||||
|
<!-- Hero Section -->
|
||||||
|
<UHero>
|
||||||
|
<template #description>
|
||||||
|
Créez des connexions réseau privées et sécurisées entre vos infrastructures,
|
||||||
|
clouds et data centers en quelques minutes avec notre plateforme automatisée.
|
||||||
|
</template>
|
||||||
|
</UHero>
|
||||||
|
|
||||||
|
<!-- SLA Alert -->
|
||||||
|
<UAlert
|
||||||
|
icon="i-lucide-clock"
|
||||||
|
title="Temps de Déploiement"
|
||||||
|
description="Provisionnement automatique en moins de 5 minutes avec SLA 99.9% de disponibilité"
|
||||||
|
color="green"
|
||||||
|
variant="soft"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Étapes de Provisionnement -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Comment ça Marche</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Un processus simple en 4 étapes pour déployer vos connexions Layer 2
|
||||||
|
</p>
|
||||||
|
<USteps :items="steps" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Video Demo -->
|
||||||
|
<div id="demo" class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Démo d'Utilisation</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Découvrez comment créer votre première connexion Layer 2 en 2 minutes
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="aspect-video bg-gray-200 dark:bg-gray-800 rounded-lg flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<UIcon name="i-lucide-play-circle" size="64" class="text-primary mb-4" />
|
||||||
|
<p class="text-lg font-medium">Démo Vidéo</p>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Cliquez pour voir le processus de création d'une connexion Layer 2
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Détails Techniques -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Spécifications Techniques</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Configurations avancées pour répondre à tous vos besoins réseau
|
||||||
|
</p>
|
||||||
|
<UAccordion :items="technicalSpecs" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tarification -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Plans et Tarification</h2>
|
||||||
|
<p class="text-gray-600 dark:text-gray-400 mb-8">
|
||||||
|
Choisissez le plan qui correspond à vos besoins
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="plan in pricingPlans"
|
||||||
|
:key="plan.name"
|
||||||
|
:class="plan.popular ? 'ring-2 ring-primary' : ''"
|
||||||
|
class="relative"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="text-center">
|
||||||
|
<UBadge v-if="plan.popular" class="mb-4" variant="solid">
|
||||||
|
Populaire
|
||||||
|
</UBadge>
|
||||||
|
<h3 class="text-xl font-bold mb-2">{{ plan.name }}</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{{ plan.bandwidth }}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-baseline justify-center">
|
||||||
|
<span class="text-3xl font-bold">{{ plan.price }}</span>
|
||||||
|
<span v-if="plan.price !== 'Sur mesure'" class="text-sm text-gray-600 dark:text-gray-400 ml-1">
|
||||||
|
/mois
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="feature in plan.features"
|
||||||
|
:key="feature"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-check" class="text-green-500" />
|
||||||
|
<span class="text-sm">{{ feature }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton
|
||||||
|
:variant="plan.popular ? 'solid' : 'outline'"
|
||||||
|
block
|
||||||
|
class="mt-6"
|
||||||
|
>
|
||||||
|
{{ plan.price === 'Sur mesure' ? 'Nous Contacter' : 'Commencer' }}
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avantages -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">
|
||||||
|
Pourquoi Choisir Nos Services Layer 2 ?
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-shield" size="24" class="text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Sécurité Maximale</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Connexions privées isolées du trafic internet public
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-gauge" size="24" class="text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Performances Optimales</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Latence ultra-faible et bande passante garantie
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-purple-100 dark:bg-purple-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-settings" size="24" class="text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Flexibilité Totale</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Modifiez votre bande passante à la demande
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-orange-100 dark:bg-orange-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-clock" size="24" class="text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Déploiement Rapide</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Provisionnement automatique en quelques minutes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-red-100 dark:bg-red-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-activity" size="24" class="text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Monitoring Avancé</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Surveillance en temps réel avec alertes proactives
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-teal-100 dark:bg-teal-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-headphones" size="24" class="text-teal-600 dark:text-teal-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Support Expert</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Équipe technique disponible 24/7 pour vous assister
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
331
app/pages/services/marketplace.vue
Normal file
331
app/pages/services/marketplace.vue
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Marketplace - Services et Partenaires | Wibx Tour',
|
||||||
|
description: 'Découvrez notre écosystème de 500+ partenaires et services cloud intégrés'
|
||||||
|
})
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ key: 'all', label: 'Tous', count: 523 },
|
||||||
|
{ key: 'security', label: 'Sécurité', count: 89 },
|
||||||
|
{ key: 'storage', label: 'Stockage', count: 156 },
|
||||||
|
{ key: 'analytics', label: 'Analytics', count: 67 },
|
||||||
|
{ key: 'ai-ml', label: 'IA & ML', count: 45 },
|
||||||
|
{ key: 'backup', label: 'Sauvegarde', count: 78 },
|
||||||
|
{ key: 'monitoring', label: 'Monitoring', count: 88 }
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectedCategory = ref('all')
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
name: 'CloudFlare Security',
|
||||||
|
provider: 'CloudFlare',
|
||||||
|
category: 'security',
|
||||||
|
description: 'Protection DDoS et firewall applicatif de nouvelle génération',
|
||||||
|
logo: 'i-simple-icons-cloudflare',
|
||||||
|
rating: 4.8,
|
||||||
|
reviews: 1250,
|
||||||
|
price: 'À partir de €29/mois',
|
||||||
|
features: ['DDoS Protection', 'WAF', 'Bot Management', 'Rate Limiting'],
|
||||||
|
integration: 'Direct',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Veeam Backup',
|
||||||
|
provider: 'Veeam',
|
||||||
|
category: 'backup',
|
||||||
|
description: 'Solution de sauvegarde et récupération pour environnements virtualisés',
|
||||||
|
logo: 'i-lucide-database',
|
||||||
|
rating: 4.6,
|
||||||
|
reviews: 892,
|
||||||
|
price: 'À partir de €89/mois',
|
||||||
|
features: ['Backup Automatique', 'Réplication', 'Récupération Instantanée'],
|
||||||
|
integration: 'API'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Splunk Analytics',
|
||||||
|
provider: 'Splunk',
|
||||||
|
category: 'analytics',
|
||||||
|
description: 'Plateforme d\'analyse de données et de logs en temps réel',
|
||||||
|
logo: 'i-lucide-bar-chart-3',
|
||||||
|
rating: 4.7,
|
||||||
|
reviews: 678,
|
||||||
|
price: 'À partir de €199/mois',
|
||||||
|
features: ['Log Analysis', 'SIEM', 'APM', 'Machine Learning'],
|
||||||
|
integration: 'Direct'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AWS Lambda',
|
||||||
|
provider: 'Amazon Web Services',
|
||||||
|
category: 'ai-ml',
|
||||||
|
description: 'Calcul serverless pour l\'exécution de code sans gestion de serveurs',
|
||||||
|
logo: 'i-simple-icons-amazonaws',
|
||||||
|
rating: 4.5,
|
||||||
|
reviews: 2150,
|
||||||
|
price: 'Pay per use',
|
||||||
|
features: ['Serverless', 'Auto Scaling', 'Event Driven', 'Multi-Language'],
|
||||||
|
integration: 'Native',
|
||||||
|
popular: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Azure Blob Storage',
|
||||||
|
provider: 'Microsoft',
|
||||||
|
category: 'storage',
|
||||||
|
description: 'Stockage d\'objets massivement évolutif pour données non structurées',
|
||||||
|
logo: 'i-simple-icons-microsoftazure',
|
||||||
|
rating: 4.4,
|
||||||
|
reviews: 1456,
|
||||||
|
price: 'À partir de €0.02/GB',
|
||||||
|
features: ['Hot/Cold Tiers', 'Géo-Réplication', 'CDN Integration'],
|
||||||
|
integration: 'Native'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Datadog Monitoring',
|
||||||
|
provider: 'Datadog',
|
||||||
|
category: 'monitoring',
|
||||||
|
description: 'Plateforme de monitoring et observabilité pour applications cloud',
|
||||||
|
logo: 'i-lucide-activity',
|
||||||
|
rating: 4.6,
|
||||||
|
reviews: 934,
|
||||||
|
price: 'À partir de €15/host/mois',
|
||||||
|
features: ['APM', 'Infrastructure Monitoring', 'Logs', 'Alertes'],
|
||||||
|
integration: 'API'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredServices = computed(() => {
|
||||||
|
if (selectedCategory.value === 'all') {
|
||||||
|
return services
|
||||||
|
}
|
||||||
|
return services.filter(service => service.category === selectedCategory.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const getIntegrationColor = (integration: string) => {
|
||||||
|
switch (integration) {
|
||||||
|
case 'Native': return 'green'
|
||||||
|
case 'Direct': return 'blue'
|
||||||
|
case 'API': return 'orange'
|
||||||
|
default: return 'gray'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="marketplace">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Marketplace">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #right>
|
||||||
|
<UButton size="sm" class="mr-2">
|
||||||
|
Devenir Partenaire
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="outline" size="sm">
|
||||||
|
<UIcon name="i-lucide-filter" class="mr-2" />
|
||||||
|
Filtres
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<template #left>
|
||||||
|
<UBadge variant="subtle" size="lg">
|
||||||
|
500+ Services et Partenaires - Écosystème Intégré
|
||||||
|
</UBadge>
|
||||||
|
</template>
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="space-y-8">
|
||||||
|
<!-- Hero Description -->
|
||||||
|
<div>
|
||||||
|
<p class="text-lg text-gray-600 dark:text-gray-400">
|
||||||
|
Accédez à notre écosystème de plus de 500 partenaires et services cloud intégrés.
|
||||||
|
Déployez et gérez vos services depuis une interface unique avec facturation centralisée.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Catégories -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold mb-4">Catégories</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<UButton
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.key"
|
||||||
|
:variant="selectedCategory === category.key ? 'solid' : 'outline'"
|
||||||
|
size="sm"
|
||||||
|
@click="selectedCategory = category.key"
|
||||||
|
>
|
||||||
|
{{ category.label }}
|
||||||
|
<UBadge
|
||||||
|
variant="subtle"
|
||||||
|
size="sm"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
{{ category.count }}
|
||||||
|
</UBadge>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Services Grid -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<UCard
|
||||||
|
v-for="service in filteredServices"
|
||||||
|
:key="service.name"
|
||||||
|
class="hover:shadow-lg transition-shadow duration-300"
|
||||||
|
:class="service.popular ? 'ring-2 ring-primary/20' : ''"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-12 h-12 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-center">
|
||||||
|
<UIcon :name="service.logo" size="24" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">{{ service.name }}</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">{{ service.provider }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UBadge
|
||||||
|
v-if="service.popular"
|
||||||
|
variant="solid"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Populaire
|
||||||
|
</UBadge>
|
||||||
|
<UBadge
|
||||||
|
:color="getIntegrationColor(service.integration)"
|
||||||
|
variant="soft"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ service.integration }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">{{ service.description }}</p>
|
||||||
|
|
||||||
|
<!-- Rating et Prix -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<UIcon name="i-lucide-star" size="16" class="text-yellow-500" />
|
||||||
|
<span class="text-sm font-medium">{{ service.rating }}</span>
|
||||||
|
<span class="text-xs text-gray-500">({{ service.reviews }})</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm font-medium text-primary">{{ service.price }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fonctionnalités -->
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-medium text-gray-600 dark:text-gray-400 mb-2">Fonctionnalités Clés</p>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<UBadge
|
||||||
|
v-for="feature in service.features"
|
||||||
|
:key="feature"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{{ feature }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<UButton block>
|
||||||
|
Installer
|
||||||
|
</UButton>
|
||||||
|
<UButton variant="outline" square>
|
||||||
|
<UIcon name="i-lucide-info" />
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats du Marketplace -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-8">
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">Marketplace en Chiffres</h2>
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl font-bold text-primary mb-2">500+</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">Services Disponibles</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl font-bold text-primary mb-2">150+</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">Partenaires Certifiés</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl font-bold text-primary mb-2">50K+</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">Déploiements Mensuels</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl font-bold text-primary mb-2">99.5%</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400">Taux de Satisfaction</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Avantages -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold mb-8 text-center">Avantages du Marketplace</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div class="text-center p-6 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
|
<div class="w-16 h-16 bg-blue-100 dark:bg-blue-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-credit-card" size="24" class="text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Facturation Unifiée</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Une seule facture pour tous vos services cloud et partenaires
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-6 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
|
<div class="w-16 h-16 bg-green-100 dark:bg-green-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-zap" size="24" class="text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Déploiement Instantané</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Activez vos services en quelques clics avec provisionnement automatique
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center p-6 border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
|
<div class="w-16 h-16 bg-purple-100 dark:bg-purple-900/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<UIcon name="i-lucide-shield-check" size="24" class="text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<h3 class="font-semibold mb-2">Sécurité Certifiée</h3>
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Tous nos partenaires sont certifiés et audités pour la sécurité
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Call to Action -->
|
||||||
|
<div class="bg-primary text-white rounded-lg p-8 text-center">
|
||||||
|
<h2 class="text-2xl font-bold mb-4">Développez Votre Écosystème</h2>
|
||||||
|
<p class="text-lg mb-8 opacity-90">
|
||||||
|
Explorez nos 500+ services et trouvez les solutions parfaites pour votre entreprise
|
||||||
|
</p>
|
||||||
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<UButton size="lg" variant="outline" color="white">
|
||||||
|
Explorer le Catalogue
|
||||||
|
</UButton>
|
||||||
|
<UButton size="lg" variant="ghost" color="white">
|
||||||
|
Devenir Partenaire
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
55
app/pages/settings.vue
Normal file
55
app/pages/settings.vue
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const links = [[{
|
||||||
|
label: 'General',
|
||||||
|
icon: 'i-lucide-user',
|
||||||
|
to: '/settings',
|
||||||
|
exact: true
|
||||||
|
}, {
|
||||||
|
label: 'Members',
|
||||||
|
icon: 'i-lucide-users',
|
||||||
|
to: '/settings/members'
|
||||||
|
}, {
|
||||||
|
label: 'Notifications',
|
||||||
|
icon: 'i-lucide-bell',
|
||||||
|
to: '/settings/notifications'
|
||||||
|
}, {
|
||||||
|
label: 'Security',
|
||||||
|
icon: 'i-lucide-shield',
|
||||||
|
to: '/settings/security'
|
||||||
|
}], [{
|
||||||
|
label: 'Documentation',
|
||||||
|
icon: 'i-lucide-book-open',
|
||||||
|
to: 'https://ui.nuxt.com/getting-started/installation/pro/nuxt',
|
||||||
|
target: '_blank'
|
||||||
|
}, {
|
||||||
|
label: 'Buy now',
|
||||||
|
icon: 'i-lucide-shopping-cart',
|
||||||
|
to: 'https://ui.nuxt.com/pro/purchase',
|
||||||
|
target: '_blank'
|
||||||
|
}]] satisfies NavigationMenuItem[][]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="settings" :ui="{ body: 'lg:py-12' }">
|
||||||
|
<template #header>
|
||||||
|
<UDashboardNavbar title="Settings">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<UDashboardToolbar>
|
||||||
|
<!-- NOTE: The `-mx-1` class is used to align with the `DashboardSidebarCollapse` button here. -->
|
||||||
|
<UNavigationMenu :items="links" highlight class="-mx-1 flex-1" />
|
||||||
|
</UDashboardToolbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #body>
|
||||||
|
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-2xl mx-auto">
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
158
app/pages/settings/index.vue
Normal file
158
app/pages/settings/index.vue
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import * as z from 'zod'
|
||||||
|
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const fileRef = ref<HTMLInputElement>()
|
||||||
|
|
||||||
|
const profileSchema = z.object({
|
||||||
|
name: z.string().min(2, 'Too short'),
|
||||||
|
email: z.string().email('Invalid email'),
|
||||||
|
username: z.string().min(2, 'Too short'),
|
||||||
|
avatar: z.string().optional(),
|
||||||
|
bio: z.string().optional()
|
||||||
|
})
|
||||||
|
|
||||||
|
type ProfileSchema = z.output<typeof profileSchema>
|
||||||
|
|
||||||
|
const profile = reactive<Partial<ProfileSchema>>({
|
||||||
|
name: 'Benjamin Canac',
|
||||||
|
email: 'ben@nuxtlabs.com',
|
||||||
|
username: 'benjamincanac',
|
||||||
|
avatar: undefined,
|
||||||
|
bio: undefined
|
||||||
|
})
|
||||||
|
const toast = useToast()
|
||||||
|
async function onSubmit(event: FormSubmitEvent<ProfileSchema>) {
|
||||||
|
toast.add({
|
||||||
|
title: 'Success',
|
||||||
|
description: 'Your settings have been updated.',
|
||||||
|
icon: 'i-lucide-check',
|
||||||
|
color: 'success'
|
||||||
|
})
|
||||||
|
console.log(event.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFileChange(e: Event) {
|
||||||
|
const input = e.target as HTMLInputElement
|
||||||
|
|
||||||
|
if (!input.files?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.avatar = URL.createObjectURL(input.files[0]!)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFileClick() {
|
||||||
|
fileRef.value?.click()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UForm
|
||||||
|
id="settings"
|
||||||
|
:schema="profileSchema"
|
||||||
|
:state="profile"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<UPageCard
|
||||||
|
title="Profile"
|
||||||
|
description="These informations will be displayed publicly."
|
||||||
|
variant="naked"
|
||||||
|
orientation="horizontal"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
form="settings"
|
||||||
|
label="Save changes"
|
||||||
|
color="neutral"
|
||||||
|
type="submit"
|
||||||
|
class="w-fit lg:ms-auto"
|
||||||
|
/>
|
||||||
|
</UPageCard>
|
||||||
|
|
||||||
|
<UPageCard variant="subtle">
|
||||||
|
<UFormField
|
||||||
|
name="name"
|
||||||
|
label="Name"
|
||||||
|
description="Will appear on receipts, invoices, and other communication."
|
||||||
|
required
|
||||||
|
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="profile.name"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
<USeparator />
|
||||||
|
<UFormField
|
||||||
|
name="email"
|
||||||
|
label="Email"
|
||||||
|
description="Used to sign in, for email receipts and product updates."
|
||||||
|
required
|
||||||
|
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="profile.email"
|
||||||
|
type="email"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
<USeparator />
|
||||||
|
<UFormField
|
||||||
|
name="username"
|
||||||
|
label="Username"
|
||||||
|
description="Your unique username for logging in and your profile URL."
|
||||||
|
required
|
||||||
|
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||||
|
>
|
||||||
|
<UInput
|
||||||
|
v-model="profile.username"
|
||||||
|
type="username"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
<USeparator />
|
||||||
|
<UFormField
|
||||||
|
name="avatar"
|
||||||
|
label="Avatar"
|
||||||
|
description="JPG, GIF or PNG. 1MB Max."
|
||||||
|
class="flex max-sm:flex-col justify-between sm:items-center gap-4"
|
||||||
|
>
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<UAvatar
|
||||||
|
:src="profile.avatar"
|
||||||
|
:alt="profile.name"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
<UButton
|
||||||
|
label="Choose"
|
||||||
|
color="neutral"
|
||||||
|
@click="onFileClick"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
ref="fileRef"
|
||||||
|
type="file"
|
||||||
|
class="hidden"
|
||||||
|
accept=".jpg, .jpeg, .png, .gif"
|
||||||
|
@change="onFileChange"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</UFormField>
|
||||||
|
<USeparator />
|
||||||
|
<UFormField
|
||||||
|
name="bio"
|
||||||
|
label="Bio"
|
||||||
|
description="Brief description for your profile. URLs are hyperlinked."
|
||||||
|
class="flex max-sm:flex-col justify-between items-start gap-4"
|
||||||
|
:ui="{ container: 'w-full' }"
|
||||||
|
>
|
||||||
|
<UTextarea
|
||||||
|
v-model="profile.bio"
|
||||||
|
:rows="5"
|
||||||
|
autoresize
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</UPageCard>
|
||||||
|
</UForm>
|
||||||
|
</template>
|
||||||
45
app/pages/settings/members.vue
Normal file
45
app/pages/settings/members.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { Member } from '~/types'
|
||||||
|
|
||||||
|
const { data: members } = await useFetch<Member[]>('/api/members', { default: () => [] })
|
||||||
|
|
||||||
|
const q = ref('')
|
||||||
|
|
||||||
|
const filteredMembers = computed(() => {
|
||||||
|
return members.value.filter((member) => {
|
||||||
|
return member.name.search(new RegExp(q.value, 'i')) !== -1 || member.username.search(new RegExp(q.value, 'i')) !== -1
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<UPageCard
|
||||||
|
title="Members"
|
||||||
|
description="Invite new members by email address."
|
||||||
|
variant="naked"
|
||||||
|
orientation="horizontal"
|
||||||
|
class="mb-4"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
label="Invite people"
|
||||||
|
color="neutral"
|
||||||
|
class="w-fit lg:ms-auto"
|
||||||
|
/>
|
||||||
|
</UPageCard>
|
||||||
|
|
||||||
|
<UPageCard variant="subtle" :ui="{ container: 'p-0 sm:p-0 gap-y-0', wrapper: 'items-stretch', header: 'p-4 mb-0 border-b border-default' }">
|
||||||
|
<template #header>
|
||||||
|
<UInput
|
||||||
|
v-model="q"
|
||||||
|
icon="i-lucide-search"
|
||||||
|
placeholder="Search members"
|
||||||
|
autofocus
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<SettingsMembersList :members="filteredMembers" />
|
||||||
|
</UPageCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
71
app/pages/settings/notifications.vue
Normal file
71
app/pages/settings/notifications.vue
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
const state = reactive<{ [key: string]: boolean }>({
|
||||||
|
email: true,
|
||||||
|
desktop: false,
|
||||||
|
product_updates: true,
|
||||||
|
weekly_digest: false,
|
||||||
|
important_updates: true
|
||||||
|
})
|
||||||
|
|
||||||
|
const sections = [{
|
||||||
|
title: 'Notification channels',
|
||||||
|
description: 'Where can we notify you?',
|
||||||
|
fields: [{
|
||||||
|
name: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
description: 'Receive a daily email digest.'
|
||||||
|
}, {
|
||||||
|
name: 'desktop',
|
||||||
|
label: 'Desktop',
|
||||||
|
description: 'Receive desktop notifications.'
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
title: 'Account updates',
|
||||||
|
description: 'Receive updates about Nuxt UI.',
|
||||||
|
fields: [{
|
||||||
|
name: 'weekly_digest',
|
||||||
|
label: 'Weekly digest',
|
||||||
|
description: 'Receive a weekly digest of news.'
|
||||||
|
}, {
|
||||||
|
name: 'product_updates',
|
||||||
|
label: 'Product updates',
|
||||||
|
description: 'Receive a monthly email with all new features and updates.'
|
||||||
|
}, {
|
||||||
|
name: 'important_updates',
|
||||||
|
label: 'Important updates',
|
||||||
|
description: 'Receive emails about important updates like security fixes, maintenance, etc.'
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
|
||||||
|
async function onChange() {
|
||||||
|
// Do something with data
|
||||||
|
console.log(state)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-for="(section, index) in sections" :key="index">
|
||||||
|
<UPageCard
|
||||||
|
:title="section.title"
|
||||||
|
:description="section.description"
|
||||||
|
variant="naked"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UPageCard variant="subtle" :ui="{ container: 'divide-y divide-default' }">
|
||||||
|
<UFormField
|
||||||
|
v-for="field in section.fields"
|
||||||
|
:key="field.name"
|
||||||
|
:name="field.name"
|
||||||
|
:label="field.label"
|
||||||
|
:description="field.description"
|
||||||
|
class="flex items-center justify-between not-last:pb-4 gap-2"
|
||||||
|
>
|
||||||
|
<USwitch
|
||||||
|
v-model="state[field.name]"
|
||||||
|
@update:model-value="onChange"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
</UPageCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
69
app/pages/settings/security.vue
Normal file
69
app/pages/settings/security.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import * as z from 'zod'
|
||||||
|
import type { FormError } from '@nuxt/ui'
|
||||||
|
|
||||||
|
const passwordSchema = z.object({
|
||||||
|
current: z.string().min(8, 'Must be at least 8 characters'),
|
||||||
|
new: z.string().min(8, 'Must be at least 8 characters')
|
||||||
|
})
|
||||||
|
|
||||||
|
type PasswordSchema = z.output<typeof passwordSchema>
|
||||||
|
|
||||||
|
const password = reactive<Partial<PasswordSchema>>({
|
||||||
|
current: undefined,
|
||||||
|
new: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const validate = (state: Partial<PasswordSchema>): FormError[] => {
|
||||||
|
const errors: FormError[] = []
|
||||||
|
if (state.current && state.new && state.current === state.new) {
|
||||||
|
errors.push({ name: 'new', message: 'Passwords must be different' })
|
||||||
|
}
|
||||||
|
return errors
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UPageCard
|
||||||
|
title="Password"
|
||||||
|
description="Confirm your current password before setting a new one."
|
||||||
|
variant="subtle"
|
||||||
|
>
|
||||||
|
<UForm
|
||||||
|
:schema="passwordSchema"
|
||||||
|
:state="password"
|
||||||
|
:validate="validate"
|
||||||
|
class="flex flex-col gap-4 max-w-xs"
|
||||||
|
>
|
||||||
|
<UFormField name="current">
|
||||||
|
<UInput
|
||||||
|
v-model="password.current"
|
||||||
|
type="password"
|
||||||
|
placeholder="Current password"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UFormField name="new">
|
||||||
|
<UInput
|
||||||
|
v-model="password.new"
|
||||||
|
type="password"
|
||||||
|
placeholder="New password"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</UFormField>
|
||||||
|
|
||||||
|
<UButton label="Update" class="w-fit" type="submit" />
|
||||||
|
</UForm>
|
||||||
|
</UPageCard>
|
||||||
|
|
||||||
|
<UPageCard
|
||||||
|
title="Account"
|
||||||
|
description="No longer want to use our service? You can delete your account here. This action is not reversible. All information related to this account will be deleted permanently."
|
||||||
|
class="bg-gradient-to-tl from-error/10 from-5% to-default"
|
||||||
|
>
|
||||||
|
<template #footer>
|
||||||
|
<UButton label="Delete account" color="error" />
|
||||||
|
</template>
|
||||||
|
</UPageCard>
|
||||||
|
</template>
|
||||||
298
app/pages/signup.vue
Normal file
298
app/pages/signup.vue
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
useSeoMeta({
|
||||||
|
title: 'Inscription - Wibx Tour Layer 2',
|
||||||
|
description: 'Créez votre compte Wibx Tour et commencez à déployer des connexions réseau Layer 2 en quelques minutes'
|
||||||
|
})
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
firstName: z.string().min(2, 'Le prénom doit contenir au moins 2 caractères'),
|
||||||
|
lastName: z.string().min(2, 'Le nom doit contenir au moins 2 caractères'),
|
||||||
|
company: z.string().min(2, 'Le nom de l\'entreprise doit contenir au moins 2 caractères'),
|
||||||
|
email: z.string().email('Email invalide'),
|
||||||
|
password: z.string().min(8, 'Le mot de passe doit contenir au moins 8 caractères'),
|
||||||
|
confirmPassword: z.string(),
|
||||||
|
terms: z.boolean().refine(val => val === true, 'Vous devez accepter les conditions d\'utilisation')
|
||||||
|
}).refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: 'Les mots de passe ne correspondent pas',
|
||||||
|
path: ['confirmPassword']
|
||||||
|
})
|
||||||
|
|
||||||
|
type Schema = z.output<typeof schema>
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
company: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
terms: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const pending = ref(false)
|
||||||
|
const success = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
async function onSubmit(_event: Schema) {
|
||||||
|
try {
|
||||||
|
pending.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
// Simulation de l'inscription
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||||
|
|
||||||
|
success.value = true
|
||||||
|
|
||||||
|
// Redirection après succès
|
||||||
|
setTimeout(() => {
|
||||||
|
navigateTo('/login')
|
||||||
|
}, 2000)
|
||||||
|
} catch (_err) {
|
||||||
|
error.value = 'Une erreur s\'est produite lors de l\'inscription'
|
||||||
|
} finally {
|
||||||
|
pending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen flex">
|
||||||
|
<!-- Left side - Form -->
|
||||||
|
<div class="flex-1 flex items-center justify-center px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="max-w-md w-full space-y-8">
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="text-3xl font-bold tracking-tight">
|
||||||
|
Créer un compte
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-gray-600 dark:text-gray-400">
|
||||||
|
Rejoignez Wibx Tour et déployez votre première connexion Layer 2
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="success"
|
||||||
|
icon="i-lucide-check-circle"
|
||||||
|
color="green"
|
||||||
|
variant="soft"
|
||||||
|
title="Compte créé avec succès !"
|
||||||
|
description="Vous allez être redirigé vers la page de connexion..."
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-else-if="error"
|
||||||
|
icon="i-lucide-alert-circle"
|
||||||
|
color="red"
|
||||||
|
variant="soft"
|
||||||
|
:title="error"
|
||||||
|
class="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UForm
|
||||||
|
v-if="!success"
|
||||||
|
:schema="schema"
|
||||||
|
:state="state"
|
||||||
|
class="space-y-6"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<UFormGroup label="Prénom" name="firstName" required>
|
||||||
|
<UInput
|
||||||
|
v-model="state.firstName"
|
||||||
|
placeholder="John"
|
||||||
|
icon="i-lucide-user"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Nom" name="lastName" required>
|
||||||
|
<UInput
|
||||||
|
v-model="state.lastName"
|
||||||
|
placeholder="Doe"
|
||||||
|
icon="i-lucide-user"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UFormGroup label="Entreprise" name="company" required>
|
||||||
|
<UInput
|
||||||
|
v-model="state.company"
|
||||||
|
placeholder="Mon Entreprise"
|
||||||
|
icon="i-lucide-building"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Email" name="email" required>
|
||||||
|
<UInput
|
||||||
|
v-model="state.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="votre@email.com"
|
||||||
|
icon="i-lucide-mail"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Mot de passe" name="password" required>
|
||||||
|
<UInput
|
||||||
|
v-model="state.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
icon="i-lucide-lock"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup label="Confirmer le mot de passe" name="confirmPassword" required>
|
||||||
|
<UInput
|
||||||
|
v-model="state.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
placeholder="••••••••"
|
||||||
|
icon="i-lucide-lock"
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UFormGroup name="terms" required>
|
||||||
|
<UCheckbox
|
||||||
|
v-model="state.terms"
|
||||||
|
name="terms"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
<span class="text-sm">
|
||||||
|
J'accepte les
|
||||||
|
<ULink to="/terms" class="text-primary hover:text-primary-600">
|
||||||
|
conditions d'utilisation
|
||||||
|
</ULink>
|
||||||
|
et la
|
||||||
|
<ULink to="/privacy" class="text-primary hover:text-primary-600">
|
||||||
|
politique de confidentialité
|
||||||
|
</ULink>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</UCheckbox>
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
type="submit"
|
||||||
|
:loading="pending"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
Créer mon compte
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<div v-if="!success" class="text-center">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Déjà un compte ?
|
||||||
|
<ULink
|
||||||
|
to="/login"
|
||||||
|
class="font-medium text-primary hover:text-primary-600"
|
||||||
|
>
|
||||||
|
Se connecter
|
||||||
|
</ULink>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Social Signup (Optional) -->
|
||||||
|
<div v-if="!success" class="mt-6">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 flex items-center">
|
||||||
|
<div class="w-full border-t border-gray-300 dark:border-gray-600" />
|
||||||
|
</div>
|
||||||
|
<div class="relative flex justify-center text-sm">
|
||||||
|
<span class="px-2 bg-white dark:bg-gray-900 text-gray-500">
|
||||||
|
Ou s'inscrire avec
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 grid grid-cols-2 gap-3">
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
<UIcon name="i-simple-icons-google" class="mr-2" />
|
||||||
|
Google
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
size="lg"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
<UIcon name="i-simple-icons-github" class="mr-2" />
|
||||||
|
GitHub
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side - Benefits -->
|
||||||
|
<div class="hidden lg:block relative flex-1">
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-br from-green-500 to-blue-600">
|
||||||
|
<div class="flex items-center justify-center h-full p-12">
|
||||||
|
<div class="text-center text-white">
|
||||||
|
<div class="mb-8">
|
||||||
|
<UIcon name="i-lucide-zap" size="80" class="mx-auto mb-4" />
|
||||||
|
<h2 class="text-4xl font-bold mb-4">
|
||||||
|
Commencez Gratuitement
|
||||||
|
</h2>
|
||||||
|
<p class="text-xl opacity-90">
|
||||||
|
Déployez votre première connexion Layer 2 en 5 minutes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-6 text-left max-w-md">
|
||||||
|
<div class="bg-white/10 backdrop-blur-sm rounded-lg p-4">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<UIcon name="i-lucide-gift" class="mr-3 text-yellow-300" />
|
||||||
|
<span class="font-semibold">Offre de Lancement</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm opacity-90">
|
||||||
|
Crédit de €100 offert pour vos premières connexions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white/10 backdrop-blur-sm rounded-lg p-4">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<UIcon name="i-lucide-clock" class="mr-3 text-blue-300" />
|
||||||
|
<span class="font-semibold">Déploiement Instantané</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm opacity-90">
|
||||||
|
Vos connexions Layer 2 prêtes en moins de 5 minutes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white/10 backdrop-blur-sm rounded-lg p-4">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<UIcon name="i-lucide-shield" class="mr-3 text-green-300" />
|
||||||
|
<span class="font-semibold">Sécurité Enterprise</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm opacity-90">
|
||||||
|
Connexions privées avec chiffrement de bout en bout
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white/10 backdrop-blur-sm rounded-lg p-4">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<UIcon name="i-lucide-headphones" class="mr-3 text-purple-300" />
|
||||||
|
<span class="font-semibold">Support 24/7</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm opacity-90">
|
||||||
|
Équipe d'experts disponible en permanence
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
60
app/types/index.d.ts
vendored
Normal file
60
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: Avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
7
app/utils/index.ts
Normal file
7
app/utils/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function randomInt(min: number, max: number): number {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomFrom<T>(array: T[]): T {
|
||||||
|
return array[Math.floor(Math.random() * array.length)]!
|
||||||
|
}
|
||||||
9
eslint.config.mjs
Normal file
9
eslint.config.mjs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// @ts-check
|
||||||
|
import withNuxt from './.nuxt/eslint.config.mjs'
|
||||||
|
|
||||||
|
export default withNuxt({
|
||||||
|
rules: {
|
||||||
|
'vue/no-multiple-template-root': 'off',
|
||||||
|
'vue/max-attributes-per-line': ['error', { singleline: 3 }]
|
||||||
|
}
|
||||||
|
})
|
||||||
35
nuxt.config.ts
Normal file
35
nuxt.config.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
modules: [
|
||||||
|
'@nuxt/eslint',
|
||||||
|
'@nuxt/ui-pro',
|
||||||
|
'@vueuse/nuxt'
|
||||||
|
],
|
||||||
|
|
||||||
|
devtools: {
|
||||||
|
enabled: true
|
||||||
|
},
|
||||||
|
|
||||||
|
css: ['~/assets/css/main.css'],
|
||||||
|
|
||||||
|
routeRules: {
|
||||||
|
'/api/**': {
|
||||||
|
cors: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
future: {
|
||||||
|
compatibilityVersion: 4
|
||||||
|
},
|
||||||
|
|
||||||
|
compatibilityDate: '2024-07-11',
|
||||||
|
|
||||||
|
eslint: {
|
||||||
|
config: {
|
||||||
|
stylistic: {
|
||||||
|
commaDangle: 'never',
|
||||||
|
braceStyle: '1tbs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
45
package.json
Normal file
45
package.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "nuxt-ui-pro-template-dashboard",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"typecheck": "nuxt typecheck"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.44",
|
||||||
|
"@iconify-json/simple-icons": "^1.2.35",
|
||||||
|
"@nuxt/ui-pro": "^3.1.3",
|
||||||
|
"@types/leaflet": "^1.9.18",
|
||||||
|
"@unovis/ts": "^1.5.2",
|
||||||
|
"@unovis/vue": "^1.5.2",
|
||||||
|
"@vueuse/nuxt": "^13.2.0",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"nuxt": "^3.17.5",
|
||||||
|
"zod": "^3.25.28"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nuxt/eslint": "^1.4.1",
|
||||||
|
"eslint": "^9.27.0",
|
||||||
|
"typescript": "^5.8.3",
|
||||||
|
"vue-tsc": "^2.2.10"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"unimport": "4.1.1"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"ignoredBuiltDependencies": [
|
||||||
|
"@parcel/watcher",
|
||||||
|
"esbuild",
|
||||||
|
"maplibre-gl",
|
||||||
|
"vue-demi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.11.0"
|
||||||
|
}
|
||||||
12142
pnpm-lock.yaml
generated
Normal file
12142
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
14
renovate.json
Normal file
14
renovate.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"github>nuxt/renovate-config-nuxt"
|
||||||
|
],
|
||||||
|
"lockFileMaintenance": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"baseBranches": ["v1", "main"],
|
||||||
|
"packageRules": [{
|
||||||
|
"matchDepTypes": ["resolutions"],
|
||||||
|
"enabled": false
|
||||||
|
}],
|
||||||
|
"postUpdateOptions": ["pnpmDedupe"]
|
||||||
|
}
|
||||||
187
server/api/customers.ts
Normal file
187
server/api/customers.ts
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import type { User } from '~/types'
|
||||||
|
|
||||||
|
const customers: User[] = [{
|
||||||
|
id: 1,
|
||||||
|
name: 'Alex Smith',
|
||||||
|
email: 'alex.smith@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=1'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'New York, USA'
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
name: 'Jordan Brown',
|
||||||
|
email: 'jordan.brown@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=2'
|
||||||
|
},
|
||||||
|
status: 'unsubscribed',
|
||||||
|
location: 'London, UK'
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
name: 'Taylor Green',
|
||||||
|
email: 'taylor.green@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=3'
|
||||||
|
},
|
||||||
|
status: 'bounced',
|
||||||
|
location: 'Paris, France'
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
name: 'Morgan White',
|
||||||
|
email: 'morgan.white@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=4'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'Berlin, Germany'
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
name: 'Casey Gray',
|
||||||
|
email: 'casey.gray@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=5'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'Tokyo, Japan'
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
name: 'Jamie Johnson',
|
||||||
|
email: 'jamie.johnson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=6'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'Sydney, Australia'
|
||||||
|
}, {
|
||||||
|
id: 7,
|
||||||
|
name: 'Riley Davis',
|
||||||
|
email: 'riley.davis@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=7'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'New York, USA'
|
||||||
|
}, {
|
||||||
|
id: 8,
|
||||||
|
name: 'Kelly Wilson',
|
||||||
|
email: 'kelly.wilson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=8'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'London, UK'
|
||||||
|
}, {
|
||||||
|
id: 9,
|
||||||
|
name: 'Drew Moore',
|
||||||
|
email: 'drew.moore@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=9'
|
||||||
|
},
|
||||||
|
status: 'bounced',
|
||||||
|
location: 'Paris, France'
|
||||||
|
}, {
|
||||||
|
id: 10,
|
||||||
|
name: 'Jordan Taylor',
|
||||||
|
email: 'jordan.taylor@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=10'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'Berlin, Germany'
|
||||||
|
}, {
|
||||||
|
id: 11,
|
||||||
|
name: 'Morgan Anderson',
|
||||||
|
email: 'morgan.anderson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=11'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'Tokyo, Japan'
|
||||||
|
}, {
|
||||||
|
id: 12,
|
||||||
|
name: 'Casey Thomas',
|
||||||
|
email: 'casey.thomas@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=12'
|
||||||
|
},
|
||||||
|
status: 'unsubscribed',
|
||||||
|
location: 'Sydney, Australia'
|
||||||
|
}, {
|
||||||
|
id: 13,
|
||||||
|
name: 'Jamie Jackson',
|
||||||
|
email: 'jamie.jackson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=13'
|
||||||
|
},
|
||||||
|
status: 'unsubscribed',
|
||||||
|
location: 'New York, USA'
|
||||||
|
}, {
|
||||||
|
id: 14,
|
||||||
|
name: 'Riley White',
|
||||||
|
email: 'riley.white@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=14'
|
||||||
|
},
|
||||||
|
status: 'unsubscribed',
|
||||||
|
location: 'London, UK'
|
||||||
|
}, {
|
||||||
|
id: 15,
|
||||||
|
name: 'Kelly Harris',
|
||||||
|
email: 'kelly.harris@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=15'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'Paris, France'
|
||||||
|
}, {
|
||||||
|
id: 16,
|
||||||
|
name: 'Drew Martin',
|
||||||
|
email: 'drew.martin@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=16'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'Berlin, Germany'
|
||||||
|
}, {
|
||||||
|
id: 17,
|
||||||
|
name: 'Alex Thompson',
|
||||||
|
email: 'alex.thompson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=17'
|
||||||
|
},
|
||||||
|
status: 'unsubscribed',
|
||||||
|
location: 'Tokyo, Japan'
|
||||||
|
}, {
|
||||||
|
id: 18,
|
||||||
|
name: 'Jordan Garcia',
|
||||||
|
email: 'jordan.garcia@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=18'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'Sydney, Australia'
|
||||||
|
}, {
|
||||||
|
id: 19,
|
||||||
|
name: 'Taylor Rodriguez',
|
||||||
|
email: 'taylor.rodriguez@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=19'
|
||||||
|
},
|
||||||
|
status: 'bounced',
|
||||||
|
location: 'New York, USA'
|
||||||
|
}, {
|
||||||
|
id: 20,
|
||||||
|
name: 'Morgan Lopez',
|
||||||
|
email: 'morgan.lopez@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=20'
|
||||||
|
},
|
||||||
|
status: 'subscribed',
|
||||||
|
location: 'London, UK'
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default eventHandler(async () => {
|
||||||
|
return customers
|
||||||
|
})
|
||||||
691
server/api/mails.ts
Normal file
691
server/api/mails.ts
Normal file
@ -0,0 +1,691 @@
|
|||||||
|
import { sub } from 'date-fns'
|
||||||
|
|
||||||
|
const mails = [{
|
||||||
|
id: 1,
|
||||||
|
from: {
|
||||||
|
name: 'Alex Smith',
|
||||||
|
email: 'alex.smith@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subject: 'Meeting Schedule: Q1 Marketing Strategy Review',
|
||||||
|
body: `Dear Team,
|
||||||
|
|
||||||
|
I hope this email finds you well. Just a quick reminder about our Q1 Marketing Strategy meeting scheduled for tomorrow at 10 AM EST in Conference Room A.
|
||||||
|
|
||||||
|
Agenda:
|
||||||
|
- Q4 Performance Review
|
||||||
|
- New Campaign Proposals
|
||||||
|
- Budget Allocation for Q2
|
||||||
|
- Team Resource Planning
|
||||||
|
|
||||||
|
Please come prepared with your department updates. I've attached the preliminary deck for your review.
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Alex Smith
|
||||||
|
Senior Marketing Director
|
||||||
|
Tel: (555) 123-4567`,
|
||||||
|
date: new Date().toISOString()
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
unread: true,
|
||||||
|
from: {
|
||||||
|
name: 'Jordan Brown',
|
||||||
|
email: 'jordan.brown@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subject: 'RE: Project Phoenix - Sprint 3 Update',
|
||||||
|
body: `Hi team,
|
||||||
|
|
||||||
|
Quick update on Sprint 3 deliverables:
|
||||||
|
|
||||||
|
✅ User authentication module completed
|
||||||
|
🏗️ Payment integration at 80%
|
||||||
|
⏳ API documentation pending review
|
||||||
|
|
||||||
|
Key metrics:
|
||||||
|
- Code coverage: 94%
|
||||||
|
- Sprint velocity: 45 points
|
||||||
|
- Bug resolution rate: 98%
|
||||||
|
|
||||||
|
Please review the attached report for detailed analysis. Let's discuss any blockers in tomorrow's stand-up.
|
||||||
|
|
||||||
|
Regards,
|
||||||
|
Jordan
|
||||||
|
|
||||||
|
--
|
||||||
|
Jordan Brown
|
||||||
|
Lead Developer | Tech Solutions
|
||||||
|
Mobile: +1 (555) 234-5678`,
|
||||||
|
date: sub(new Date(), { minutes: 7 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
unread: true,
|
||||||
|
from: {
|
||||||
|
name: 'Taylor Green',
|
||||||
|
email: 'taylor.green@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subject: 'Lunch Plans',
|
||||||
|
body: `Hi there!
|
||||||
|
|
||||||
|
I was wondering if you'd like to grab lunch this Friday? There's this amazing new Mexican restaurant downtown called "La Casa" that I've been wanting to try. They're known for their authentic tacos and house-made guacamole.
|
||||||
|
|
||||||
|
Would 12:30 PM work for you? It would be great to catch up and discuss the upcoming team building event while we're there.
|
||||||
|
|
||||||
|
Let me know what you think!
|
||||||
|
|
||||||
|
Best,
|
||||||
|
Taylor`,
|
||||||
|
date: sub(new Date(), { hours: 3 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
from: {
|
||||||
|
name: 'Morgan White',
|
||||||
|
email: 'morgan.white@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=4'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subject: 'New Proposal: Project Horizon',
|
||||||
|
body: `Hi team,
|
||||||
|
|
||||||
|
I've just uploaded the comprehensive proposal for Project Horizon to our shared drive. The document includes:
|
||||||
|
|
||||||
|
• Detailed project objectives and success metrics
|
||||||
|
• Resource allocation and team structure
|
||||||
|
• Timeline with key milestones
|
||||||
|
• Budget breakdown
|
||||||
|
• Risk assessment and mitigation strategies
|
||||||
|
|
||||||
|
I'm particularly excited about our innovative approach to the user engagement component, which could set a new standard for our industry.
|
||||||
|
|
||||||
|
Could you please review and provide feedback by EOD Friday? I'd like to present this to the steering committee next week.
|
||||||
|
|
||||||
|
Thanks in advance,
|
||||||
|
|
||||||
|
Morgan White
|
||||||
|
Senior Project Manager
|
||||||
|
Tel: (555) 234-5678`,
|
||||||
|
date: sub(new Date(), { days: 1 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
from: {
|
||||||
|
name: 'Casey Gray',
|
||||||
|
email: 'casey.gray@example.com'
|
||||||
|
},
|
||||||
|
subject: 'Updated: San Francisco Conference Trip Itinerary',
|
||||||
|
body: `Dear [Name],
|
||||||
|
|
||||||
|
Please find your confirmed travel itinerary below:
|
||||||
|
|
||||||
|
FLIGHT DETAILS:
|
||||||
|
Outbound: AA 1234
|
||||||
|
Date: March 15, 2024
|
||||||
|
DEP: JFK 09:30 AM
|
||||||
|
ARR: SFO 12:45 PM
|
||||||
|
|
||||||
|
HOTEL:
|
||||||
|
Marriott San Francisco
|
||||||
|
Check-in: March 15
|
||||||
|
Check-out: March 18
|
||||||
|
Confirmation #: MR123456
|
||||||
|
|
||||||
|
SCHEDULE:
|
||||||
|
March 15 - Evening: Welcome Reception (6 PM)
|
||||||
|
March 16 - Conference Day 1 (9 AM - 5 PM)
|
||||||
|
March 17 - Conference Day 2 (9 AM - 4 PM)
|
||||||
|
|
||||||
|
Please let me know if you need any modifications.
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Casey Gray
|
||||||
|
Travel Coordinator
|
||||||
|
Office: (555) 345-6789`,
|
||||||
|
date: sub(new Date(), { days: 1 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
from: {
|
||||||
|
name: 'Jamie Johnson',
|
||||||
|
email: 'jamie.johnson@example.com'
|
||||||
|
},
|
||||||
|
subject: 'Q1 2024 Financial Performance Review',
|
||||||
|
body: `Dear Leadership Team,
|
||||||
|
|
||||||
|
Please find attached our Q1 2024 financial analysis report. Key highlights:
|
||||||
|
|
||||||
|
PERFORMANCE METRICS:
|
||||||
|
• Revenue: $12.4M (+15% YoY)
|
||||||
|
• Operating Expenses: $8.2M (-3% vs. budget)
|
||||||
|
• Net Profit Margin: 18.5% (+2.5% vs. Q4 2023)
|
||||||
|
|
||||||
|
AREAS OF OPTIMIZATION:
|
||||||
|
1. Cloud infrastructure costs (+22% over budget)
|
||||||
|
2. Marketing spend efficiency (-8% ROI vs. target)
|
||||||
|
3. Office operational costs (+5% vs. forecast)
|
||||||
|
|
||||||
|
I've scheduled a detailed review for Thursday at 2 PM EST. Calendar invite to follow.
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Jamie Johnson
|
||||||
|
Chief Financial Officer
|
||||||
|
Ext: 4567`,
|
||||||
|
date: sub(new Date(), { days: 2 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 7,
|
||||||
|
from: {
|
||||||
|
name: 'Riley Davis',
|
||||||
|
email: 'riley.davis@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=7'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subject: '[Mandatory] New DevOps Tools Training Session',
|
||||||
|
body: `Hello Development Team,
|
||||||
|
|
||||||
|
This is a reminder about next week's mandatory training session on our updated DevOps toolkit.
|
||||||
|
|
||||||
|
📅 Date: Tuesday, March 19
|
||||||
|
⏰ Time: 10:00 AM - 12:30 PM EST
|
||||||
|
📍 Location: Virtual (Zoom link below)
|
||||||
|
|
||||||
|
We'll be covering:
|
||||||
|
• GitLab CI/CD pipeline improvements
|
||||||
|
• Docker container optimization
|
||||||
|
• Kubernetes cluster management
|
||||||
|
• New monitoring tools integration
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
1. Install Docker Desktop 4.25
|
||||||
|
2. Update VS Code to latest version
|
||||||
|
3. Complete pre-training survey (link attached)
|
||||||
|
|
||||||
|
Zoom Link: https://zoom.us/j/123456789
|
||||||
|
Password: DevOps2024
|
||||||
|
|
||||||
|
--
|
||||||
|
Riley Davis
|
||||||
|
DevOps Lead
|
||||||
|
Technical Operations
|
||||||
|
M: (555) 777-8888`,
|
||||||
|
date: sub(new Date(), { days: 2 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 8,
|
||||||
|
unread: true,
|
||||||
|
from: {
|
||||||
|
name: 'Kelly Wilson',
|
||||||
|
email: 'kelly.wilson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=8'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
subject: '🎉 Happy Birthday!',
|
||||||
|
body: `Dear [Name],
|
||||||
|
|
||||||
|
On behalf of the entire team, wishing you a fantastic birthday! 🎂
|
||||||
|
|
||||||
|
We've organized a small celebration in the break room at 3 PM today. Cake and refreshments will be served!
|
||||||
|
|
||||||
|
Your dedication and positive energy make our workplace better every day. Here's to another great year ahead!
|
||||||
|
|
||||||
|
Best wishes,
|
||||||
|
Kelly & The HR Team
|
||||||
|
|
||||||
|
P.S. Don't forget to check your email for a special birthday surprise from the company! 🎁
|
||||||
|
|
||||||
|
--
|
||||||
|
Kelly Wilson
|
||||||
|
HR Director
|
||||||
|
Human Resources Department
|
||||||
|
Tel: (555) 999-0000`,
|
||||||
|
date: sub(new Date(), { days: 2 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 9,
|
||||||
|
from: {
|
||||||
|
name: 'Drew Moore',
|
||||||
|
email: 'drew.moore@example.com'
|
||||||
|
},
|
||||||
|
subject: 'Website Redesign Feedback Request - Phase 2',
|
||||||
|
body: `Hi there,
|
||||||
|
|
||||||
|
We're entering Phase 2 of our website redesign project and would value your input on the latest iterations.
|
||||||
|
|
||||||
|
New Features Implementation:
|
||||||
|
1. Dynamic product catalog
|
||||||
|
2. Enhanced search functionality
|
||||||
|
3. Personalized user dashboard
|
||||||
|
4. Mobile-responsive navigation
|
||||||
|
|
||||||
|
Review Links:
|
||||||
|
• Staging Environment: https://staging.example.com
|
||||||
|
• Design Specs: [Figma Link]
|
||||||
|
• User Flow Documentation: [Confluence Link]
|
||||||
|
|
||||||
|
Please provide feedback by EOD Friday. Key areas to focus on:
|
||||||
|
- User experience
|
||||||
|
- Navigation flow
|
||||||
|
- Content hierarchy
|
||||||
|
- Mobile responsiveness
|
||||||
|
|
||||||
|
Your insights will be crucial for our final implementation decisions.
|
||||||
|
|
||||||
|
Thanks in advance,
|
||||||
|
Drew Moore
|
||||||
|
UX Design Lead
|
||||||
|
Product Design Team`,
|
||||||
|
date: sub(new Date(), { days: 5 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 10,
|
||||||
|
from: {
|
||||||
|
name: 'Jordan Taylor',
|
||||||
|
email: 'jordan.taylor@example.com'
|
||||||
|
},
|
||||||
|
subject: 'Corporate Wellness Program - Membership Renewal',
|
||||||
|
body: `Dear Valued Member,
|
||||||
|
|
||||||
|
Your corporate wellness program membership is due for renewal on April 1st, 2024.
|
||||||
|
|
||||||
|
NEW AMENITIES:
|
||||||
|
✨ Expanded yoga studio
|
||||||
|
🏋️ State-of-the-art cardio equipment
|
||||||
|
🧘 Meditation room
|
||||||
|
👥 Additional group fitness classes
|
||||||
|
|
||||||
|
RENEWAL BENEFITS:
|
||||||
|
• 15% early bird discount
|
||||||
|
• 3 complimentary personal training sessions
|
||||||
|
• Free wellness assessment
|
||||||
|
• Access to new mobile app
|
||||||
|
|
||||||
|
To schedule a tour or discuss renewal options, please book a time here: [Booking Link]
|
||||||
|
|
||||||
|
Stay healthy!
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Jordan Taylor
|
||||||
|
Corporate Wellness Coordinator
|
||||||
|
Downtown Fitness Center
|
||||||
|
Tel: (555) 123-7890`,
|
||||||
|
date: sub(new Date(), { days: 5 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 11,
|
||||||
|
unread: true,
|
||||||
|
from: {
|
||||||
|
name: 'Morgan Anderson',
|
||||||
|
email: 'morgan.anderson@example.com'
|
||||||
|
},
|
||||||
|
subject: 'Important: Updates to Your Corporate Insurance Policy',
|
||||||
|
body: `Dear [Employee Name],
|
||||||
|
|
||||||
|
This email contains important information about changes to your corporate insurance coverage effective April 1, 2024.
|
||||||
|
|
||||||
|
KEY UPDATES:
|
||||||
|
1. Health Insurance
|
||||||
|
• Reduced co-pay for specialist visits ($35 → $25)
|
||||||
|
• Extended telehealth coverage
|
||||||
|
• New mental health benefits
|
||||||
|
|
||||||
|
2. Dental Coverage
|
||||||
|
• Increased annual maximum ($1,500 → $2,000)
|
||||||
|
• Added orthodontic coverage for dependents
|
||||||
|
|
||||||
|
3. Vision Benefits
|
||||||
|
• Enhanced frame allowance
|
||||||
|
• New LASIK discount program
|
||||||
|
|
||||||
|
Please review the attached documentation carefully and complete the acknowledgment form by March 25th.
|
||||||
|
|
||||||
|
Questions? Join our virtual info session:
|
||||||
|
📅 March 20th, 2024
|
||||||
|
⏰ 11:00 AM EST
|
||||||
|
🔗 [Teams Link]
|
||||||
|
|
||||||
|
Regards,
|
||||||
|
Morgan Anderson
|
||||||
|
Benefits Coordinator
|
||||||
|
HR Department`,
|
||||||
|
date: sub(new Date(), { days: 12 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 12,
|
||||||
|
from: {
|
||||||
|
name: 'Casey Thomas',
|
||||||
|
email: 'casey.thomas@example.com'
|
||||||
|
},
|
||||||
|
subject: '📚 March Book Club Meeting: "The Great Gatsby"',
|
||||||
|
body: `Hello Book Lovers!
|
||||||
|
|
||||||
|
I hope you're enjoying F. Scott Fitzgerald's masterpiece! Our next meeting details:
|
||||||
|
|
||||||
|
📅 Thursday, March 21st
|
||||||
|
⏰ 5:30 PM - 7:00 PM
|
||||||
|
📍 Main Conference Room (or Zoom)
|
||||||
|
|
||||||
|
Discussion Topics:
|
||||||
|
1. Symbolism of the green light
|
||||||
|
2. The American Dream theme
|
||||||
|
3. Character development
|
||||||
|
4. Social commentary
|
||||||
|
|
||||||
|
Please bring your suggestions for April's book selection!
|
||||||
|
|
||||||
|
Refreshments will be provided 🍪
|
||||||
|
|
||||||
|
RSVP by replying to this email.
|
||||||
|
|
||||||
|
Happy reading!
|
||||||
|
Casey
|
||||||
|
|
||||||
|
--
|
||||||
|
Casey Thomas
|
||||||
|
Book Club Coordinator
|
||||||
|
Internal Culture Committee`,
|
||||||
|
date: sub(new Date(), { months: 1 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 13,
|
||||||
|
from: {
|
||||||
|
name: 'Jamie Jackson',
|
||||||
|
email: 'jamie.jackson@example.com'
|
||||||
|
},
|
||||||
|
subject: '🍳 Company Cookbook Project - Recipe Submission Reminder',
|
||||||
|
body: `Dear Colleagues,
|
||||||
|
|
||||||
|
Final call for our company cookbook project submissions!
|
||||||
|
|
||||||
|
Guidelines for Recipe Submission:
|
||||||
|
1. Include ingredients list with measurements
|
||||||
|
2. Step-by-step instructions
|
||||||
|
3. Cooking time and servings
|
||||||
|
4. Photo of the finished dish (optional)
|
||||||
|
5. Any cultural or personal significance
|
||||||
|
|
||||||
|
Submission Deadline: March 22nd, 2024
|
||||||
|
|
||||||
|
We already have some amazing entries:
|
||||||
|
• Sarah's Famous Chili
|
||||||
|
• Mike's Mediterranean Pasta
|
||||||
|
• Lisa's Vegan Brownies
|
||||||
|
• Tom's Family Paella
|
||||||
|
|
||||||
|
All proceeds from cookbook sales will support our local food bank.
|
||||||
|
|
||||||
|
Submit here: [Form Link]
|
||||||
|
|
||||||
|
Cooking together,
|
||||||
|
Jamie Jackson
|
||||||
|
Community Engagement Committee
|
||||||
|
Ext. 5432`,
|
||||||
|
date: sub(new Date(), { months: 1 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 14,
|
||||||
|
from: {
|
||||||
|
name: 'Riley White',
|
||||||
|
email: 'riley.white@example.com'
|
||||||
|
},
|
||||||
|
subject: '🧘♀️ Updated Corporate Wellness Schedule - Spring 2024',
|
||||||
|
body: `Dear Wellness Program Participants,
|
||||||
|
|
||||||
|
Our Spring 2024 wellness schedule is now available!
|
||||||
|
|
||||||
|
NEW CLASSES:
|
||||||
|
Monday:
|
||||||
|
• 7:30 AM - Morning Flow Yoga
|
||||||
|
• 12:15 PM - HIIT Express
|
||||||
|
• 5:30 PM - Meditation Basics
|
||||||
|
|
||||||
|
Wednesday:
|
||||||
|
• 8:00 AM - Power Vinyasa
|
||||||
|
• 12:00 PM - Desk Stretching
|
||||||
|
• 4:30 PM - Mindfulness Workshop
|
||||||
|
|
||||||
|
Friday:
|
||||||
|
• 7:45 AM - Gentle Yoga
|
||||||
|
• 12:30 PM - Stress Management
|
||||||
|
• 4:45 PM - Weekend Wind-Down
|
||||||
|
|
||||||
|
All classes available in-person and via Zoom.
|
||||||
|
Download our app to reserve your spot!
|
||||||
|
|
||||||
|
Namaste,
|
||||||
|
Riley White
|
||||||
|
Corporate Wellness Instructor
|
||||||
|
Wellness & Benefits Team`,
|
||||||
|
date: sub(new Date(), { months: 1 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 15,
|
||||||
|
from: {
|
||||||
|
name: 'Kelly Harris',
|
||||||
|
email: 'kelly.harris@example.com'
|
||||||
|
},
|
||||||
|
subject: '📚 Book Launch Event: "Digital Transformation in the Modern Age"',
|
||||||
|
body: `Dear [Name],
|
||||||
|
|
||||||
|
You're cordially invited to the launch of my new book, "Digital Transformation in the Modern Age: A Leadership Guide"
|
||||||
|
|
||||||
|
EVENT DETAILS:
|
||||||
|
📅 Date: April 15th, 2024
|
||||||
|
⏰ Time: 6:00 PM - 8:30 PM EST
|
||||||
|
📍 Grand Hotel Downtown
|
||||||
|
123 Business Ave.
|
||||||
|
|
||||||
|
AGENDA:
|
||||||
|
6:00 PM - Welcome Reception
|
||||||
|
6:30 PM - Keynote Presentation
|
||||||
|
7:15 PM - Q&A Session
|
||||||
|
7:45 PM - Book Signing
|
||||||
|
8:00 PM - Networking
|
||||||
|
|
||||||
|
Light refreshments will be served.
|
||||||
|
Each attendee will receive a signed copy of the book.
|
||||||
|
|
||||||
|
RSVP by April 1st: [Event Link]
|
||||||
|
|
||||||
|
Looking forward to sharing this milestone with you!
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Kelly Harris
|
||||||
|
Digital Strategy Consultant
|
||||||
|
Author, "Digital Transformation in the Modern Age"`,
|
||||||
|
date: sub(new Date(), { months: 1 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 16,
|
||||||
|
from: {
|
||||||
|
name: 'Drew Martin',
|
||||||
|
email: 'drew.martin@example.com'
|
||||||
|
},
|
||||||
|
subject: '🚀 TechCon 2024: Early Bird Registration Now Open',
|
||||||
|
body: `Dear Tech Enthusiasts,
|
||||||
|
|
||||||
|
Registration is now open for TechCon 2024: "Innovation at Scale"
|
||||||
|
|
||||||
|
CONFERENCE HIGHLIGHTS:
|
||||||
|
📅 May 15-17, 2024
|
||||||
|
📍 Tech Convention Center
|
||||||
|
|
||||||
|
KEYNOTE SPEAKERS:
|
||||||
|
• Sarah Johnson - CEO, Future Tech Inc.
|
||||||
|
• Dr. Michael Chang - AI Research Director
|
||||||
|
• Lisa Rodriguez - Cybersecurity Expert
|
||||||
|
|
||||||
|
TRACKS:
|
||||||
|
1. AI/ML Innovation
|
||||||
|
2. Cloud Architecture
|
||||||
|
3. DevSecOps
|
||||||
|
4. Digital Transformation
|
||||||
|
5. Emerging Technologies
|
||||||
|
|
||||||
|
EARLY BIRD PRICING (ends April 1):
|
||||||
|
Full Conference Pass: $899 (reg. $1,199)
|
||||||
|
Team Discount (5+): 15% off
|
||||||
|
|
||||||
|
Register here: [Registration Link]
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Drew Martin
|
||||||
|
Conference Director
|
||||||
|
TechCon 2024`,
|
||||||
|
date: sub(new Date(), { months: 1, days: 4 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 17,
|
||||||
|
from: {
|
||||||
|
name: 'Alex Thompson',
|
||||||
|
email: 'alex.thompson@example.com'
|
||||||
|
},
|
||||||
|
subject: '🎨 Modern Perspectives: Contemporary Art Exhibition',
|
||||||
|
body: `Hi there,
|
||||||
|
|
||||||
|
Hope you're well! I wanted to personally invite you to an extraordinary art exhibition this weekend.
|
||||||
|
|
||||||
|
"Modern Perspectives: Breaking Boundaries"
|
||||||
|
📅 Saturday & Sunday
|
||||||
|
⏰ 10 AM - 6 PM
|
||||||
|
📍 Metropolitan Art Gallery
|
||||||
|
|
||||||
|
FEATURED ARTISTS:
|
||||||
|
• Maria Chen - Mixed Media
|
||||||
|
• James Wright - Digital Art
|
||||||
|
• Sofia Patel - Installation
|
||||||
|
• Robert Kim - Photography
|
||||||
|
|
||||||
|
SPECIAL EVENTS:
|
||||||
|
• Artist Talk: Saturday, 2 PM
|
||||||
|
• Workshop: Sunday, 11 AM
|
||||||
|
• Wine Reception: Saturday, 5 PM
|
||||||
|
|
||||||
|
Would love to meet you there! Let me know if you'd like to go together.
|
||||||
|
|
||||||
|
Best,
|
||||||
|
Alex Thompson
|
||||||
|
Curator
|
||||||
|
Metropolitan Art Gallery
|
||||||
|
Tel: (555) 234-5678`,
|
||||||
|
date: sub(new Date(), { months: 1, days: 15 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 18,
|
||||||
|
from: {
|
||||||
|
name: 'Jordan Garcia',
|
||||||
|
email: 'jordan.garcia@example.com'
|
||||||
|
},
|
||||||
|
subject: '🤝 Industry Networking Event: "Connect & Innovate 2024"',
|
||||||
|
body: `Dear Professional Network,
|
||||||
|
|
||||||
|
You're invited to our premier networking event!
|
||||||
|
|
||||||
|
EVENT DETAILS:
|
||||||
|
📅 March 28th, 2024
|
||||||
|
⏰ 6:00 PM - 9:00 PM
|
||||||
|
📍 Innovation Hub
|
||||||
|
456 Enterprise Street
|
||||||
|
|
||||||
|
SPEAKERS:
|
||||||
|
• Mark Thompson - "Future of Work"
|
||||||
|
• Dr. Sarah Chen - "Innovation Trends"
|
||||||
|
• Robert Mills - "Digital Leadership"
|
||||||
|
|
||||||
|
SCHEDULE:
|
||||||
|
6:00 - Registration & Welcome
|
||||||
|
6:30 - Keynote Presentations
|
||||||
|
7:30 - Networking Session
|
||||||
|
8:30 - Panel Discussion
|
||||||
|
|
||||||
|
Complimentary hors d'oeuvres and beverages will be served.
|
||||||
|
|
||||||
|
RSVP Required: [Registration Link]
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
Jordan Garcia
|
||||||
|
Event Coordinator
|
||||||
|
Professional Networking Association`,
|
||||||
|
date: sub(new Date(), { months: 1, days: 18 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 19,
|
||||||
|
from: {
|
||||||
|
name: 'Taylor Rodriguez',
|
||||||
|
email: 'taylor.rodriguez@example.com'
|
||||||
|
},
|
||||||
|
subject: '🌟 Community Service Day - Volunteer Opportunities',
|
||||||
|
body: `Dear Colleagues,
|
||||||
|
|
||||||
|
Join us for our annual Community Service Day!
|
||||||
|
|
||||||
|
EVENT DETAILS:
|
||||||
|
📅 Saturday, April 6th, 2024
|
||||||
|
⏰ 9:00 AM - 3:00 PM
|
||||||
|
📍 Multiple Locations
|
||||||
|
|
||||||
|
VOLUNTEER OPPORTUNITIES:
|
||||||
|
1. City Park Cleanup
|
||||||
|
• Garden maintenance
|
||||||
|
• Trail restoration
|
||||||
|
• Playground repair
|
||||||
|
|
||||||
|
2. Food Bank
|
||||||
|
• Sorting donations
|
||||||
|
• Packing meals
|
||||||
|
• Distribution
|
||||||
|
|
||||||
|
3. Animal Shelter
|
||||||
|
• Dog walking
|
||||||
|
• Facility cleaning
|
||||||
|
• Social media support
|
||||||
|
|
||||||
|
All volunteers receive:
|
||||||
|
• Company volunteer t-shirt
|
||||||
|
• Lunch and refreshments
|
||||||
|
• Certificate of participation
|
||||||
|
• 8 hours community service credit
|
||||||
|
|
||||||
|
Sign up here: [Volunteer Portal]
|
||||||
|
|
||||||
|
Making a difference together,
|
||||||
|
Taylor Rodriguez
|
||||||
|
Community Outreach Coordinator
|
||||||
|
Corporate Social Responsibility Team`,
|
||||||
|
date: sub(new Date(), { months: 1, days: 25 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 20,
|
||||||
|
from: {
|
||||||
|
name: 'Morgan Lopez',
|
||||||
|
email: 'morgan.lopez@example.com'
|
||||||
|
},
|
||||||
|
subject: '🚗 Vehicle Maintenance Reminder: 30,000 Mile Service',
|
||||||
|
body: `Dear Valued Customer,
|
||||||
|
|
||||||
|
Your vehicle is due for its 30,000-mile maintenance service.
|
||||||
|
|
||||||
|
RECOMMENDED SERVICES:
|
||||||
|
• Oil and filter change
|
||||||
|
• Tire rotation and alignment
|
||||||
|
• Brake system inspection
|
||||||
|
• Multi-point safety inspection
|
||||||
|
• Fluid level check and top-off
|
||||||
|
• Battery performance test
|
||||||
|
|
||||||
|
SERVICE CENTER DETAILS:
|
||||||
|
📍 Downtown Auto Care
|
||||||
|
789 Service Road
|
||||||
|
|
||||||
|
☎️ (555) 987-6543
|
||||||
|
|
||||||
|
Available Appointments:
|
||||||
|
• Monday-Friday: 7:30 AM - 6:00 PM
|
||||||
|
• Saturday: 8:00 AM - 2:00 PM
|
||||||
|
|
||||||
|
Schedule online: [Booking Link]
|
||||||
|
or call our service desk directly.
|
||||||
|
|
||||||
|
Drive safely,
|
||||||
|
Morgan Lopez
|
||||||
|
Service Coordinator
|
||||||
|
Downtown Auto Care
|
||||||
|
Emergency: (555) 987-6544`,
|
||||||
|
date: sub(new Date(), { months: 2 }).toISOString()
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default eventHandler(async () => {
|
||||||
|
return mails
|
||||||
|
})
|
||||||
60
server/api/members.ts
Normal file
60
server/api/members.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
const members = [{
|
||||||
|
name: 'Anthony Fu',
|
||||||
|
username: 'antfu',
|
||||||
|
role: 'member',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/antfu' }
|
||||||
|
}, {
|
||||||
|
name: 'Baptiste Leproux',
|
||||||
|
username: 'larbish',
|
||||||
|
role: 'member',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/larbish' }
|
||||||
|
}, {
|
||||||
|
name: 'Benjamin Canac',
|
||||||
|
username: 'benjamincanac',
|
||||||
|
role: 'owner',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/benjamincanac' }
|
||||||
|
}, {
|
||||||
|
name: 'Céline Dumerc',
|
||||||
|
username: 'celinedumerc',
|
||||||
|
role: 'member',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/celinedumerc' }
|
||||||
|
}, {
|
||||||
|
name: 'Daniel Roe',
|
||||||
|
username: 'danielroe',
|
||||||
|
role: 'member',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/danielroe' }
|
||||||
|
}, {
|
||||||
|
name: 'Farnabaz',
|
||||||
|
username: 'farnabaz',
|
||||||
|
role: 'member',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/farnabaz' }
|
||||||
|
}, {
|
||||||
|
name: 'Ferdinand Coumau',
|
||||||
|
username: 'FerdinandCoumau',
|
||||||
|
role: 'member',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/FerdinandCoumau' }
|
||||||
|
}, {
|
||||||
|
name: 'Hugo Richard',
|
||||||
|
username: 'hugorcd',
|
||||||
|
role: 'owner',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/hugorcd' }
|
||||||
|
}, {
|
||||||
|
name: 'Pooya Parsa',
|
||||||
|
username: 'pi0',
|
||||||
|
role: 'member',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/pi0' }
|
||||||
|
}, {
|
||||||
|
name: 'Sarah Moriceau',
|
||||||
|
username: 'SarahM19',
|
||||||
|
role: 'member',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/SarahM19' }
|
||||||
|
}, {
|
||||||
|
name: 'Sébastien Chopin',
|
||||||
|
username: 'Atinux',
|
||||||
|
role: 'owner',
|
||||||
|
avatar: { src: 'https://ipx.nuxt.com/f_auto,s_192x192/gh_avatar/atinux' }
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default eventHandler(async () => {
|
||||||
|
return members
|
||||||
|
})
|
||||||
256
server/api/notifications.ts
Normal file
256
server/api/notifications.ts
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import { sub } from 'date-fns'
|
||||||
|
|
||||||
|
const notifications = [{
|
||||||
|
id: 1,
|
||||||
|
unread: true,
|
||||||
|
sender: {
|
||||||
|
name: 'Jordan Brown',
|
||||||
|
email: 'jordan.brown@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'sent you a message',
|
||||||
|
date: sub(new Date(), { minutes: 7 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 2,
|
||||||
|
sender: {
|
||||||
|
name: 'Lindsay Walton'
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { hours: 1 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 3,
|
||||||
|
unread: true,
|
||||||
|
sender: {
|
||||||
|
name: 'Taylor Green',
|
||||||
|
email: 'taylor.green@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'sent you a message',
|
||||||
|
date: sub(new Date(), { hours: 3 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 4,
|
||||||
|
sender: {
|
||||||
|
name: 'Courtney Henry',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=4'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'added you to a project',
|
||||||
|
date: sub(new Date(), { hours: 3 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 5,
|
||||||
|
sender: {
|
||||||
|
name: 'Tom Cook',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=5'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'abandonned cart',
|
||||||
|
date: sub(new Date(), { hours: 7 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 6,
|
||||||
|
sender: {
|
||||||
|
name: 'Casey Thomas',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=6'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'purchased your product',
|
||||||
|
date: sub(new Date(), { days: 1, hours: 3 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 7,
|
||||||
|
unread: true,
|
||||||
|
sender: {
|
||||||
|
name: 'Kelly Wilson',
|
||||||
|
email: 'kelly.wilson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=8'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'sent you a message',
|
||||||
|
date: sub(new Date(), { days: 2 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 8,
|
||||||
|
sender: {
|
||||||
|
name: 'Jamie Johnson',
|
||||||
|
email: 'jamie.johnson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=9'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'requested a refund',
|
||||||
|
date: sub(new Date(), { days: 5, hours: 4 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 9,
|
||||||
|
unread: true,
|
||||||
|
sender: {
|
||||||
|
name: 'Morgan Anderson',
|
||||||
|
email: 'morgan.anderson@example.com'
|
||||||
|
},
|
||||||
|
body: 'sent you a message',
|
||||||
|
date: sub(new Date(), { days: 6 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 10,
|
||||||
|
sender: {
|
||||||
|
name: 'Drew Moore'
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 6 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 11,
|
||||||
|
sender: {
|
||||||
|
name: 'Riley Davis'
|
||||||
|
},
|
||||||
|
body: 'abandonned cart',
|
||||||
|
date: sub(new Date(), { days: 7 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 12,
|
||||||
|
sender: {
|
||||||
|
name: 'Jordan Taylor'
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 9 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 13,
|
||||||
|
sender: {
|
||||||
|
name: 'Kelly Wilson',
|
||||||
|
email: 'kelly.wilson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=8'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 10 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 14,
|
||||||
|
sender: {
|
||||||
|
name: 'Jamie Johnson',
|
||||||
|
email: 'jamie.johnson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=9'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 11 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 15,
|
||||||
|
sender: {
|
||||||
|
name: 'Morgan Anderson'
|
||||||
|
},
|
||||||
|
body: 'purchased your product',
|
||||||
|
date: sub(new Date(), { days: 12 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 16,
|
||||||
|
sender: {
|
||||||
|
name: 'Drew Moore',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=16'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 13 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 17,
|
||||||
|
sender: {
|
||||||
|
name: 'Riley Davis'
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 14 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 18,
|
||||||
|
sender: {
|
||||||
|
name: 'Jordan Taylor'
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 15 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 19,
|
||||||
|
sender: {
|
||||||
|
name: 'Kelly Wilson',
|
||||||
|
email: 'kelly.wilson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=8'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 16 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 20,
|
||||||
|
sender: {
|
||||||
|
name: 'Jamie Johnson',
|
||||||
|
email: 'jamie.johnson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=9'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'purchased your product',
|
||||||
|
date: sub(new Date(), { days: 17 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 21,
|
||||||
|
sender: {
|
||||||
|
name: 'Morgan Anderson'
|
||||||
|
},
|
||||||
|
body: 'abandonned cart',
|
||||||
|
date: sub(new Date(), { days: 17 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 22,
|
||||||
|
sender: {
|
||||||
|
name: 'Drew Moore'
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 18 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 23,
|
||||||
|
sender: {
|
||||||
|
name: 'Riley Davis'
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 19 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 24,
|
||||||
|
sender: {
|
||||||
|
name: 'Jordan Taylor',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=24'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 20 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 25,
|
||||||
|
sender: {
|
||||||
|
name: 'Kelly Wilson',
|
||||||
|
email: 'kelly.wilson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=8'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 20 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 26,
|
||||||
|
sender: {
|
||||||
|
name: 'Jamie Johnson',
|
||||||
|
email: 'jamie.johnson@example.com',
|
||||||
|
avatar: {
|
||||||
|
src: 'https://i.pravatar.cc/128?u=9'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
body: 'abandonned cart',
|
||||||
|
date: sub(new Date(), { days: 21 }).toISOString()
|
||||||
|
}, {
|
||||||
|
id: 27,
|
||||||
|
sender: {
|
||||||
|
name: 'Morgan Anderson'
|
||||||
|
},
|
||||||
|
body: 'subscribed to your email list',
|
||||||
|
date: sub(new Date(), { days: 22 }).toISOString()
|
||||||
|
}]
|
||||||
|
|
||||||
|
export default eventHandler(async () => {
|
||||||
|
return notifications
|
||||||
|
})
|
||||||
4
tsconfig.json
Normal file
4
tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user