diff --git a/app.config.ts b/app.config.ts index 2db671c0c13087b4a7a3f87340637409a1913b99..d1630e5b59a00987386673bc48b2794c65d90cae 100644 --- a/app.config.ts +++ b/app.config.ts @@ -28,6 +28,19 @@ export default defineAppConfig({ success: '#00aa00', } } + }, + toaster: { + position: 'bottom-right' as const, + expand: true, + duration: 5000 + }, + ui: { + colors: { + primary: 'primary', + secondary: 'secondary', + success: 'success', + error: 'error', + } } }) diff --git a/app.vue b/app.vue index 8cf5a1a9b6a7a6ac80355ddb6dafe15532d13613..90d2c9e41fa766624399fe7501fe90f1c0afe668 100644 --- a/app.vue +++ b/app.vue @@ -1,4 +1,6 @@ <script setup lang="ts"> +import appConfig from "~/app.config"; + useHead({ titleTemplate: (titleChunk) => { return titleChunk ? `3SPA | ${titleChunk}` : '3SPA'; @@ -8,7 +10,7 @@ useHead({ <template> <div> - <UApp> + <UApp :toaster="appConfig.toaster"> <NuxtLayout> <NuxtPage/> </NuxtLayout> diff --git a/assets/css/colors.css b/assets/css/colors.css new file mode 100644 index 0000000000000000000000000000000000000000..d2b7c48f9b602eb6914ea8df7accaca082bfe01e --- /dev/null +++ b/assets/css/colors.css @@ -0,0 +1,65 @@ +@import "tailwindcss"; + +@theme { + /*created with https://www.tailwindshades.com/*/ + + --color-primary-50: #9DF5D8; + --color-primary-100: #8BF3D0; + --color-primary-200: #66F0C1; + --color-primary-300: #41ECB3; + --color-primary-400: #1CE8A4; + --color-primary-500: #14C78B; + --color-primary-600: #10A271; + --color-primary-700: #0D7D57; + --color-primary-800: #09583D; + --color-primary-900: #053323; + --color-primary-950: #032016; + + --color-secondary-50: light-dark(#427684, #FDFEF8); + --color-secondary-100: light-dark(#3E6F7C, #FDFDF6); + --color-secondary-200: light-dark(#36606B, #FBFCF2); + --color-secondary-300: light-dark(#2D505A, #FAFCEE); + --color-secondary-400: light-dark(#254149, #F9FBE9); + --color-secondary-500: light-dark(#1C3238, #F8FAE5); + --color-secondary-600: light-dark(#192C31, #F7F9E1); + --color-secondary-700: light-dark(#15262A, #F6F8DC); + --color-secondary-800: light-dark(#122024, #F5F8D8); + --color-secondary-900: light-dark(#0E1A1D, #F3F7D4); + --color-secondary-950: light-dark(#0D1719, #F3F6D2); + + --color-background-50: light-dark(#FDFEF8, #427684); + --color-background-100: light-dark(#FDFDF6, #3E6F7C); + --color-background-200: light-dark(#FBFCF2, #36606B); + --color-background-300: light-dark(#FAFCEE, #2D505A); + --color-background-400: light-dark(#F9FBE9, #254149); + --color-background-500: light-dark(#F8FAE5, #1C3238); + --color-background-600: light-dark(#F7F9E1, #192C31); + --color-background-700: light-dark(#F6F8DC, #15262A); + --color-background-800: light-dark(#F5F8D8, #122024); + --color-background-900: light-dark(#F3F7D4, #0E1A1D); + --color-background-950: light-dark(#F3F6D2, #0D1719); + + --color-error-50: #FFB8B8; + --color-error-100: #FFA3A3; + --color-error-200: #FF7A7A; + --color-error-300: #FF5252; + --color-error-400: #FF2929; + --color-error-500: #FF0000; + --color-error-600: #D60000; + --color-error-700: #AD0000; + --color-error-800: #850000; + --color-error-900: #5C0000; + --color-error-950: #470000; + + --color-success-50: #07FF07; + --color-success-100: #00FC00; + --color-success-200: #00E700; + --color-success-300: #00D300; + --color-success-400: #00BE00; + --color-success-500: #00AA00; + --color-success-600: #009600; + --color-success-700: #008100; + --color-success-800: #006D00; + --color-success-900: #005800; + --color-success-950: #004E00; +} \ No newline at end of file diff --git a/assets/css/main.css b/assets/css/main.css index e8a7659ca1e2abddf2b1b2e5d977f2e76d8d6a1a..2cb0caa9f250644d20bb9e62f1d2e7712c4e5d5d 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -1,3 +1,2 @@ @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Open+Sans:ital,wdth,wght@0,75..100,300..800;1,75..100,300..800&family=Oswald:wght@200..700&display=swap'); -@import "tailwindcss"; @import "@nuxt/ui"; diff --git a/components/AppFooter.vue b/components/AppFooter.vue index eeee7b9014c0ee72fc53dbe7b72cc793d77ae087..7ae083b9d9e95895ffa2515d85985bd476af4d50 100644 --- a/components/AppFooter.vue +++ b/components/AppFooter.vue @@ -4,22 +4,26 @@ const appConfig = useAppConfig() <style> footer { - position: fixed; - bottom: 0; - padding-left: 8px; - width: 100%; + position: fixed; + bottom: 0; + padding-left: 8px; + width: 100%; - display: flex; - background-color: v-bind(appConfig.theme.dark.colors.background); + display: flex; + background-color: var(--color-background-500); } + footer > p { - margin: 1vh; - color: v-bind(appConfig.theme.dark.colors.primary); + margin: 1vh; + color: var(--color-secondary-500); } </style> <template> - <footer> - <p>© 2025 3SPA</p> - </footer> + <footer> + <p>© 2025 3SPA</p> + <p>Accessibilité : non conforme</p> + <p><a href="">Conditions générales de ventes</a></p> + <p><a href="">Conditions générales d'utilisation</a></p> + </footer> </template> \ No newline at end of file diff --git a/components/AppHeader.vue b/components/AppHeader.vue index 7bb6ae4953ee9f89dd0bac0e2e656bd6bdba267e..b16b04df58a7de4fbce3744895e7283b7b36130e 100644 --- a/components/AppHeader.vue +++ b/components/AppHeader.vue @@ -1,5 +1,6 @@ <script setup lang="ts"> const appConfig = useAppConfig() +const open = ref(false) </script> <style> @@ -12,17 +13,12 @@ header { justify-items: center; align-items: center; grid-template-columns:1fr 10vw; - background-color: v-bind(appConfig.theme.dark.colors.background); + background-color: var(--color-background-500); } .title { margin: 1vh; - color: v-bind(appConfig.theme.dark.colors.secondary); -} - -header a { - width: 100%; - height: 100%; + color: var(--color-primary-500); } header span { @@ -38,7 +34,19 @@ header span { {{ appConfig.title }} </NuxtLink> </h1> - <UButton icon="material-symbols:account-circle" size="xl" color="primary" variant="link" rounded-full - to="/login"/> + <USlideover + aria-label="Modal de l'utilisateur" + v-model:open="open" + :close="false" + :ui="{ footer: 'justify-center'}"> + <UButton icon="material-symbols:account-circle" size="xl" color="primary" variant="link" rounded-full + class="h-full w-full" aria-label="account"/> + <template #body> + <Placeholder class="h-full"/> + </template> + <template #footer> + <UButton color="neutral" label="Fermer" @click="open = false"/> + </template> + </USlideover> </header> </template> diff --git a/components/ColorModeButton.vue b/components/ColorModeButton.vue new file mode 100644 index 0000000000000000000000000000000000000000..aa3ad47c304eb063618b1489f02be4c8b36e56eb --- /dev/null +++ b/components/ColorModeButton.vue @@ -0,0 +1,23 @@ +<script setup> +const colorMode = useColorMode() +const isDark = computed({ + get() { + return colorMode.value === 'dark' + }, + set() { + colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark' + } +}) +</script> + +<template> + <ClientOnly v-if="!colorMode?.forced"> + <USwitch + unchecked-icon="i-lucide-sun" + checked-icon="i-lucide-moon" + v-model="isDark" + size="xl" + label="mode sombre" + /> + </ClientOnly> +</template> diff --git a/components/Placeholder.vue b/components/Placeholder.vue new file mode 100644 index 0000000000000000000000000000000000000000..8535263350fad4222dcb12f8d67566b7cf20eb06 --- /dev/null +++ b/components/Placeholder.vue @@ -0,0 +1,51 @@ +<script setup lang="ts"> +import userData from '../data/mock/user.json'; +import {UserMapper} from "~/utils/mapper/UserMapper"; + +const user = ref<User>(UserMapper.from(userData)); + +function getStatus(): "error" | "success" | "warning" | "neutral" | "primary" | "secondary" | "info" | undefined { + switch (user.value.status) { + case Status.ACTIVE: + return 'success'; + case Status.ABSENT: + return 'warning'; + case Status.DND: + return 'error'; + case Status.INVISIBLE: + case Status.OFFLINE: + return 'neutral'; + default: + return 'primary'; + } +} +</script> + +<style> +.chip { + display: flex; + flex-direction: column; + align-items: center; + gap: 1vh; +} +</style> + +<template> + <div class="chip"> + <UTooltip :text="user.firstname + ' ' + user.lastname"> + <UChip inset size="3xl" :color="getStatus()"> + <UAvatar + size="3xl" + :src="user.icon" + :alt="user.firstname + ' ' + user.lastname" + /> + </UChip> + </UTooltip> + <p class="text-center"> + <b>{{ user.lastname }}</b> {{ user.firstname }} + <br>{{ user.role ?? 'Inconnu' }} + </p> + <USeparator label="Paramètres" class="m-2"/> + <ColorModeButton></ColorModeButton> + </div> +</template> \ No newline at end of file diff --git a/data/mock/user.json b/data/mock/user.json new file mode 100644 index 0000000000000000000000000000000000000000..21085e9d0e1e37ff22051951da5c5a8b9d06ea9d --- /dev/null +++ b/data/mock/user.json @@ -0,0 +1,10 @@ +{ + "id": 1, + "lastname": "Martin", + "firstname": "Pierre-Alexandre", + "username": "pmartin", + "language": "fr", + "icon": "https://randomuser.me/api/portraits/m", + "role": "admin", + "status": "active" +} \ No newline at end of file diff --git a/layouts/login.vue b/layouts/clear.vue similarity index 100% rename from layouts/login.vue rename to layouts/clear.vue diff --git a/nuxt.config.ts b/nuxt.config.ts index 9248c03f19d0377a8c53ac831420c21882317a37..ef2a82aa0bfe57123015d72d89f40be33fd74726 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -31,6 +31,9 @@ export default defineNuxtConfig({ head: { charset: 'utf-8', viewport: 'width=device-width, initial-scale=1', + htmlAttrs: { + lang: 'en' + } }, pageTransition: {name: 'page', mode: 'out-in'}, layoutTransition: {name: 'layout', mode: 'out-in'} @@ -41,6 +44,7 @@ export default defineNuxtConfig({ '~/assets/css/main.css', '~/assets/css/fonts.css', '~/assets/css/transitions.css', + '~/assets/css/colors.css', ], components: [ @@ -57,6 +61,7 @@ export default defineNuxtConfig({ }, modules: ['@nuxt/ui'], + icon: { serverBundle: { collections: ['material-symbols'] diff --git a/pages/index.vue b/pages/index.vue index a369b93824e902d55c5d58ebe726a62adae56416..38133b6b248a5b32f8438a91facace4cb8d3657d 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -17,5 +17,6 @@ definePageMeta({ <NuxtLink to="/login">Log in</NuxtLink> <br> <NuxtLink to="/exercices">Exercices</NuxtLink> + <br> </div> </template> \ No newline at end of file diff --git a/pages/login.vue b/pages/login.vue index a3885b357e95c695e2db563a96d0dcbbf7dca0cd..3c4885bd13920ebe28b0a40041dcd79f6e8b4c3b 100644 --- a/pages/login.vue +++ b/pages/login.vue @@ -1,6 +1,6 @@ <script setup lang="ts"> definePageMeta({ - layout: 'login' + layout: 'clear' }) import * as v from 'valibot' import type {FormSubmitEvent} from '#ui/types' @@ -45,15 +45,11 @@ async function onSubmit(event: FormSubmitEvent<Schema>) { flex-direction: column; align-items: center; } - -div.page-login { - height: 100%; -} </style> <template> - <div class="page-login"> - <h2>Login</h2><br> + <div class="page-login h-full"> + <h1>Login</h1><br> <UForm :schema="v.safeParser(schema)" :state="state" class="space-y-4 page-login" @submit="onSubmit"> <UFormField label="Email" name="email"> <UInput v-model="state.email" placeholder="e-mail" icon="material-symbols:account-circle" diff --git a/types/User.ts b/types/User.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9a3cdeeda38497f3acd186ae27d647b4d1b0672 --- /dev/null +++ b/types/User.ts @@ -0,0 +1,13 @@ +import type {Role} from "~/types/enum/Role"; +import type {Status} from "~/types/enum/Status"; + +export interface User { + id: number; + lastname: string; + firstname: string; + username: string; + language: string; + icon: string; + role: Role; + status: Status; +} \ No newline at end of file diff --git a/types/enum/ExerciseCategory.ts b/types/enum/ExerciseCategory.ts index 760928e8767928f1620015adc7a3755afccf16cd..c405b4d15beb392d0ddb45d4bcf8c5b1ddac6dcc 100644 --- a/types/enum/ExerciseCategory.ts +++ b/types/enum/ExerciseCategory.ts @@ -1,5 +1,5 @@ export enum ExerciseCategory { - Renforcement = 'Renforcement', - Cardio = 'Cardio', - Stretching = 'Stretching', + RENFORCEMENT = 'Renforcement', + CARDIO = 'Cardio', + STRETCHING = 'Stretching', } \ No newline at end of file diff --git a/types/enum/Role.ts b/types/enum/Role.ts new file mode 100644 index 0000000000000000000000000000000000000000..e69e5ebae58573316104ec724f44dd37fb2a60f9 --- /dev/null +++ b/types/enum/Role.ts @@ -0,0 +1,6 @@ +export enum Role { + ADMIN = 'Administrateur', + A_USER = 'Utilisateur approuvé', + USER = "Utilisateur", + GUEST = 'Invité', +} \ No newline at end of file diff --git a/types/enum/Status.ts b/types/enum/Status.ts new file mode 100644 index 0000000000000000000000000000000000000000..c288be1a463f620c7c88fdbb1b7658831c139dc4 --- /dev/null +++ b/types/enum/Status.ts @@ -0,0 +1,7 @@ +export enum Status { + ACTIVE = 'active', + ABSENT = 'absent', + DND = 'dnd', + INVISIBLE = 'invisible', + OFFLINE = 'offline' +} \ No newline at end of file diff --git a/utils/mapper/UserMapper.ts b/utils/mapper/UserMapper.ts new file mode 100644 index 0000000000000000000000000000000000000000..3bd8459b5ea724a124adcc54f181c63ce1a20a24 --- /dev/null +++ b/utils/mapper/UserMapper.ts @@ -0,0 +1,17 @@ +import {Role} from "~/types/enum/Role"; +import {Status} from "~/types/enum/Status"; + +export class UserMapper { + static from(user: any): User { + return { + id: user.id, + lastname: user.lastname, + firstname: user.firstname, + username: user.username, + language: user.language, + icon: user.icon, + role: Role[user.role.toUpperCase() as keyof typeof Role], + status: Status[user.status.toUpperCase() as keyof typeof Status], + }; + } +} \ No newline at end of file