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). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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 |
|
true |
|
|
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 }
-
Creer
app/data/site.tsen migrant le contenu desrc/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/). ExportersiteConfiget les interfacesSiteConfig,ContactInfo,SocialLink,FiverrService,FiverrConfig. -
Ajouter les interfaces manquantes dans
shared/types/index.ts:SiteConfig,ContactInfo,SocialLink,FiverrService,FiverrConfig(ou les exporter depuisapp/data/site.tsdirectement — au choix du plus simple). -
Mettre a jour
nuxt.config.tspour 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).
-
Mettre a jour
app/app.vuepour wrapper avec<UApp>— requis pour queuseToast()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
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 isOpenref +currentIndexrefuseTemplateRef('carousel')pour acceder aemblaApiopenGallery(index): set currentIndex, isOpen=true,nextTick(() => carouselRef.value?.emblaApi?.scrollTo(index, true))goTo(index): set currentIndex, scrollTo- Navigation clavier :
onMountedkeydown listener — ArrowRight/ArrowLeft/Escape (per D-07) onUnmountedcleanup 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
openGalleryviadefineExpose({ 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
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} <${email}></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>