Landing Page Upgrade Implementation Plan
DerinFor agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add ComparisonTable, DB-backed Testimonials system (with admin CRUD + customer review form), TrustGuarantee section, FAQ refund policy, homepage reorder, and cleanup of deprecated components.
Architecture: Three new landing page sections (ComparisonTable, Testimonials, TrustGuarantee) integrated into the existing Next.js App Router + Prisma + next-intl stack. Testimonials use a new Prisma model with Server Actions for admin CRUD and an API route for public review submission. Static components use i18n for all translatable content.
Tech Stack: Next.js 16 (App Router), React 19, Prisma (PostgreSQL), next-intl, Tailwind CSS v4, Lucide React
Spec: docs/superpowers/specs/2026-04-02-landing-page-upgrade-design.md
Style Guide: docs/style_guide.md
File Structure
Bölüm başlığı “File Structure”New Files
Bölüm başlığı “New Files”| File | Responsibility |
|---|---|
components/ComparisonTable.tsx | Feature + Price hybrid comparison section |
components/TrustGuarantee.tsx | ”Try Before You Buy” trust section |
components/admin/TestimonialForm.tsx | Admin testimonial create/edit form |
app/[locale]/admin/(panel)/testimonials/page.tsx | Admin testimonials list |
app/[locale]/admin/(panel)/testimonials/create/page.tsx | Admin create testimonial |
app/[locale]/admin/(panel)/testimonials/[id]/edit/page.tsx | Admin edit testimonial |
app/[locale]/review/page.tsx | Public customer review form |
app/api/reviews/route.ts | Public POST for customer review submission |
Modified Files
Bölüm başlığı “Modified Files”| File | Change |
|---|---|
prisma/schema.prisma | Add Testimonial model |
prisma/seed.ts | Add testimonial seed data |
app/lib/actions.ts | Add testimonial CRUD server actions |
components/admin/AdminSidebar.tsx | Add “Müşteri Yorumları” menu item |
components/Testimonials.tsx | Rewrite: DB-backed instead of i18n-driven |
app/[locale]/page.tsx | Add new sections, reorder, fetch testimonials |
messages/en.json | Add ComparisonTable, TrustGuarantee namespaces; update FAQ, Testimonials |
components/Pricing.tsx | Remove commented-out PricingComparisonTable import |
Deleted Files
Bölüm başlığı “Deleted Files”| File | Reason |
|---|---|
components/PricingComparisonTable.tsx | Replaced by ComparisonTable |
components/MoneyBackGuarantee.tsx | Dangerous for source-code sales, replaced by TrustGuarantee |
Task 1: Prisma Model + Migration
Bölüm başlığı “Task 1: Prisma Model + Migration”Files:
-
Modify:
prisma/schema.prisma -
Step 1: Add Testimonial model to schema
Add at the end of prisma/schema.prisma:
// ─── Testimonials ──────────────────────────────────────────model Testimonial { id Int @id @default(autoincrement()) name String company String? country String // ISO 3166-1 alpha-2 (e.g. "IT", "DE", "GB") rating Int @default(5) text String photoUrl String? approved Boolean @default(false) featured Boolean @default(false) locale String @default("en") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
@@index([approved, featured])}- Step 2: Generate and apply migration
Run: cd /var/www/vhosts/ecutuningportal.com/httpdocs && npx prisma migrate dev --name add-testimonial-model
Expected: Migration created and applied, prisma/migrations/XXXXXX_add_testimonial_model/ directory created.
- Step 3: Generate Prisma client
Run: npx prisma generate
Expected: Prisma Client generated successfully
- Step 4: Commit
git add prisma/schema.prisma prisma/migrations/git commit -m "feat(db): Testimonial modeli eklendi"Task 2: Seed Data
Bölüm başlığı “Task 2: Seed Data”Files:
-
Modify:
prisma/seed.ts -
Step 1: Add testimonial seed data
Add at the end of the main() function in prisma/seed.ts, before the closing }:
// ─── Testimonials ───────────────────────────────────────── const existingTestimonials = await prisma.testimonial.count(); if (existingTestimonials === 0) { await prisma.testimonial.createMany({ data: [ { name: 'Marco R.', company: 'AutoTune Milano', country: 'IT', rating: 5, text: 'The portal paid for itself in the first week. My customers love the professional interface and the automated invoicing saves me hours every day.', approved: true, featured: true, locale: 'en', }, { name: 'Stefan K.', company: 'ChipPower GmbH', country: 'DE', rating: 5, text: 'Switched from a €149/month SaaS platform. Saved thousands in the first year alone and now I own the source code. Best investment for my business.', approved: true, featured: true, locale: 'en', }, { name: 'James T.', company: 'RevoRemap UK', country: 'GB', rating: 5, text: '66K vehicle database and 24-language support out of the box. Exactly what our international client base needed. Setup was done in under a day.', approved: true, featured: true, locale: 'en', }, { name: 'Aleksander W.', company: 'TurboChip', country: 'PL', rating: 5, text: 'The credit system and white-label branding are outstanding. Our customers think we built the portal ourselves. Truly professional solution.', approved: true, featured: true, locale: 'en', }, { name: 'Carlos M.', company: 'SpeedTune', country: 'ES', rating: 5, text: 'From purchase to going live in 48 hours. The portal looks incredibly professional and the payment integration with Stripe and PayPal works flawlessly.', approved: true, featured: true, locale: 'en', }, { name: 'Ahmed H.', company: 'ProRemap', country: 'AE', rating: 5, text: 'With 24 languages and RTL support, we serve clients across the Middle East and Europe from a single portal. The multi-currency credit system is a game changer.', approved: true, featured: true, locale: 'en', }, ], }); console.log('✅ Testimonials seeded (6 entries)'); } else { console.log(`⏭️ Testimonials already exist (${existingTestimonials}), skipping`); }- Step 2: Run seed
Run: npx prisma db seed
Expected: ✅ Testimonials seeded (6 entries)
- Step 3: Commit
git add prisma/seed.tsgit commit -m "feat(db): Testimonial seed data eklendi (6 uluslararası yorum)"Task 3: ComparisonTable — i18n + Component
Bölüm başlığı “Task 3: ComparisonTable — i18n + Component”Files:
-
Modify:
messages/en.json(add ComparisonTable namespace) -
Create:
components/ComparisonTable.tsx -
Step 1: Add ComparisonTable namespace to en.json
Add this namespace to messages/en.json (after the PricingComparison namespace or at end):
"ComparisonTable": { "badge": "// Compare & Save", "title": "WHY PAY <highlight>MORE?</highlight>", "subtitle": "3-year total cost comparison — one-time vs recurring", "savings": "SAVE UP TO €7,500+ OVER 3 YEARS", "cta": "Get Your Portal — €499", "disclaimer": "* Prices based on industry averages. SaaS: typical chiptuning SaaS platforms. Agency: UK/EU development agencies.", "columns": { "feature": "Feature", "saas": "SaaS Platforms", "portal": "ECU Tuning Portal", "agency": "Agency Solutions" }, "saas_desc": "Monthly subscription", "agency_desc": "Setup + monthly fees", "portal_desc": "One-time payment. Forever.", "saas_price": "€3,564+", "agency_price": "€8,000+", "portal_price": "€499", "saas_calc": "€99/mo × 36 months", "agency_calc": "€4K setup + €100/mo support + update fees", "features": { "pricing_model": "Pricing Model", "source_code": "Source Code", "white_label": "White-Label", "vehicle_db": "Vehicle Database", "payment": "Payment Integration", "language": "Multi-Language", "credit_system": "Credit & Subscription", "updates": "Updates", "vendor_lock": "Vendor Lock-in" }, "data": { "saas": { "pricing_model": "€99–149/month", "pricing_model_sub": "locked in forever", "source_code": "false", "source_code_sub": "no access", "white_label": "partial", "white_label_sub": "limited branding", "vehicle_db": "partial", "vehicle_db_sub": "basic models", "payment": "1–2 methods", "payment_sub": "", "language": "2–5 languages", "language_sub": "", "credit_system": "partial", "credit_system_sub": "basic only", "updates": "included", "updates_sub": "while subscribed", "vendor_lock": "HIGH", "vendor_lock_sub": "stop paying = lose access" }, "portal": { "pricing_model": "€499 ONE-TIME", "pricing_model_sub": "no recurring fees", "source_code": "true", "source_code_sub": "full ownership", "white_label": "true", "white_label_sub": "100% your brand", "vehicle_db": "true", "vehicle_db_sub": "66,000+ vehicles", "payment": "Stripe, PayPal, SEPA, Bank", "payment_sub": "", "language": "24 Languages", "language_sub": "", "credit_system": "true", "credit_system_sub": "credits + subscriptions", "updates": "€99/year", "updates_sub": "optional", "vendor_lock": "NONE", "vendor_lock_sub": "your code, your server" }, "agency": { "pricing_model": "€4,000+ setup", "pricing_model_sub": "+ monthly retainer", "source_code": "false", "source_code_sub": "agency owns it", "white_label": "true", "white_label_sub": "custom built", "vehicle_db": "partial", "vehicle_db_sub": "varies", "payment": "custom quote", "payment_sub": "", "language": "extra cost per lang", "language_sub": "", "credit_system": "custom quote", "credit_system_sub": "", "updates": "€500+ per update", "updates_sub": "", "vendor_lock": "HIGH", "vendor_lock_sub": "agency dependency" } }}- Step 2: Create ComparisonTable component
Create components/ComparisonTable.tsx. This is a 'use client' component using useTranslations('ComparisonTable'), Lucide icons, and the industrial design system (clip-path, Chakra Petch headings, JetBrains Mono data). Key structure:
- Bar chart zone: 3 flex items with proportional height CSS (portal bar ~50px, saas ~160px, agency ~280px)
- Savings banner: hexagonal clip-path, teal colors
- Feature table: 9 rows, 4 columns (feature name, saas, portal, agency)
- Portal column:
ecu.svglogo in header via<Image src="/assets/images/logo/ecu.svg" /> - Cell renderer:
"true"→ teal Check,"false"→ gray X,"partial"→ amber Minus, text → JetBrains Mono - Row icons:
DollarSign, Code, Layers, Database, CreditCard, Globe, Wallet, RefreshCw, Lockfrom Lucide - Mobile: horizontal scroll-snap card swiper (like existing PricingComparisonTable mobile pattern)
- Desktop: full table
- CTA button at bottom:
btn-industrialwith brand-red
The component should be around 200-250 lines. Follow the exact visual design from the approved mockup at .superpowers/brainstorm/1936757-1775173059/content/comparison-mockup-b2.html.
Reference files for patterns:
-
Card style:
globals.css.card-accent,.btn-industrial,.badge-industrial -
Check/cross icons: See style guide section 5
-
Mobile swiper: See
components/PricingComparisonTable.tsxlines 56-133 for the mobile card swiper pattern -
Step 3: Verify TypeScript compiles
Run: npx tsc --noEmit --pretty 2>&1 | head -20
Expected: No errors related to ComparisonTable.
- Step 4: Commit
git add components/ComparisonTable.tsx messages/en.jsongit commit -m "feat: ComparisonTable section eklendi (Feature + Price Hybrid)"Task 4: TrustGuarantee — i18n + Component
Bölüm başlığı “Task 4: TrustGuarantee — i18n + Component”Files:
-
Modify:
messages/en.json(add TrustGuarantee namespace) -
Create:
components/TrustGuarantee.tsx -
Step 1: Add TrustGuarantee namespace to en.json
"TrustGuarantee": { "badge": "// Zero Risk", "title": "TRY BEFORE <highlight>YOU BUY</highlight>", "subtitle": "Request a demo, explore every module, then decide.", "cards": { "demo": { "title": "Live Demo", "description": "Full portal walkthrough before purchase" }, "source_code": { "title": "Source Code", "description": "Full ownership — no vendor lock-in" }, "pricing": { "title": "Transparent Pricing", "description": "€499 one-time — no hidden fees" } }, "cta": "Request Demo"}- Step 2: Create TrustGuarantee component
Create components/TrustGuarantee.tsx. A 'use client' component with:
- Section header: badge-industrial + Chakra Petch title + mono subtitle
- 3 cards in a horizontal flex/grid:
card-accent-tealstyle - Each card: Lucide icon (Monitor, Code, Shield), title, description
- CTA button linking to
/demo - Clips: industrial clip-path on cards
- Responsive: stack on mobile, 3-col on
md+
This is a simple static component (~80-100 lines).
Icons: import { Monitor, Code, Shield } from 'lucide-react'
- Step 3: Verify TypeScript compiles
Run: npx tsc --noEmit --pretty 2>&1 | head -20
Expected: No errors.
- Step 4: Commit
git add components/TrustGuarantee.tsx messages/en.jsongit commit -m "feat: TrustGuarantee section eklendi (Try Before You Buy)"Task 5: Testimonial Server Actions + API Route
Bölüm başlığı “Task 5: Testimonial Server Actions + API Route”Files:
-
Modify:
app/lib/actions.ts(add testimonial CRUD actions) -
Create:
app/api/reviews/route.ts(public POST for customer reviews) -
Step 1: Add testimonial server actions to actions.ts
Add at the end of app/lib/actions.ts:
// --- Testimonial Actions ---
export async function createTestimonial(prevState: any, formData: FormData) { const admin = await validateAdminSession(); if (!admin) return { message: 'Yetkisiz işlem.' };
try { await prisma.testimonial.create({ data: { name: formData.get('name') as string, company: (formData.get('company') as string) || null, country: formData.get('country') as string, rating: parseInt(formData.get('rating') as string) || 5, text: formData.get('text') as string, photoUrl: (formData.get('photoUrl') as string) || null, approved: formData.get('approved') === 'on', featured: formData.get('featured') === 'on', locale: (formData.get('locale') as string) || 'en', }, }); } catch (error) { console.error('Create Testimonial Error:', error); return { message: 'Yorum oluşturulamadı.' }; }
revalidatePath('/[locale]/admin/testimonials'); revalidatePath('/[locale]'); const locale = await getLocaleFromReferer(); redirect(`/${locale}/admin/testimonials`);}
export async function updateTestimonial(id: number, prevState: any, formData: FormData) { const admin = await validateAdminSession(); if (!admin) return { message: 'Yetkisiz işlem.' };
try { await prisma.testimonial.update({ where: { id }, data: { name: formData.get('name') as string, company: (formData.get('company') as string) || null, country: formData.get('country') as string, rating: parseInt(formData.get('rating') as string) || 5, text: formData.get('text') as string, photoUrl: (formData.get('photoUrl') as string) || null, approved: formData.get('approved') === 'on', featured: formData.get('featured') === 'on', locale: (formData.get('locale') as string) || 'en', }, }); } catch (error) { console.error('Update Testimonial Error:', error); return { message: 'Yorum güncellenemedi.' }; }
revalidatePath('/[locale]/admin/testimonials'); revalidatePath('/[locale]'); const locale = await getLocaleFromReferer(); redirect(`/${locale}/admin/testimonials`);}
export async function deleteTestimonial(id: number) { const admin = await validateAdminSession(); if (!admin) return { message: 'Yetkisiz işlem.' };
try { await prisma.testimonial.delete({ where: { id } }); revalidatePath('/[locale]/admin/testimonials'); revalidatePath('/[locale]'); return { message: 'Yorum silindi.' }; } catch { return { message: 'Yorum silinemedi.' }; }}
export async function toggleTestimonialApproval(id: number) { const admin = await validateAdminSession(); if (!admin) return { message: 'Yetkisiz işlem.' };
try { const testimonial = await prisma.testimonial.findUnique({ where: { id } }); if (!testimonial) return { message: 'Yorum bulunamadı.' };
await prisma.testimonial.update({ where: { id }, data: { approved: !testimonial.approved }, }); revalidatePath('/[locale]/admin/testimonials'); revalidatePath('/[locale]'); return { message: testimonial.approved ? 'Yorum onayı kaldırıldı.' : 'Yorum onaylandı.' }; } catch { return { message: 'İşlem başarısız.' }; }}
export async function toggleTestimonialFeatured(id: number) { const admin = await validateAdminSession(); if (!admin) return { message: 'Yetkisiz işlem.' };
try { const testimonial = await prisma.testimonial.findUnique({ where: { id } }); if (!testimonial) return { message: 'Yorum bulunamadı.' };
await prisma.testimonial.update({ where: { id }, data: { featured: !testimonial.featured }, }); revalidatePath('/[locale]/admin/testimonials'); revalidatePath('/[locale]'); return { message: testimonial.featured ? 'Öne çıkarma kaldırıldı.' : 'Öne çıkarıldı.' }; } catch { return { message: 'İşlem başarısız.' }; }}- Step 2: Create public review API route
Create app/api/reviews/route.ts:
import { NextRequest, NextResponse } from 'next/server';import prisma from '@/lib/prisma';import { rateLimit } from '@/lib/rate-limit';
export async function POST(request: NextRequest) { const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || request.headers.get('x-real-ip') || 'unknown';
const { allowed } = rateLimit(ip, { maxRequests: 3, windowMs: 24 * 60 * 60 * 1000 }); if (!allowed) { return NextResponse.json( { error: 'Too many submissions. Please try again later.' }, { status: 429 } ); }
try { const body = await request.json(); const { name, company, country, rating, text, honeypot } = body;
// Honeypot — silently accept if filled if (honeypot) { return NextResponse.json({ message: 'Thank you for your review!' }, { status: 201 }); }
// Validation if (!name || typeof name !== 'string' || name.trim().length < 2) { return NextResponse.json({ error: 'Name is required (min 2 characters).' }, { status: 400 }); } if (!text || typeof text !== 'string' || text.trim().length < 20) { return NextResponse.json({ error: 'Review text must be at least 20 characters.' }, { status: 400 }); } if (text.trim().length > 500) { return NextResponse.json({ error: 'Review text must be at most 500 characters.' }, { status: 400 }); } if (!country || typeof country !== 'string' || country.length !== 2) { return NextResponse.json({ error: 'Valid country code is required.' }, { status: 400 }); } const parsedRating = parseInt(String(rating)); if (isNaN(parsedRating) || parsedRating < 1 || parsedRating > 5) { return NextResponse.json({ error: 'Rating must be between 1 and 5.' }, { status: 400 }); }
await prisma.testimonial.create({ data: { name: name.trim(), company: company?.trim() || null, country: country.toUpperCase(), rating: parsedRating, text: text.trim(), approved: false, featured: false, locale: 'en', }, });
return NextResponse.json( { message: 'Thank you! Your review will be published after approval.' }, { status: 201 } ); } catch (error) { console.error('Review submission error:', error); return NextResponse.json({ error: 'An error occurred.' }, { status: 500 }); }}- Step 3: Verify TypeScript compiles
Run: npx tsc --noEmit --pretty 2>&1 | head -20
- Step 4: Commit
git add app/lib/actions.ts app/api/reviews/route.tsgit commit -m "feat: Testimonial server actions + public review API eklendi"Task 6: Admin Testimonials Pages
Bölüm başlığı “Task 6: Admin Testimonials Pages”Files:
-
Create:
components/admin/TestimonialForm.tsx -
Create:
app/[locale]/admin/(panel)/testimonials/page.tsx -
Create:
app/[locale]/admin/(panel)/testimonials/create/page.tsx -
Create:
app/[locale]/admin/(panel)/testimonials/[id]/edit/page.tsx -
Modify:
components/admin/AdminSidebar.tsx -
Step 1: Create TestimonialForm component
Create components/admin/TestimonialForm.tsx. A 'use client' form component following the FaqForm pattern:
- Fields: name (text), company (text), country (select dropdown with ISO codes), rating (1-5 number), text (textarea), photoUrl (text), locale (select), approved (checkbox), featured (checkbox)
- Uses
useActionState(React 19) with the passed server action - Country dropdown: common countries (DE, GB, IT, ES, FR, NL, PL, TR, AE, US, etc.)
- Submit button with loading state
Reference: components/admin/FaqForm.tsx for the exact pattern (useActionState, form structure, styling).
- Step 2: Create admin testimonials list page
Create app/[locale]/admin/(panel)/testimonials/page.tsx. A Server Component that:
- Calls
validateAdminSession()at top - Fetches
prisma.testimonial.findMany({ orderBy: { createdAt: 'desc' } }) - Renders a table with columns: Name, Company, Country (flag emoji from ISO code), Rating (stars), Approved (green/red badge), Featured (yellow badge), Actions
- Each row has: Approve toggle form (calls
toggleTestimonialApproval), Featured toggle form (callstoggleTestimonialFeatured), Edit link, Delete form (callsdeleteTestimonial) - “Yeni Yorum Ekle” link to
/admin/testimonials/create
Country code to flag emoji helper (inline):
const countryFlag = (code: string) => String.fromCodePoint(...[...code.toUpperCase()].map(c => 0x1F1E6 + c.charCodeAt(0) - 65));Reference: app/[locale]/admin/(panel)/faq/page.tsx for the Server Component + inline Server Action pattern.
- Step 3: Create admin create page
Create app/[locale]/admin/(panel)/testimonials/create/page.tsx:
import { createTestimonial } from '@/app/lib/actions';import TestimonialForm from '@/components/admin/TestimonialForm';
export default function CreateTestimonialPage() { return ( <div className="max-w-2xl mx-auto"> <h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-6"> Yeni Müşteri Yorumu </h1> <TestimonialForm action={createTestimonial} /> </div> );}- Step 4: Create admin edit page
Create app/[locale]/admin/(panel)/testimonials/[id]/edit/page.tsx:
import prisma from '@/lib/prisma';import { updateTestimonial } from '@/app/lib/actions';import TestimonialForm from '@/components/admin/TestimonialForm';import { notFound } from 'next/navigation';
export default async function EditTestimonialPage({ params,}: { params: Promise<{ id: string; locale: string }>;}) { const { id } = await params; const testimonial = await prisma.testimonial.findUnique({ where: { id: parseInt(id) }, });
if (!testimonial) notFound();
const updateWithId = updateTestimonial.bind(null, testimonial.id);
return ( <div className="max-w-2xl mx-auto"> <h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-6"> Yorumu Düzenle </h1> <TestimonialForm action={updateWithId} initialData={testimonial} /> </div> );}- Step 5: Add testimonials link to AdminSidebar
In components/admin/AdminSidebar.tsx, add Star to the Lucide import:
import { LayoutDashboard, FileText, LogOut, Menu, X, HelpCircle, Inbox, BarChart2, Monitor, Users, Wrench, FolderOpen, CreditCard, Code, Zap, ShieldCheck, ImageIcon, UserCog, Activity, MessageSquareText, Tags, Award, Star,} from 'lucide-react';Add to menuItems array, after the 'SSS Yönetimi' entry:
{ name: 'Müşteri Yorumları', href: '/admin/testimonials', icon: Star },- Step 6: Verify TypeScript compiles
Run: npx tsc --noEmit --pretty 2>&1 | head -20
- Step 7: Commit
git add components/admin/TestimonialForm.tsx \ app/[locale]/admin/\(panel\)/testimonials/ \ components/admin/AdminSidebar.tsxgit commit -m "feat(admin): Testimonial yönetim sayfaları eklendi (list/create/edit)"Task 7: Customer Review Page
Bölüm başlığı “Task 7: Customer Review Page”Files:
-
Create:
app/[locale]/review/page.tsx -
Modify:
messages/en.json(add Review namespace) -
Step 1: Add Review namespace to en.json
"Review": { "meta_title": "Write a Review", "meta_desc": "Share your experience with ECU Tuning Portal", "title": "SHARE YOUR <highlight>EXPERIENCE</highlight>", "subtitle": "Help other professionals make the right choice", "form": { "name": "Your Name", "name_placeholder": "e.g. John D.", "company": "Company (optional)", "company_placeholder": "e.g. AutoTune GmbH", "country": "Country", "country_select": "Select your country", "rating": "Rating", "review": "Your Review", "review_placeholder": "Tell us about your experience with ECU Tuning Portal...", "review_hint": "Minimum 20 characters", "submit": "Submit Review", "submitting": "Submitting..." }, "success": { "title": "Thank You!", "message": "Your review has been submitted and will be published after approval." }, "errors": { "generic": "Something went wrong. Please try again.", "rate_limit": "Too many submissions. Please try again later." }}- Step 2: Create review page
Create app/[locale]/review/page.tsx. A hybrid page with server metadata + client form:
- Server part:
generateMetadatafor SEO - Client form component (can be inline
'use client'or separate):- Industrial dark styling matching the landing page
- Fields: name, company, country dropdown (with flag emojis), star rating picker (clickable), text (textarea with char counter)
- Hidden honeypot:
<input type="text" name="honeypot" className="hidden" tabIndex={-1} autoComplete="off" /> - Submits to
/api/reviewsviafetchPOST - Shows success state (green card) on 201, error on 4xx/5xx
- Uses
useTranslations('Review')for all strings
Since Next.js App Router pages with generateMetadata must be server components, split into:
-
app/[locale]/review/page.tsx— server component with metadata, renders<ReviewForm /> -
app/[locale]/review/ReviewForm.tsx— client component with the form -
Step 3: Verify TypeScript compiles
Run: npx tsc --noEmit --pretty 2>&1 | head -20
- Step 4: Commit
git add app/[locale]/review/ messages/en.jsongit commit -m "feat: Müşteri yorum sayfası eklendi (/review)"Task 8: Rewrite Testimonials Landing Component
Bölüm başlığı “Task 8: Rewrite Testimonials Landing Component”Files:
-
Modify:
components/Testimonials.tsx(full rewrite) -
Modify:
messages/en.json(update Testimonials namespace) -
Step 1: Update Testimonials namespace in en.json
Replace the existing Testimonials namespace. Keep UI strings, remove the reviews array (data now comes from DB):
"Testimonials": { "badge": "// Testimonials", "title": "TRUSTED BY RESELLERS <highlight>WORLDWIDE</highlight>", "description": "What our customers say about ECU Tuning Portal", "prev_review_aria": "Previous review", "next_review_aria": "Next review", "go_to_review_aria": "Go to review {index}", "write_review": "Write a Review", "no_reviews": "No reviews yet. Be the first to share your experience!"}- Step 2: Rewrite Testimonials component
Rewrite components/Testimonials.tsx to accept reviews as props instead of reading from i18n:
'use client';
import { useTranslations } from 'next-intl';// ... other imports
interface TestimonialData { id: number; name: string; company: string | null; country: string; rating: number; text: string;}
interface TestimonialsProps { reviews: TestimonialData[];}
export default function Testimonials({ reviews }: TestimonialsProps) { const t = useTranslations('Testimonials'); // ... component logic using reviews prop instead of t.raw('reviews')}Keep the existing mobile swiper + desktop grid layout. Update card design to:
- Country flag emoji (convert ISO code to flag:
String.fromCodePoint(...[...country].map(c => 0x1F1E6 + c.charCodeAt(0) - 65))) - Star rating (1-5, using
brand-amberfor filled,gray-600for empty) - Industrial card style (clip-path,
card-accent-teal) - “Write a Review” link at the bottom pointing to
/review
Also handle the empty state: if reviews.length === 0, show t('no_reviews').
Important: The layout.tsx uses t.raw('reviews') from Testimonials namespace for JSON-LD structured data. Since we’re removing the reviews array from i18n, update app/[locale]/layout.tsx to no longer extract Testimonials.reviews for schema. Instead, the review schema in layout.tsx should be removed or fetched from DB separately. For now, simply remove the testimonialMessages / testimonialReviews / reviewSchemas logic from layout.tsx and set 'review': [] in the JSON-LD SoftwareApplication schema. Real reviews can be added to schema later.
- Step 3: Verify TypeScript compiles
Run: npx tsc --noEmit --pretty 2>&1 | head -20
- Step 4: Commit
git add components/Testimonials.tsx messages/en.json app/[locale]/layout.tsxgit commit -m "feat: Testimonials DB-backed olarak yeniden yazıldı"Task 9: FAQ Refund Policy
Bölüm başlığı “Task 9: FAQ Refund Policy”Files:
-
Modify:
messages/en.json(add refund policy Q&A to FAQ namespace) -
Modify:
app/[locale]/page.tsx(add to faqKeys array) -
Step 1: Add refund policy FAQ to en.json
Add to the FAQ namespace in messages/en.json:
"question_refund_policy": "What is your refund policy?","answer_refund_policy": "As a digital product with full source code delivery, refunds are not available after purchase. However, we offer a comprehensive live demo before purchase so you can explore every feature and make an informed decision. We want you to be 100% confident before you buy."- Step 2: Add to faqKeys in page.tsx
In app/[locale]/page.tsx, add 'refund_policy' to the faqKeys array:
const faqKeys = [ 'domain', 'web_design', 'winols_evc', 'setup_time', 'migrate_data', 'payment_methods', 'ads_management', 'rental_purchase', 'refund_policy',];- Step 3: Commit
git add messages/en.json app/[locale]/page.tsxgit commit -m "feat(faq): iade politikası maddesi eklendi"Task 10: Homepage Integration + Cleanup
Bölüm başlığı “Task 10: Homepage Integration + Cleanup”Files:
-
Modify:
app/[locale]/page.tsx(add new sections, reorder, fetch testimonials) -
Modify:
components/Pricing.tsx(remove commented-out PricingComparisonTable) -
Delete:
components/PricingComparisonTable.tsx -
Delete:
components/MoneyBackGuarantee.tsx -
Step 1: Add dynamic imports for new components in page.tsx
Add after the existing dynamic imports in app/[locale]/page.tsx:
const ComparisonTable = dynamic(() => import('@/components/ComparisonTable'), { loading: () => <SectionLoader />});const Testimonials = dynamic(() => import('@/components/Testimonials'), { loading: () => <SectionLoader />});const TrustGuarantee = dynamic(() => import('@/components/TrustGuarantee'), { loading: () => <SectionLoader />});- Step 2: Fetch testimonials in server component
Add to the Home function body in app/[locale]/page.tsx, after the heroImages query:
// Fetch approved testimonials for landing pageconst approvedTestimonials = await prisma.testimonial.findMany({ where: { approved: true }, orderBy: [{ featured: 'desc' }, { createdAt: 'desc' }], take: 6, select: { id: true, name: true, company: true, country: true, rating: true, text: true },});- Step 3: Reorder sections in JSX
Update the return JSX in the Home component to the new order. The existing safeJsonLd script tag stays. New section order:
Hero → Brands → PricingSection → ComparisonTable → HowItWorks → Features →WhiteLabelCustomization → BeforeAfter → VehicleDatabaseSection → LanguageSupport →Testimonials (with reviews={approvedTestimonials}) → Stats → TrustGuarantee →About → Founder → SecurityEnterprise → FAQ → Contact → SocialProofPopupWrap new components in <LazySection>:
-
<LazySection><ComparisonTable /></LazySection> -
<LazySection><Testimonials reviews={approvedTestimonials} /></LazySection> -
<LazySection><TrustGuarantee /></LazySection> -
Step 4: Clean up Pricing.tsx
In components/Pricing.tsx, remove the commented-out {/* <PricingComparisonTable /> */} line and any import of PricingComparisonTable.
- Step 5: Delete deprecated components
rm components/PricingComparisonTable.tsxrm components/MoneyBackGuarantee.tsx- Step 6: Verify no broken imports
Run: npx tsc --noEmit --pretty 2>&1 | grep -i "error" | head -10
Fix any import errors (other files referencing deleted components).
- Step 7: Commit
git add -Agit commit -m "feat: homepage yeni section sıralaması + eski komponentler silindi
- ComparisonTable, Testimonials, TrustGuarantee homepage'e eklendi- PricingComparisonTable ve MoneyBackGuarantee silindi- Yeni section sıralaması uygulandı"Task 11: i18n Translations (23 languages)
Bölüm başlığı “Task 11: i18n Translations (23 languages)”Files:
-
Modify: all
messages/*.jsonfiles (23 languages) -
Step 1: Run smart translate script
The project has scripts/smart-translate.ts that uses Google GenAI to translate from English to all other languages.
Run: npx tsx scripts/smart-translate.ts
This should translate the new namespaces (ComparisonTable, TrustGuarantee, Review) and updated namespaces (Testimonials, FAQ) to all 23 other languages.
If the script requires specific arguments, check: head -50 scripts/smart-translate.ts to understand the interface.
- Step 2: Verify a sample translation
Run: node -e "const d=require('./messages/de.json'); console.log(d.ComparisonTable?.title, d.TrustGuarantee?.title, d.FAQ?.question_refund_policy)"
Expected: German translations for all three values.
- Step 3: Commit
git add messages/git commit -m "feat(i18n): yeni section'lar 24 dile çevrildi"Task 12: Build Verification
Bölüm başlığı “Task 12: Build Verification”- Step 1: Run full build
Run: cd /var/www/vhosts/ecutuningportal.com/httpdocs && npm run build 2>&1 | tail -30
Expected: Build succeeds with no errors. Warnings are acceptable.
If build fails: Read the error output carefully. Common issues:
-
Missing imports → fix the import path
-
Type errors → check prop types match between parent and child
-
i18n key mismatches → verify en.json has all referenced keys
-
Step 2: Restart PM2
Run: pm2 restart ecutuningportal
Expected: Process restarts successfully.
- Step 3: Verify pages load
Run: curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/
Expected: 200
Run: curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/review
Expected: 200
Run: curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/admin/testimonials
Expected: 302 (redirect to login) or 200 (if already authenticated)
- Step 4: Final commit (if any fixes were needed)
git add -Agit commit -m "fix: build hataları düzeltildi"