3e38ea02b1
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>
316 lines
17 KiB
Markdown
316 lines
17 KiB
Markdown
---
|
|
phase: 03-pages-ship
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- 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
|
|
autonomous: true
|
|
requirements:
|
|
- COMP-01
|
|
- COMP-02
|
|
- COMP-03
|
|
- COMP-04
|
|
must_haves:
|
|
truths:
|
|
- "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"
|
|
artifacts:
|
|
- path: "app/components/ProjectGallery.vue"
|
|
provides: "UModal + UCarousel gallery with thumbnails and keyboard nav"
|
|
- path: "app/components/ContactForm.vue"
|
|
provides: "UForm + Zod validated contact form"
|
|
- path: "server/api/contact.post.ts"
|
|
provides: "Nodemailer SMTP server route"
|
|
- path: "app/components/sections/FAQSection.vue"
|
|
provides: "UAccordion FAQ section"
|
|
- path: "app/components/sections/TestimonialsSection.vue"
|
|
provides: "Testimonials with UCard"
|
|
key_links:
|
|
- from: "app/components/ContactForm.vue"
|
|
to: "server/api/contact.post.ts"
|
|
via: "$fetch('/api/contact', { method: 'POST' })"
|
|
pattern: "\\$fetch.*api/contact"
|
|
- from: "app/components/ProjectGallery.vue"
|
|
to: "UModal + UCarousel"
|
|
via: "v-model:open + useTemplateRef"
|
|
pattern: "UModal|UCarousel"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<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>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
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:
|
|
```typescript
|
|
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):
|
|
```typescript
|
|
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 }
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Installer deps, migrer site config, ajouter UApp, configurer runtimeConfig SMTP</name>
|
|
<files>package.json, package-lock.json, app/data/site.ts, shared/types/index.ts, nuxt.config.ts, app/app.vue</files>
|
|
<action>
|
|
1. Installer les dependances :
|
|
```bash
|
|
npm install nodemailer zod
|
|
npm install --save-dev @types/nodemailer
|
|
```
|
|
|
|
2. 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`.
|
|
|
|
3. 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).
|
|
|
|
4. Mettre a jour `nuxt.config.ts` pour ajouter le runtimeConfig SMTP prive (per D-11, D-13) :
|
|
```typescript
|
|
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).
|
|
|
|
5. Mettre a jour `app/app.vue` pour wrapper avec `<UApp>` — requis pour que `useToast()` fonctionne (per D-10, RESEARCH.md Pitfall 1) :
|
|
```vue
|
|
<template>
|
|
<UApp>
|
|
<NuxtLayout>
|
|
<NuxtPage />
|
|
</NuxtLayout>
|
|
</UApp>
|
|
</template>
|
|
```
|
|
Conserver le `<script setup>` existant (useLocaleHead, useHead).
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<done>nodemailer et zod installes, site config migree dans app/data/site.ts, runtimeConfig SMTP ajoute (section privee), app.vue wrappe avec UApp</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Creer les composants partages — sections landing + ProjectCard + TechBadge + ProjectGallery</name>
|
|
<files>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</files>
|
|
<action>
|
|
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
|
|
</action>
|
|
<verify>
|
|
<automated>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"</automated>
|
|
</verify>
|
|
<done>9 composants crees : 6 sections landing + ProjectCard + TechBadge + ProjectGallery avec UModal+UCarousel+thumbnails+keyboard nav</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 3: Creer ContactForm + server route nodemailer SMTP</name>
|
|
<files>app/components/ContactForm.vue, server/api/contact.post.ts</files>
|
|
<action>
|
|
**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} <${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>
|