├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .prettierrc.json ├── .vscode └── settings.json ├── README.md ├── app ├── app.vue ├── assets │ └── css │ │ └── main.css ├── components │ ├── about │ │ ├── FaqSection.vue │ │ ├── IntroSection.vue │ │ ├── MakerCard.vue │ │ ├── MakersSection.vue │ │ ├── OrgCard.vue │ │ └── OrgaSection.vue │ ├── content │ │ └── YoutubeVideoDetails.vue │ ├── home │ │ ├── LatestTalks.vue │ │ ├── NextSpeakers.vue │ │ ├── SectionGallery.vue │ │ ├── SectionHero.vue │ │ ├── SpeakerCard.vue │ │ ├── SponsorCard.vue │ │ └── SponsorSection.vue │ ├── shared │ │ ├── AppButton.vue │ │ ├── AppCarousel.vue │ │ ├── AppFooter.vue │ │ ├── AppHeader.vue │ │ ├── AppLink.vue │ │ ├── AppMobileHeader.vue │ │ ├── AppOutlineButton.vue │ │ ├── AppSectionTitle.vue │ │ └── AppSocials.vue │ ├── sponsor │ │ ├── SectionBecomeHost.vue │ │ └── SectionBecomeSponsor.vue │ └── talks │ │ ├── SearchBar.vue │ │ └── YoutubeCard.vue ├── layouts │ └── default.vue ├── pages │ ├── about.vue │ ├── index.vue │ ├── sponsor.vue │ └── talks │ │ ├── [...slug].vue │ │ └── index.vue └── utils │ ├── decodeHTML.ts │ └── slugify.ts ├── composables └── useTalks.ts ├── content └── talks │ ├── ce-que-j-ai-appris-en-creant-une-libraire-de-composants-vue-3-stanislas-ormieres-vuejs-paris-26.md │ ├── comment-gerer-l-etat-serveur-efficacement-elise-patrikainen-vue-js-paris-25.md │ ├── core-web-vitals-nicolas-frizzarin-vue-js-paris-26.md │ ├── de-l-usage-du-jsx-en-vue-yoann-fort-vue-js-paris-25.md │ ├── developpement-d-un-generateur-de-bannieres-vue-js-vs-svelte-marc-bouvier-vue-js-paris-26.md │ ├── nuxt-islands-julien-huang-vue-js-paris-24.md │ ├── nuxt-studio-le-cms-de-l-ecosysteme-vue-baptiste-leproux-vue-js-paris-24.md │ ├── pinceau-road-to-v1-yael-guilloux-vue-js-paris-24.md │ ├── vue-js-paris-meetup-16-migration-d-une-application-vers-vue-js-nuxt-js.md │ ├── vue-js-paris-meetup-19-vue-instantsearch-jhipster.md │ └── vue-js-paris-meetup-22-vue-3-deep-dive-jamstack-is-the-future.md ├── eslint.config.mjs ├── nuxt.config.ts ├── package.json ├── pnpm-lock.yaml ├── public ├── images │ ├── Eduardo.webp │ ├── Elise.webp │ ├── Florian.webp │ ├── Neon.webp │ ├── Nx.webp │ ├── Paris_cover.webp │ ├── hero image.svg │ ├── mastering_pinia.webp │ ├── meetupAtelier1.webp │ ├── meetupAtelier2.webp │ ├── meetupAtelier3.webp │ ├── meetupAtelier4.webp │ ├── meetupAtelier5.webp │ ├── meetupAtelier6.webp │ ├── meetupAtelier7.webp │ ├── meetupAtelier8.webp │ ├── meetupAtelier9.webp │ ├── photoAlex.webp │ └── unknownSpeaker.webp ├── logo │ ├── vuejs_paris_logo.svg │ ├── vuejs_paris_logo.webp │ ├── vuejs_paris_logo_dark.svg │ └── vuejs_paris_logo_dark.webp └── robots.txt ├── shared └── types │ ├── YouTubeSearchItem.ts │ └── YoutubeVideo.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Nuxt dev/build outputs 2 | .output 3 | .data 4 | .nuxt 5 | .nitro 6 | .cache 7 | dist 8 | 9 | # Node dependencies 10 | node_modules 11 | 12 | # Logs 13 | logs 14 | *.log 15 | 16 | # Misc 17 | .DS_Store 18 | .fleet 19 | .idea 20 | 21 | # Local env files 22 | .env 23 | .env.* 24 | !.env.example 25 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,mjs,ts,vue}": ["eslint --fix", "prettier --write"], 3 | "*.{json,md,yml}": ["prettier --write"] 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.useFlatConfig": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "[vue]": { 6 | "editor.defaultFormatter": "esbenp.prettier-vscode" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Minimal Starter 2 | 3 | Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more. 4 | 5 | ## Setup 6 | 7 | Make sure to install dependencies: 8 | 9 | ```bash 10 | # npm 11 | npm install 12 | 13 | # pnpm 14 | pnpm install 15 | 16 | # yarn 17 | yarn install 18 | 19 | # bun 20 | bun install 21 | ``` 22 | 23 | ## Development Server 24 | 25 | Start the development server on `http://localhost:3000`: 26 | 27 | ```bash 28 | # npm 29 | npm run dev 30 | 31 | # pnpm 32 | pnpm dev 33 | 34 | # yarn 35 | yarn dev 36 | 37 | # bun 38 | bun run dev 39 | ``` 40 | 41 | ## Production 42 | 43 | Build the application for production: 44 | 45 | ```bash 46 | # npm 47 | npm run build 48 | 49 | # pnpm 50 | pnpm build 51 | 52 | # yarn 53 | yarn build 54 | 55 | # bun 56 | bun run build 57 | ``` 58 | 59 | Locally preview production build: 60 | 61 | ```bash 62 | # npm 63 | npm run preview 64 | 65 | # pnpm 66 | pnpm preview 67 | 68 | # yarn 69 | yarn preview 70 | 71 | # bun 72 | bun run preview 73 | ``` 74 | 75 | Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. 76 | 77 | # website 78 | -------------------------------------------------------------------------------- /app/app.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /app/assets/css/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin '@tailwindcss/typography'; 3 | 4 | @custom-variant dark (&:is(.dark *)); 5 | 6 | @theme { 7 | --color-primarygreen-100: #4ade80; 8 | --color-primarygreen-200: #1c6739; 9 | 10 | --color-primaryblue: #35495e; 11 | --color-darkbg: #0f172a; 12 | --color-darkerbg: #010317; 13 | --color-bordercolor: #4f4d4d; 14 | 15 | --font-roboto: Roboto, sans-serif; 16 | --font-montserrat: Montserrat, sans-serif; 17 | --font-herotitle: Gasoek One, sans-serif; 18 | 19 | --outline-offset-3: 1rem; 20 | 21 | --height-section: calc(100vh - 100px); 22 | --height-sectionxl: calc(100vh - 200px); 23 | 24 | --animate-fade-in-out: fadeInOut 5s infinite; 25 | --animate-rotate: rotate 4s linear infinite; 26 | 27 | @keyframes fadeInOut { 28 | 0%, 29 | 100% { 30 | opacity: 0; 31 | } 32 | 50% { 33 | opacity: 1; 34 | } 35 | } 36 | @keyframes rotate { 37 | 0% { 38 | transform: rotate(0deg) scale(10); 39 | } 40 | 100% { 41 | transform: rotate(360deg) scale(10); 42 | } 43 | } 44 | } 45 | 46 | h1, 47 | h2, 48 | h3, 49 | h4, 50 | h5, 51 | h6 { 52 | font-family: "Montserrat", sans-serif; 53 | } 54 | 55 | .background { 56 | background: rgb(1, 3, 23); 57 | background: linear-gradient(90deg, rgb(0, 0, 0) 56%, rgb(24, 24, 24) 97%); 58 | } 59 | 60 | .background-stripes { 61 | background: repeating-linear-gradient( 62 | 135deg, 63 | #35353500, 64 | #34343400 6px, 65 | #2b2b2b00 6px, 66 | #4f4d4db2 7px 67 | ); 68 | } 69 | 70 | .background-dots { 71 | position: relative; 72 | overflow: hidden; 73 | } 74 | 75 | .background-dots::before { 76 | content: ""; 77 | position: absolute; 78 | top: 0; 79 | left: 0; 80 | width: 100%; 81 | height: 100%; 82 | background-image: radial-gradient( 83 | rgba(255, 255, 255, 0.7) 2px, 84 | transparent 1px 85 | ); 86 | background-size: 15px 15px; 87 | opacity: 0.7; 88 | pointer-events: none; 89 | transition: opacity 0.2s ease-out; 90 | mask-image: radial-gradient( 91 | circle 150px at var(--mouse-x) var(--mouse-y), 92 | rgba(255, 255, 255, 0.9), 93 | rgba(255, 255, 255, 0.2) 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /app/components/about/FaqSection.vue: -------------------------------------------------------------------------------- 1 | 87 | -------------------------------------------------------------------------------- /app/components/about/IntroSection.vue: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /app/components/about/MakerCard.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 62 | -------------------------------------------------------------------------------- /app/components/about/MakersSection.vue: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /app/components/about/OrgCard.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 52 | -------------------------------------------------------------------------------- /app/components/about/OrgaSection.vue: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /app/components/content/YoutubeVideoDetails.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 49 | -------------------------------------------------------------------------------- /app/components/home/LatestTalks.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /app/components/home/NextSpeakers.vue: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /app/components/home/SectionGallery.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /app/components/home/SectionHero.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 69 | -------------------------------------------------------------------------------- /app/components/home/SpeakerCard.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 122 | -------------------------------------------------------------------------------- /app/components/home/SponsorCard.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 43 | -------------------------------------------------------------------------------- /app/components/home/SponsorSection.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /app/components/shared/AppButton.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 39 | -------------------------------------------------------------------------------- /app/components/shared/AppCarousel.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /app/components/shared/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /app/components/shared/AppHeader.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 87 | -------------------------------------------------------------------------------- /app/components/shared/AppLink.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 41 | -------------------------------------------------------------------------------- /app/components/shared/AppMobileHeader.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 61 | -------------------------------------------------------------------------------- /app/components/shared/AppOutlineButton.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 49 | -------------------------------------------------------------------------------- /app/components/shared/AppSectionTitle.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 34 | -------------------------------------------------------------------------------- /app/components/shared/AppSocials.vue: -------------------------------------------------------------------------------- 1 | 53 | -------------------------------------------------------------------------------- /app/components/sponsor/SectionBecomeHost.vue: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /app/components/sponsor/SectionBecomeSponsor.vue: -------------------------------------------------------------------------------- 1 | 74 | -------------------------------------------------------------------------------- /app/components/talks/SearchBar.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /app/components/talks/YoutubeCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 42 | -------------------------------------------------------------------------------- /app/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/pages/about.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /app/pages/index.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/pages/sponsor.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /app/pages/talks/[...slug].vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /app/pages/talks/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 56 | -------------------------------------------------------------------------------- /app/utils/decodeHTML.ts: -------------------------------------------------------------------------------- 1 | export function decodeHTML(html: string): string { 2 | if (typeof window === "undefined") { 3 | return html; 4 | } 5 | 6 | const textarea = document.createElement("textarea"); 7 | textarea.innerHTML = html; 8 | return textarea.value; 9 | } 10 | -------------------------------------------------------------------------------- /app/utils/slugify.ts: -------------------------------------------------------------------------------- 1 | export function slugify(title: string): string { 2 | return ( 3 | title 4 | // Décoder l'entité HTML ' en véritable apostrophe 5 | .replace(/'/g, "'") 6 | // Décompose les caractères accentués (NFD) 7 | .normalize("NFD") 8 | // Supprime les diacritiques (accents) 9 | .replace(/[\u0300-\u036f]/g, "") 10 | // Convertit en minuscules 11 | .toLowerCase() 12 | // Remplace explicitement les apostrophes par des tirets 13 | .replace(/[’']/g, "-") 14 | // Remplace tous les caractères non alphanumériques ou tirets existants 15 | // par des tirets (pour éviter d'autres caractères spéciaux) 16 | .replace(/[^a-z0-9-]+/g, "-") 17 | // Remplace les tirets multiples par un seul 18 | .replace(/-+/g, "-") 19 | // Supprime les tirets en début et fin de chaîne 20 | .replace(/^-+|-+$/g, "") 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /composables/useTalks.ts: -------------------------------------------------------------------------------- 1 | import type { YouTubeVideo } from "../shared/types/YoutubeVideo"; 2 | 3 | interface TalkContent { 4 | _path: string; 5 | videoId: string; 6 | title: string; 7 | description?: string; 8 | date?: string; 9 | } 10 | 11 | export function useTalks() { 12 | const { 13 | data: talks, 14 | pending, 15 | error, 16 | } = useAsyncData("talks", async () => { 17 | const contents = await queryContent("talks") 18 | .where({ videoId: { $exists: true } }) 19 | .sort({ date: -1 }) 20 | .find(); 21 | 22 | return contents.map((talk) => ({ 23 | id: { videoId: talk.videoId }, 24 | snippet: { 25 | title: talk.title, 26 | description: talk.description || "", 27 | publishedAt: talk.date || "", 28 | thumbnails: { 29 | default: { 30 | url: `https://img.youtube.com/vi/${talk.videoId}/default.jpg`, 31 | }, 32 | medium: { 33 | url: `https://img.youtube.com/vi/${talk.videoId}/mqdefault.jpg`, 34 | }, 35 | high: { 36 | url: `https://img.youtube.com/vi/${talk.videoId}/hqdefault.jpg`, 37 | }, 38 | }, 39 | }, 40 | })); 41 | }); 42 | 43 | return { talks, pending, error }; 44 | } 45 | 46 | export function useLatestTalks() { 47 | const { 48 | data: talks, 49 | pending, 50 | error, 51 | } = useAsyncData("latestTalks", async () => { 52 | const contents = await queryContent("talks") 53 | .where({ videoId: { $exists: true } }) 54 | .sort({ date: -1 }) 55 | .limit(3) 56 | .find(); 57 | 58 | return contents.map((talk) => ({ 59 | id: { videoId: talk.videoId }, 60 | snippet: { 61 | title: talk.title, 62 | description: talk.description || "", 63 | publishedAt: talk.date || "", 64 | thumbnails: { 65 | default: { 66 | url: `https://img.youtube.com/vi/${talk.videoId}/default.jpg`, 67 | }, 68 | medium: { 69 | url: `https://img.youtube.com/vi/${talk.videoId}/mqdefault.jpg`, 70 | }, 71 | high: { 72 | url: `https://img.youtube.com/vi/${talk.videoId}/hqdefault.jpg`, 73 | }, 74 | }, 75 | }, 76 | })); 77 | }); 78 | 79 | return { talks, pending, error }; 80 | } 81 | -------------------------------------------------------------------------------- /content/talks/ce-que-j-ai-appris-en-creant-une-libraire-de-composants-vue-3-stanislas-ormieres-vuejs-paris-26.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Ce que j'ai appris en créant une libraire de composants Vue 3 - Stanislas Ormières - Vuejs Paris #26" 3 | videoId: "T-ZCpHOH0mc" 4 | date: "29-09-2024" 5 | description: "Vue.js Paris meetup #26 Sponsors: Mastering Pinia & Nx Host: Valtech" 6 | --- 7 | 8 | 9 | 10 | ## Résumé du talk 11 | 12 | Le speaker (Stanislas Ormières) présente son retour d’expérience sur la création d’une **bibliothèque de composants Vue 3**, pensée pour respecter le **DSFR** (Design System de l’Administration Française) et les exigences d’accessibilité (RGAA). Il détaille les choix techniques (Vite, Rollup, Storybook, Vitest, Cypress, VitePress…), les bonnes pratiques de conception (structure, conventions de commit, tests), ainsi que les défis rencontrés (tests d’accessibilité, documentation, compatibilité SSR, etc.). 13 | 14 | --- 15 | 16 | ### 1. Démarrage de projet et écosystème 17 | 18 | - **Objectifs** : 19 | - Construire des composants conformes au DSFR, réutilisables et accessibles. 20 | - Miser sur **Vue 3** (et non Vue 2) pour favoriser l’adoption de la dernière version. 21 | - Publier la bibliothèque sur NPM, documenter, tester et garantir l’accessibilité. 22 | - **Choix de Vue 3** : 23 | - Bénéficier des nouveautés (Composition API, meilleure performance). 24 | - Encourager la migration vers la V3 dans l’écosystème du client. 25 | - **Organisation** : 26 | - **Conventions Git** (Git flow ou GitHub flow) et **Conventional Commits** pour générer automatiquement le changelog. 27 | - **Open source** avec un dépôt GitHub, un serveur Discord pour faciliter la contribution. 28 | 29 | --- 30 | 31 | ### 2. Philosophie, syntaxe et tests 32 | 33 | - **Stack technique** : 34 | - **Vite** pour un démarrage rapide et un bundle performant (initialement Rollup pour le mode “library”). 35 | - **Storybook** pour documenter et visualiser les composants de manière isolée. 36 | - **Vitest** pour les tests unitaires (propulsé par Vite, proche de Jest). 37 | - **Cypress** pour les tests end-to-end et de composants, notamment l’accessibilité (focus, navigation clavier, etc.). 38 | - **Accessibilité** : 39 | - Respect du RGAA et intégration du “focus trap” pour les modales. 40 | - Vérification que le retour du focus se fasse à l’élément déclencheur. 41 | - Importance des tests de navigation clavier pour s’assurer de la conformité. 42 | 43 | --- 44 | 45 | ### 3. Exemple(s) de code 46 | 47 | - Le speaker illustre différentes **approches** pour : 48 | - Ajouter des composants (boutons, modales, accordéons…), 49 | - Configurer la **personnalisation** (props, slots) tout en masquant la complexité DSFR (classes CSS, etc.), 50 | - Tester la réactivité et l’accessibilité à travers Cypress ou Storybook Play Functions. 51 | - Chaque composant possède généralement : 52 | - Un fichier `.vue`, 53 | - Des tests unitaires (Vitest), 54 | - Des tests end-to-end (Cypress), 55 | - Une story (Storybook), 56 | - Une doc spécifique (VitePress). 57 | 58 | --- 59 | 60 | ### 4. SSR et écosystème 61 | 62 | - **Compatibilité SSR** : 63 | - Le speaker souligne l’importance d’assurer une compatibilité SSR (par exemple avec Nuxt) pour certaines applications de l’administration. 64 | - Publication en **ES modules** pour une intégration plus facile dans différents environnements. 65 | - **Écosystème additionnel** : 66 | - **create-vue-dsfr** : un assistant pour générer rapidement un projet Vue ou Nuxt préconfiguré avec la bibliothèque DSFR. 67 | - **Extension VS Code** : des snippets pour insérer plus rapidement les composants et respecter les conventions. 68 | 69 | --- 70 | 71 | ### 5. Choix et perspectives 72 | 73 | - **Stack évolutive** : 74 | - Passage progressif à **Lightning CSS** (remplacement de PostCSS). 75 | - Possibilité de migrer les tests end-to-end vers d’autres outils (Playwright ou Storybook Play Functions), pour réduire le nombre de dépendances. 76 | - Abandon du format CommonJS au profit d’un build full ESM. 77 | - **Documentation** : 78 | - Utilisation conjointe de **Storybook** et **VitePress** (avec **WFrame** pour isoler les styles DSFR). 79 | - Objectif : une doc unique côté “utilisateur” (composants, props) sans besoin de consulter directement la doc DSFR. 80 | 81 | --- 82 | 83 | ### Conclusion 84 | 85 | Le speaker met en avant l’importance de **concevoir dès le départ** une bibliothèque de composants Vue 3 en tenant compte de **l’accessibilité**, de **l’automatisation** (CI/CD, changelog) et d’une **documentation** claire pour les équipes de développement. Il souligne que l’adoption de conventions (Git flow, Conventional Commits) et d’outils adaptés (Storybook, Vitest, Cypress, etc.) facilite grandement la maintenance et l’évolution du projet. Cette expérience montre combien il est essentiel de penser à la **cohérence visuelle**, à la **réutilisabilité** des composants et au **respect des standards** (DSFR, RGAA), afin de proposer une base solide pour tous les projets de l’administration ou de clients partageant les mêmes exigences. 86 | -------------------------------------------------------------------------------- /content/talks/comment-gerer-l-etat-serveur-efficacement-elise-patrikainen-vue-js-paris-25.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Comment gérer l'état serveur efficacement ? - Elise Patrikainen - Vue.js Paris #25" 3 | videoId: "4Vgz2wteyNs" 4 | date: "28-09-2024" 5 | description: "" 6 | --- 7 | 8 | 9 | 10 | ## Résumé du talk : Server State Management (côté front-end) 11 | 12 | La speaker introduit la notion de _server state management_ **dans un contexte purement front-end**, en distinguant deux types d’état dans une application : 13 | 14 | - **L’état côté serveur (“server state”)** : données récupérées depuis un back-end (ex. liste de produits d’un e-commerce). 15 | - **L’état côté client (“client state”)** : données propres à l’interface utilisateur (ex. le contenu du panier, sélection locale, etc.). 16 | 17 | Elle montre ensuite pourquoi gérer l’état serveur peut rapidement devenir **complexe** (chargement asynchrone, gestion du cache, erreurs, rafraîchissement…), et propose d’explorer des **librairies de “server state management”** pour éviter de tout coder à la main. 18 | 19 | --- 20 | 21 | ### 1. Les défis du “server state” 22 | 23 | 1. **Asynchronisme** : nécessite du fetch, des états de “loading” et “erreur”. 24 | 2. **Multiples mutations** : la donnée peut changer à tout moment côté serveur (par d’autres utilisateurs, par exemple). 25 | 3. **Rafraîchissement, invalidation et cache** : ne pas re-fetcher en permanence, mais garder des données à jour. 26 | 27 | Les approches habituelles en front-end (stocker la donnée dans un composant ou dans un store classique) ne couvrent pas forcément la gestion du cache, du re-fetch, etc. 28 | 29 | --- 30 | 31 | ### 2. Présentation de TanStack Query 32 | 33 | La speaker présente **TanStack Query** (anciennement React Query) qui est désormais **framework-agnostique** grâce à un “cœur” pur JavaScript, avec des adaptateurs pour React, Vue, etc. 34 | 35 | ### 2.1 Concepts clés 36 | 37 | - **Query** : pour **fetcher** des données (GET) de manière déclarative. 38 | 39 | - On définit une _query key_ (identifiant) et une _query function_ (comment on fetch). 40 | - On configure diverses options (ex. `staleTime` pour marquer les données “fraîches” pendant un certain temps). 41 | - **Retours** d’état (ex. `data`, `isLoading`, `isError`) sont _réactifs_. 42 | 43 | - **Mutation** : pour **envoyer** ou **mettre à jour** des données (POST, PATCH, DELETE…). 44 | 45 | - Impérative (déclenchée via une fonction `mutate`). 46 | - Permet d’exécuter des callbacks de succès, d’erreur… 47 | - Peut faire des **optimistic updates** (mettre à jour le cache immédiatement, puis rollback si échec). 48 | 49 | - **Query Client** : un “store” interne qui centralise toutes les queries/mutations. 50 | - Peut préfetcher (`prefetchQuery`), invalider des queries, ou modifier directement le cache (`setQueryData`). 51 | 52 | ### 2.2 Démonstration 53 | 54 | Sur une application e-commerce (Vue/Nuxt + Tailwind + Superbase), la speaker illustre : 55 | 56 | 1. **Fetch** de la liste de produits avec une `useQuery`. 57 | 2. **Fetch** du détail produit selon un `productId`, en utilisant la même configuration “queryOptions”. 58 | 3. **Optimisations** : 59 | - **Placeholder data** : remplir les infos déjà connues (ex. tirées de la liste) avant que la requête ne se termine. 60 | - **Prefetch** : lancer en arrière-plan une requête pour le détail d’un produit dès que l’utilisateur survole un lien. 61 | 4. **Mutation** : ajout d’un produit au panier, qui met à jour l’info de disponibilité dans le cache. 62 | - Possibilité de mettre à jour le cache en “optimistic update” et de faire un “rollback” en cas d’erreur. 63 | 64 | --- 65 | 66 | ### 3. Alternatives à TanStack Query 67 | 68 | - **Apollo Client** : fonctionne très bien mais plutôt dédié à GraphQL. 69 | - **Pinacolada** (en développement par Eduardo) : une solution inspirée de Pinia pour la gestion du “server state” côté Vue. 70 | 71 | Enfin, pour en savoir plus : 72 | 73 | - Le blog de **TKDodo** (un contributeur majeur de TanStack Query), avec plein d’articles détaillés. 74 | - Le code de la démo est disponible en ligne. 75 | 76 | --- 77 | 78 | ### Conclusion 79 | 80 | Gérer l’état serveur (“Server state”) diffère sensiblement de l’état purement local. Des librairies comme **TanStack Query** simplifient énormément la vie : elles gèrent le cache, le re-fetch, l’invalidation et même l’optimistic UI. Cette approche fonctionne quelle que soit la stack (Vue, React, Nuxt…) et permet de **conserver une application fluide** avec un code plus propre. 81 | -------------------------------------------------------------------------------- /content/talks/core-web-vitals-nicolas-frizzarin-vue-js-paris-26.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Core web vitals - Nicolas Frizzarin - Vue.js Paris #26" 3 | videoId: "xkFWZvl3JZE" 4 | date: "29-09-2024" 5 | description: "Vue.js Paris meetup #26 Sponsors: Mastering Pinia & Nx Host: Valtech" 6 | --- 7 | 8 | 9 | 10 | ## Résumé du talk 11 | 12 | Nolas Frarin, développeur advocate et senior staff engineer, expose les **principaux problèmes de performance** que rencontrent les utilisateurs sur le web, puis présente les **Core Web Vitals** de Google et leurs optimisations. Même si l’exemple est illustré avec Angular, les bonnes pratiques sont **génériques** et s’appliquent aussi bien à Vue qu’à d’autres frameworks ou au vanilla JavaScript. 13 | 14 | --- 15 | 16 | ### Pourquoi les utilisateurs sont mécontents 17 | 18 | 1. **Page blanche** au chargement (ou un loader interminable). 19 | 2. **Instabilité** de l’affichage (scroll soudain, perte de focus de lecture). 20 | 3. **Interactivité lente** : le clic ou l’action met trop de temps avant de donner un retour à l’utilisateur. 21 | 22 | Ces problèmes font perdre des utilisateurs (et donc de l’argent) car ils nuisent à l’expérience et la concurrence est rude. 23 | 24 | --- 25 | 26 | ### Les Core Web Vitals 27 | 28 | Google a introduit les Core Web Vitals pour définir des **indicateurs clés** de l’expérience utilisateur : 29 | 30 | 1. **LCP (Largest Contentful Paint)** : temps d’affichage du plus gros élément visuel (image, bloc de texte). 31 | 2. **CLS (Cumulative Layout Shift)** : mesure de la **stabilité** visuelle (évite l’effet “écran qui bouge”). 32 | 3. **INP (Interaction to Next Paint)** : remplace le FID (First Input Delay). Évalue le délai entre l’**interaction** de l’utilisateur et l’affichage de la frame suivante (c’est la plus récente et encore expérimentale). 33 | 34 | - **Seuils** généraux : 35 | - **LCP** : moins de 2,5s conseillé. 36 | - **CLS** : moins de 0,1 (cumul de décalage) conseillé. 37 | - **INP** : moins de 200ms conseillé. 38 | 39 | --- 40 | 41 | ### Mesurer les performances 42 | 43 | - **Field data (données réelles)** : mesures en production via Chrome UX Report, PageSpeed Insights, la lib Web Vitals JS, etc. 44 | - **Lab data (données de labo)** : simulation en local ou via Lighthouse, Chrome DevTools… (mais INP est mal simulé). 45 | 46 | Les deux approches sont complémentaires : 47 | 48 | - Le Lab permet de **détecter tôt** les problèmes avant la prod. 49 | - Le Field reflète la **vraie expérience** sur divers appareils et réseaux. 50 | 51 | --- 52 | 53 | ### Optimiser chaque métrique 54 | 55 | #### 1. Optimiser le LCP 56 | 57 | - **Améliorer le “Time to First Byte”** (par ex. config serveur). 58 | - **Précharger** (preload) les ressources critiques (CSS, images). 59 | - **Différer** (defer) ou découper (split) le JavaScript. 60 | - **Charger en parallèle** les composants/libraries non indispensables au premier rendu. 61 | 62 | #### 2. Réduire le CLS 63 | 64 | - **Réserver l’espace** pour les images (largeur/hauteur, aspect-ratio). 65 | - Éviter d’injecter des éléments “tardivement” (ex. iframes, pubs) sans taille fixe. 66 | - Gérer correctement les **fontes** (utiliser `font-size-adjust` ou `font-display`). 67 | - Tirer parti des solutions côté framework (ex. Suspense, SSR). 68 | 69 | #### 3. Mieux gérer l’INP 70 | 71 | - **Fractionner** les longues tâches JavaScript (long tasks) : utiliser des `setTimeout`, promises, ou l’API (expérimentale) `scheduler.postTask`. 72 | - Ne pas bloquer le main thread avec trop de code ou d’actions successives (fetch, analytics, etc. peuvent se faire en différé). 73 | - Réduire la complexité DOM/CSS (éviter des selecteurs trop lourds). 74 | - Gérer en priorité ce qui est **critique** pour l’utilisateur (affichage immédiat), décaler le reste pour ne pas retarder le feedback. 75 | 76 | --- 77 | 78 | ### Conclusion 79 | 80 | En **moins de 20 minutes**, le talk illustre : 81 | 82 | - **Pourquoi** l’utilisateur peut juger l’app trop lente ou peu agréable. 83 | - **Comment** chaque métrique (LCP, CLS, INP) représente un aspect clé de l’expérience. 84 | - **Quelles techniques** simples mettre en place pour optimiser la performance sur tous les frameworks (Vue, Angular, React, vanilla…). 85 | 86 | Le mot d’ordre : **réduire la taille et la complexité** des ressources critiques, **différer** le reste et **donner un retour rapide** à l’utilisateur pour améliorer l’expérience globale. 87 | -------------------------------------------------------------------------------- /content/talks/de-l-usage-du-jsx-en-vue-yoann-fort-vue-js-paris-25.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "De l'usage du JSX en Vue - Yoann Fort - Vue.js Paris #25" 3 | videoId: "NcunWlFPpSc" 4 | date: "28-09-2024" 5 | description: "Vue.js Paris meetup #25 Sponsors: Mastering Pinia & Nx Host: L'Atelier" 6 | --- 7 | 8 | 9 | 10 | ## Résumé du talk : Utiliser du JSX dans Vue.js pour des fonctions de rendu plus lisibles\*\* 11 | 12 | Le speaker présente l’usage du **JSX** dans Vue.js afin de simplifier les **fonctions de rendu** (render functions) et rendre le code plus clair, notamment pour des cas de logique complexe. Voici l’essentiel à retenir : 13 | 14 | --- 15 | 16 | ### 1. Qu’est-ce que le JSX ? 17 | 18 | - **JSX** est une extension syntaxique de JavaScript qui permet d’écrire du code ressemblant à du HTML directement dans du JS/TS. 19 | - Inventé par Facebook pour React (2013), il est désormais **transpilable** pour fonctionner dans d’autres environnements, dont Vue.js. 20 | - Il **bénéficie de toute la puissance** du langage TypeScript (map, filter, typage, etc.) pour générer dynamiquement du HTML. 21 | 22 | --- 23 | 24 | ### 2. Pourquoi aller au-delà du template classique de Vue ? 25 | 26 | 1. **Fonctions de rendu (render functions)** : 27 | 28 | - Introduites pour donner un contrôle plus fin que le template classique. 29 | - Permettent d’utiliser `h` (ou `createVNode`) afin de construire un Virtual DOM en pur JavaScript. 30 | - Mais la syntaxe “manuelle” de `h` peut rapidement devenir **verbeuse** et **moins lisible** quand la structure HTML est complexe. 31 | 32 | 2. **Avantages du JSX par rapport à la render function “classique”** : 33 | 34 | - **Lisibilité** : on écrit une syntaxe proche du HTML, plutôt qu’enchaîner des appels à `h(...)`. 35 | - **Écriture plus concise** : pas besoin de balises `