İçeriğe geç

Landing Page Upgrade Implementation Plan

Derin

For 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


FileResponsibility
components/ComparisonTable.tsxFeature + Price hybrid comparison section
components/TrustGuarantee.tsx”Try Before You Buy” trust section
components/admin/TestimonialForm.tsxAdmin testimonial create/edit form
app/[locale]/admin/(panel)/testimonials/page.tsxAdmin testimonials list
app/[locale]/admin/(panel)/testimonials/create/page.tsxAdmin create testimonial
app/[locale]/admin/(panel)/testimonials/[id]/edit/page.tsxAdmin edit testimonial
app/[locale]/review/page.tsxPublic customer review form
app/api/reviews/route.tsPublic POST for customer review submission
FileChange
prisma/schema.prismaAdd Testimonial model
prisma/seed.tsAdd testimonial seed data
app/lib/actions.tsAdd testimonial CRUD server actions
components/admin/AdminSidebar.tsxAdd “Müşteri Yorumları” menu item
components/Testimonials.tsxRewrite: DB-backed instead of i18n-driven
app/[locale]/page.tsxAdd new sections, reorder, fetch testimonials
messages/en.jsonAdd ComparisonTable, TrustGuarantee namespaces; update FAQ, Testimonials
components/Pricing.tsxRemove commented-out PricingComparisonTable import
FileReason
components/PricingComparisonTable.tsxReplaced by ComparisonTable
components/MoneyBackGuarantee.tsxDangerous for source-code sales, replaced by TrustGuarantee

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
Terminal window
git add prisma/schema.prisma prisma/migrations/
git commit -m "feat(db): Testimonial modeli eklendi"

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
Terminal window
git add prisma/seed.ts
git commit -m "feat(db): Testimonial seed data eklendi (6 uluslararası yorum)"

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.svg logo 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, Lock from Lucide
  • Mobile: horizontal scroll-snap card swiper (like existing PricingComparisonTable mobile pattern)
  • Desktop: full table
  • CTA button at bottom: btn-industrial with 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.tsx lines 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
Terminal window
git add components/ComparisonTable.tsx messages/en.json
git commit -m "feat: ComparisonTable section eklendi (Feature + Price Hybrid)"

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-teal style
  • 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
Terminal window
git add components/TrustGuarantee.tsx messages/en.json
git commit -m "feat: TrustGuarantee section eklendi (Try Before You Buy)"

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
Terminal window
git add app/lib/actions.ts app/api/reviews/route.ts
git commit -m "feat: Testimonial server actions + public review API eklendi"

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:

  1. Calls validateAdminSession() at top
  2. Fetches prisma.testimonial.findMany({ orderBy: { createdAt: 'desc' } })
  3. Renders a table with columns: Name, Company, Country (flag emoji from ISO code), Rating (stars), Approved (green/red badge), Featured (yellow badge), Actions
  4. Each row has: Approve toggle form (calls toggleTestimonialApproval), Featured toggle form (calls toggleTestimonialFeatured), Edit link, Delete form (calls deleteTestimonial)
  5. “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
Terminal window
git add components/admin/TestimonialForm.tsx \
app/[locale]/admin/\(panel\)/testimonials/ \
components/admin/AdminSidebar.tsx
git commit -m "feat(admin): Testimonial yönetim sayfaları eklendi (list/create/edit)"

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: generateMetadata for 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/reviews via fetch POST
    • 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
Terminal window
git add app/[locale]/review/ messages/en.json
git commit -m "feat: Müşteri yorum sayfası eklendi (/review)"

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-amber for filled, gray-600 for 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
Terminal window
git add components/Testimonials.tsx messages/en.json app/[locale]/layout.tsx
git commit -m "feat: Testimonials DB-backed olarak yeniden yazıldı"

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
Terminal window
git add messages/en.json app/[locale]/page.tsx
git commit -m "feat(faq): iade politikası maddesi eklendi"

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 page
const 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 → SocialProofPopup

Wrap 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
Terminal window
rm components/PricingComparisonTable.tsx
rm 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
Terminal window
git add -A
git 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ı"

Files:

  • Modify: all messages/*.json files (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
Terminal window
git add messages/
git commit -m "feat(i18n): yeni section'lar 24 dile çevrildi"

  • 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)
Terminal window
git add -A
git commit -m "fix: build hataları düzeltildi"