Files
portfolio/.planning/phases/03-pages-ship/03-01-PLAN.md
T
kayjaydee e5e14ef362 docs(03): create phase 3 plans — pages, components, Docker SSR
4 plans across 3 waves: shared components + deps (wave 1),
pages landing/projects/detail + about/contact/fiverr/404 (wave 2),
Dockerfile SSR + GA4 + docker-compose (wave 3).
2026-04-08 18:25:28 +02:00

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-pages-ship 01 execute 1
package.json
package-lock.json
app/data/site.ts
shared/types/index.ts
app/components/sections/HeroSection.vue
app/components/sections/ServicesSection.vue
app/components/sections/FeaturedProjectsSection.vue
app/components/sections/TestimonialsSection.vue
app/components/sections/FAQSection.vue
app/components/sections/CTASection.vue
app/components/ProjectCard.vue
app/components/TechBadge.vue
app/components/ProjectGallery.vue
app/components/ContactForm.vue
server/api/contact.post.ts
nuxt.config.ts
app/app.vue
true
COMP-01
COMP-02
COMP-03
COMP-04
truths artifacts key_links
Gallery modal opens with UModal + UCarousel and thumbnails, keyboard nav works
Contact form validates with Zod, sends via nodemailer SMTP, shows UToast
FAQ accordion renders i18n content with UAccordion
Testimonials section renders all testimonials with UCard
Project cards link to detail pages with translated content
Site config data (contact, social, fiverr) is available as typed data
path provides
app/components/ProjectGallery.vue UModal + UCarousel gallery with thumbnails and keyboard nav
path provides
app/components/ContactForm.vue UForm + Zod validated contact form
path provides
server/api/contact.post.ts Nodemailer SMTP server route
path provides
app/components/sections/FAQSection.vue UAccordion FAQ section
path provides
app/components/sections/TestimonialsSection.vue Testimonials with UCard
from to via pattern
app/components/ContactForm.vue server/api/contact.post.ts $fetch('/api/contact', { method: 'POST' }) $fetch.*api/contact
from to via pattern
app/components/ProjectGallery.vue UModal + UCarousel v-model:open + useTemplateRef UModal|UCarousel
Installer les dependances manquantes (nodemailer, zod), migrer la config site, et creer tous les composants partages reutilisables : sections landing (Hero, Services, FeaturedProjects, Testimonials, FAQ, CTA), ProjectCard, TechBadge, ProjectGallery (UModal+UCarousel), ContactForm (UForm+Zod+nodemailer), et la route serveur contact.

Purpose: Ces composants sont consommes par toutes les pages en Wave 2-3. Les construire d'abord evite la duplication et permet le parallelisme. Output: Composants dans app/components/, route serveur dans server/api/, dependances installees.

<execution_context> @C:\Users\minit.claude\get-shit-done\workflows\execute-plan.md @C:\Users\minit.claude\get-shit-done\templates\summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/03-pages-ship/03-CONTEXT.md @.planning/phases/03-pages-ship/03-RESEARCH.md

@app/data/projects.ts @app/data/testimonials.ts @app/data/faq.ts @app/data/techstack.ts @app/composables/useProjects.ts @shared/types/index.ts @src/config/site.ts @src/components/sections/HeroSection.vue @src/components/sections/ServicesSection.vue @src/components/TestimonialsSection.vue @src/components/ServiceFAQ.vue @src/components/ProjectCard.vue @src/components/TechBadge.vue @src/components/GalleryModal.vue @src/components/FiverrHero.vue @src/components/FiverrServiceCard.vue @app/app.vue @nuxt.config.ts

From shared/types/index.ts: ```typescript export interface Project { id: string; title: string; description: string; longDescription?: string; image: string; technologies: string[]; category: string; date: string; featured?: boolean; buttons?: ProjectButton[]; gallery?: string[]; demoUrl?: string; githubUrl?: string; features?: string[] } export interface Technology { name: string; level: 'Beginner' | 'Intermediate' | 'Advanced'; image: string } export interface TechStack { programming: Technology[]; front: Technology[]; database: Technology[]; devtools: Technology[]; operating_systems: Technology[]; socials: Technology[] } export interface Testimonial { name: string; role: string; company: string; avatar: string; rating: number; content: string; date: string; platform: string; featured?: boolean; project_type: string; results?: string[] } export interface TestimonialsStats { totalReviews: number; averageRating: number; projectsCompleted: number } export interface FAQ { questionKey: string; answerKey: string; featuresKey?: string } ```

From app/composables/useProjects.ts:

export function useProjects(): { projects: ComputedRef<Project[]>; featuredProjects: ComputedRef<Project[]>; filterByCategory(cat: string): ComputedRef<Project[]>; search(query: Ref<string> | string): ComputedRef<Project[]>; findById(id: string): ComputedRef<Project | undefined> }

From src/config/site.ts (to migrate):

export interface SiteConfig { name: string; title: string; description: string; author: string; contact: ContactInfo; social: SocialLink[]; fiverr: FiverrConfig; url: string; seo: {...}; performance: {...} }
export interface FiverrService { id: string; url: string; image: string; price: string }
export interface FiverrConfig { profileUrl: string; services: FiverrService[] }
export interface ContactInfo { email: string; phone: string; location: string }
export interface SocialLink { name: string; url: string; icon: string; username?: string }
Task 1: Installer deps, migrer site config, ajouter UApp, configurer runtimeConfig SMTP package.json, package-lock.json, app/data/site.ts, shared/types/index.ts, nuxt.config.ts, app/app.vue 1. Installer les dependances : ```bash npm install nodemailer zod npm install --save-dev @types/nodemailer ```
  1. Creer app/data/site.ts en migrant le contenu de src/config/site.ts. Copier la structure exacte (siteConfig avec contact, social, fiverr, seo, performance). Ajuster les chemins images fiverr : remplacer @/assets/images/fiverr/ par /images/fiverr/ (images dans public/). Exporter siteConfig et les interfaces SiteConfig, ContactInfo, SocialLink, FiverrService, FiverrConfig.

  2. Ajouter les interfaces manquantes dans shared/types/index.ts : SiteConfig, ContactInfo, SocialLink, FiverrService, FiverrConfig (ou les exporter depuis app/data/site.ts directement — au choix du plus simple).

  3. Mettre a jour nuxt.config.ts pour ajouter le runtimeConfig SMTP prive (per D-11, D-13) :

    runtimeConfig: {
      smtpHost: '',    // NUXT_SMTP_HOST
      smtpUser: '',    // NUXT_SMTP_USER
      smtpPass: '',    // NUXT_SMTP_PASS
      smtpTo: '',      // NUXT_SMTP_TO
      public: {
        gtag: { id: '' },
      },
    },
    

    IMPORTANT : les credentials SMTP dans la section privee, JAMAIS dans public (per RESEARCH.md Pitfall 4).

  4. Mettre a jour app/app.vue pour wrapper avec <UApp> — requis pour que useToast() fonctionne (per D-10, RESEARCH.md Pitfall 1) :

    <template>
      <UApp>
        <NuxtLayout>
          <NuxtPage />
        </NuxtLayout>
      </UApp>
    </template>
    

    Conserver le <script setup> existant (useLocaleHead, useHead). cd C:/Users/minit/Desktop/portfolio/portfolio && node -e "require('nodemailer'); require('zod'); console.log('deps OK')" && grep -q "smtpHost" nuxt.config.ts && grep -q "UApp" app/app.vue && echo "PASS" nodemailer et zod installes, site config migree dans app/data/site.ts, runtimeConfig SMTP ajoute (section privee), app.vue wrappe avec UApp

Task 2: Creer les composants partages — sections landing + ProjectCard + TechBadge + ProjectGallery app/components/sections/HeroSection.vue, app/components/sections/ServicesSection.vue, app/components/sections/FeaturedProjectsSection.vue, app/components/sections/TestimonialsSection.vue, app/components/sections/FAQSection.vue, app/components/sections/CTASection.vue, app/components/ProjectCard.vue, app/components/TechBadge.vue, app/components/ProjectGallery.vue Migrer chaque composant depuis src/ vers app/components/ en utilisant Nuxt UI v3 et les auto-imports Nuxt. Pour chaque composant : - Remplacer les imports manuels (`import { useI18n } from '@/composables/useI18n'`) par les auto-imports Nuxt (`const { t } = useI18n()` direct) - Remplacer `RouterLink` par `NuxtLink` - Remplacer les classes CSS custom par du Tailwind + composants Nuxt UI - Remplacer `getImageUrl()` par `NuxtImg` avec `loading="lazy"` et `format="webp"`

HeroSection.vue (per D-02) : Texte seul — titre (t('home.title')), sous-titre (t('home.subtitle')), 3 boutons CTA (Projets, Fiverr, Contact) avec UButton. Pas d'image, pas d'animation.

FeaturedProjectsSection.vue (per D-03) : Utiliser useProjects().featuredProjects pour obtenir les 3 projets featured. Afficher avec ProjectCard. Titre et sous-titre via i18n.

ServicesSection.vue : Migrer les 4 cards services (webDev, mobileApps, optimization, maintenance) avec UCard. Icones via lucide icon names dans UIcon si dispo, sinon SVG inline.

TestimonialsSection.vue (per COMP-04) : Utiliser UCard pour chaque temoignage. Importer testimonials et testimonialsStats depuis ~/data/testimonials. Props i18n pour titre, sous-titre, stats labels. Afficher rating avec etoiles, contenu, nom, role, date.

FAQSection.vue (per COMP-03, D-18) : Utiliser UAccordion avec :items array. Chaque item : { label: t(faq.questionKey), content: t(faq.answerKey), value: faq.questionKey }. Props : faqs: FAQ[], title: string, subtitle: string. Pattern exact du RESEARCH.md Pattern 4. Ajouter type="single" collapsible.

CTASection.vue : Section CTA finale avec titre, sous-titre et bouton UButton vers /contact. Tout i18n.

ProjectCard.vue : Migrer depuis src/. Utiliser NuxtLink vers /project/${project.id}. Afficher image avec NuxtImg, categorie traduite, titre traduit, description traduite, badges technologies (3 max + "+N"), bouton "Voir projet". Schema.org microdata conserve.

TechBadge.vue (per D-17) : Migrer depuis src/. Accepter Technology | string en prop. Lookup dans techStack pour resoudre les strings. Afficher image + nom + niveau optionnel. Utiliser NuxtImg pour les images tech.

ProjectGallery.vue (per D-05, D-06, D-07, COMP-01) : Nouveau composant utilisant UModal + UCarousel du RESEARCH.md Pattern 1. Implementation exacte :

  • Props : gallery: string[], projectTitle: string
  • isOpen ref + currentIndex ref
  • useTemplateRef('carousel') pour acceder a emblaApi
  • openGallery(index) : set currentIndex, isOpen=true, nextTick(() => carouselRef.value?.emblaApi?.scrollTo(index, true))
  • goTo(index) : set currentIndex, scrollTo
  • Navigation clavier : onMounted keydown listener — ArrowRight/ArrowLeft/Escape (per D-07)
  • onUnmounted cleanup du listener
  • Template : UModal v-model:open fullscreen > UCarousel ref="carousel" :items="gallery" arrows loop > NuxtImg :src="item"
  • Thumbnails sous le carousel : boutons avec NuxtImg :src="img" width="80" height="60", ring-2 ring-primary sur le courant (per D-06)
  • Expose openGallery via defineExpose({ openGallery }) pour que la page parent puisse l'appeler cd C:/Users/minit/Desktop/portfolio/portfolio && ls app/components/sections/HeroSection.vue app/components/sections/ServicesSection.vue app/components/sections/FeaturedProjectsSection.vue app/components/sections/TestimonialsSection.vue app/components/sections/FAQSection.vue app/components/sections/CTASection.vue app/components/ProjectCard.vue app/components/TechBadge.vue app/components/ProjectGallery.vue && echo "ALL FILES EXIST" 9 composants crees : 6 sections landing + ProjectCard + TechBadge + ProjectGallery avec UModal+UCarousel+thumbnails+keyboard nav
Task 3: Creer ContactForm + server route nodemailer SMTP app/components/ContactForm.vue, server/api/contact.post.ts **server/api/contact.post.ts** (per D-11, COMP-02) : Route serveur Nuxt avec nodemailer. Implementation exacte du RESEARCH.md Pattern 3 : ```typescript import nodemailer from 'nodemailer'

export default defineEventHandler(async (event) => { const body = await readBody(event) const config = useRuntimeConfig(event)

// Validation cote serveur (per RESEARCH.md Security) const { name, email, message } = body if (!name || typeof name !== 'string' || name.length < 2 || name.length > 100) { throw createError({ statusCode: 400, message: 'Invalid name' }) } if (!email || typeof email !== 'string' || !email.includes('@') || email.length > 200) { throw createError({ statusCode: 400, message: 'Invalid email' }) } if (!message || typeof message !== 'string' || message.length < 10 || message.length > 5000) { throw createError({ statusCode: 400, message: 'Invalid message' }) }

const transporter = nodemailer.createTransport({ host: config.smtpHost, port: 465, secure: true, auth: { user: config.smtpUser, pass: config.smtpPass }, })

await transporter.sendMail({ from: "Portfolio" <${config.smtpUser}>, to: config.smtpTo, subject: Contact portfolio - ${name}, text: De: ${name} <${email}>\n\n${message}, html: <p><strong>De:</strong> ${name} &lt;${email}&gt;</p><p>${message.replace(/\n/g, '<br>')}</p>, })

return { success: true } })

IMPORTANT : `server/` a la racine du projet (meme niveau que `app/`), PAS dans `app/server/` (per RESEARCH.md Pitfall 5).

**ContactForm.vue** (per D-08, D-09, D-10, COMP-02) : Implementation exacte du RESEARCH.md Pattern 2 :
- Schema Zod : name min(2), email .email(), message min(10) — 3 champs seulement (per D-08)
- State reactive `Partial<Schema>` avec name, email, message undefined
- `useToast()` pour feedback (per D-10) — succes en vert, erreur en rouge
- `$fetch('/api/contact', { method: 'POST', body: event.data })` sur submit
- Loading state sur le bouton submit
- Template : `UForm :schema :state @submit` > 3x `UFormField` > `UInput` pour nom/email + `UTextarea rows="5"` pour message > `UButton type="submit" :loading`
- Labels i18n : `t('contact.form.name')`, `t('contact.form.email')`, `t('contact.form.message')`, `t('contact.form.submit')`
- Toast messages i18n : `t('contact.form.success')`, `t('contact.form.error')`
  </action>
  <verify>
    <automated>cd C:/Users/minit/Desktop/portfolio/portfolio && ls server/api/contact.post.ts app/components/ContactForm.vue && grep -q "nodemailer" server/api/contact.post.ts && grep -q "UForm" app/components/ContactForm.vue && grep -q "zod" app/components/ContactForm.vue && echo "PASS"</automated>
  </verify>
  <done>ContactForm.vue cree avec UForm+Zod+UToast, server/api/contact.post.ts cree avec nodemailer SMTP + validation serveur</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| client -> server/api/contact.post.ts | Donnees formulaire non fiables traversent vers le serveur SMTP |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-03-01 | Spoofing | server/api/contact.post.ts | mitigate | Validation Zod cote client + validation longueur/type cote serveur dans readBody |
| T-03-02 | Tampering | server/api/contact.post.ts HTML email | mitigate | Echapper le HTML dans le corps email — remplacer newlines par `<br>` mais ne pas injecter de HTML brut utilisateur |
| T-03-03 | Information Disclosure | nuxt.config.ts runtimeConfig | mitigate | Credentials SMTP dans section privee runtimeConfig uniquement (jamais public) |
| T-03-04 | Denial of Service | server/api/contact.post.ts | accept | Pas de rate limiting en Phase 3 — endpoint public, risque de spam faible pour un portfolio. Mitigation partielle : validation longueur message max 5000 chars |
</threat_model>

<verification>
- `npm run build` passe sans erreur TypeScript
- `npx nuxi dev` demarre et les composants sont auto-importes
- `curl -X POST http://localhost:3000/api/contact -H "Content-Type: application/json" -d '{"name":"Test","email":"test@test.com","message":"Test message long enough"}' ` retourne `{"success":true}` (avec .env SMTP configure) ou une erreur SMTP lisible
</verification>

<success_criteria>
- nodemailer et zod dans package.json dependencies
- app/data/site.ts exporte siteConfig type
- 9 composants sections/partages existent dans app/components/
- ProjectGallery utilise UModal + UCarousel + thumbnails + keydown listener
- ContactForm utilise UForm + Zod schema + useToast
- server/api/contact.post.ts utilise nodemailer avec runtimeConfig prive
- app.vue contient UApp wrapper
- nuxt.config.ts contient smtpHost/smtpUser/smtpPass/smtpTo dans runtimeConfig (pas public)
</success_criteria>

<output>
After completion, create `.planning/phases/03-pages-ship/03-01-SUMMARY.md`
</output>