This commit is contained in:
jletienne 2025-06-06 23:53:28 +02:00
commit a1c2fac4b8
55 changed files with 19665 additions and 0 deletions

13
.editorconfig Normal file
View 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
View 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
View 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

1
.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

63
README.md Normal file
View File

@ -0,0 +1,63 @@
# Nuxt Dashboard Template
[![Nuxt UI Pro](https://img.shields.io/badge/Made%20with-Nuxt%20UI%20Pro-00DC82?logo=nuxt&labelColor=020420)](https://ui.nuxt.com/pro)
[![Deploy to NuxtHub](https://img.shields.io/badge/Deploy%20to-NuxtHub-00DC82?logo=nuxt&labelColor=020420)](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
View File

@ -0,0 +1,8 @@
export default defineAppConfig({
ui: {
colors: {
primary: 'green',
neutral: 'zinc'
}
}
})

42
app/app.vue Normal file
View 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
View 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;
}

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

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

24
app/error.vue Normal file
View 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
View 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
View 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 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>

View 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">
&lt; 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>

View 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>

View 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>

View 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">
&lt; 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
View 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
View 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
View 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
View 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>

View 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>

View 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
View 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>

View 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>

View 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>

View 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>

View 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
View 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
View File

@ -0,0 +1,60 @@
import type { AvatarProps } from '@nuxt/ui'
export type UserStatus = 'subscribed' | 'unsubscribed' | 'bounced'
export type SaleStatus = 'paid' | 'failed' | 'refunded'
export interface User {
id: number
name: string
email: string
avatar?: AvatarProps
status: UserStatus
location: string
}
export interface Mail {
id: number
unread?: boolean
from: User
subject: string
body: string
date: string
}
export interface Member {
name: string
username: string
role: 'member' | 'owner'
avatar: 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

14
renovate.json Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}