From 155e5ae6e40cbe41854c53cd140efd7f742b0d6a Mon Sep 17 00:00:00 2001 From: tetiana zorii Date: Sat, 31 Jan 2026 03:19:19 -0500 Subject: [PATCH 1/3] (SP:3) feat(i18n): translate about page and auth form validation messages - Add about page translations (EN, UK, PL) - Add auth.fields.validation translations for form errors --- frontend/app/[locale]/about/page.tsx | 13 +- frontend/app/api/ai/explain/route.ts | 197 ++++++++++++++++-- .../components/about/CommunitySection.tsx | 29 +-- frontend/components/about/FeaturesSection.tsx | 126 +++++------ frontend/components/about/HeroSection.tsx | 39 ++-- frontend/components/about/InteractiveGame.tsx | 50 ++--- frontend/components/about/PricingSection.tsx | 99 +++++---- frontend/components/about/SponsorsWall.tsx | 20 +- frontend/components/about/TopicsSection.tsx | 21 +- .../components/auth/fields/EmailField.tsx | 15 ++ frontend/components/auth/fields/NameField.tsx | 13 ++ .../components/auth/fields/PasswordField.tsx | 17 ++ frontend/messages/en.json | 167 ++++++++++++++- frontend/messages/pl.json | 170 ++++++++++++++- frontend/messages/uk.json | 172 ++++++++++++++- 15 files changed, 937 insertions(+), 211 deletions(-) diff --git a/frontend/app/[locale]/about/page.tsx b/frontend/app/[locale]/about/page.tsx index 5165b909..57c93a75 100644 --- a/frontend/app/[locale]/about/page.tsx +++ b/frontend/app/[locale]/about/page.tsx @@ -1,3 +1,4 @@ +import { getTranslations } from "next-intl/server" import { getPlatformStats } from "@/lib/about/stats" import { getSponsors } from "@/lib/about/github-sponsors" @@ -7,6 +8,14 @@ import { FeaturesSection } from "@/components/about/FeaturesSection" import { PricingSection } from "@/components/about/PricingSection" import { CommunitySection } from "@/components/about/CommunitySection" +export async function generateMetadata() { + const t = await getTranslations("about") + return { + title: t("metaTitle"), + description: t("metaDescription"), + } +} + export default async function AboutPage() { const [stats, sponsors] = await Promise.all([ getPlatformStats(), @@ -17,13 +26,13 @@ export default async function AboutPage() {
- + - +
) } \ No newline at end of file diff --git a/frontend/app/api/ai/explain/route.ts b/frontend/app/api/ai/explain/route.ts index 115ce801..9cd05f7b 100644 --- a/frontend/app/api/ai/explain/route.ts +++ b/frontend/app/api/ai/explain/route.ts @@ -9,6 +9,88 @@ import { } from '@/lib/ai/prompts'; import { getClientIp } from '@/lib/security/client-ip'; +// ============================================================================= +// DEBUG LOGGING - TEMPORARY (remove after debugging) +// ============================================================================= +function logEnvironmentDiagnostics() { + const apiKey = process.env.GROQ_API_KEY; + console.log('=== NETLIFY FUNCTION DIAGNOSTICS ==='); + console.log('[ENV] GROQ_API_KEY exists:', !!apiKey); + console.log('[ENV] GROQ_API_KEY prefix:', apiKey ? apiKey.substring(0, 4) + '****' : 'N/A'); + console.log('[ENV] GROQ_API_KEY length:', apiKey?.length ?? 0); + console.log('[ENV] NODE_ENV:', process.env.NODE_ENV); + console.log('[ENV] NETLIFY:', process.env.NETLIFY); + console.log('[ENV] AWS_LAMBDA_FUNCTION_NAME:', process.env.AWS_LAMBDA_FUNCTION_NAME); + console.log('[ENV] CONTEXT:', process.env.CONTEXT); // Netlify deploy context + console.log('[ENV] DEPLOY_URL:', process.env.DEPLOY_URL); + console.log('[RUNTIME] Expected: nodejs (set via export)'); + console.log('[ENV] All GROQ-related vars:', Object.keys(process.env).filter(k => k.toLowerCase().includes('groq'))); + console.log('===================================='); +} + +function logRequestDiagnostics(request: NextRequest) { + console.log('=== REQUEST DIAGNOSTICS ==='); + console.log('[REQ] Method:', request.method); + console.log('[REQ] URL:', request.url); + console.log('[REQ] Headers:'); + request.headers.forEach((value, key) => { + // Redact sensitive headers + const safeValue = ['authorization', 'cookie', 'x-api-key'].includes(key.toLowerCase()) + ? '[REDACTED]' + : value; + console.log(` ${key}: ${safeValue}`); + }); + console.log('==========================='); +} + +function logBodyParsingResult(success: boolean, body?: unknown, error?: unknown) { + console.log('=== BODY PARSING ==='); + console.log('[BODY] Parse success:', success); + if (success && body) { + console.log('[BODY] Parsed body:', JSON.stringify(body, null, 2)); + } + if (error) { + console.log('[BODY] Parse error:', error instanceof Error ? error.message : String(error)); + } + console.log('===================='); +} + +function logGroqInitialization(success: boolean, error?: unknown) { + console.log('=== GROQ SDK INITIALIZATION ==='); + console.log('[GROQ] Init success:', success); + if (error) { + const err = error as Error & { status?: number; code?: string }; + console.log('[GROQ] Init error name:', err.name); + console.log('[GROQ] Init error message:', err.message); + console.log('[GROQ] Init error status:', err.status); + console.log('[GROQ] Init error code:', err.code); + } + console.log('==============================='); +} + +function logGroqApiCall(phase: 'start' | 'success' | 'error', details?: unknown) { + console.log(`=== GROQ API CALL (${phase.toUpperCase()}) ===`); + if (phase === 'start') { + console.log('[GROQ] Starting API call to llama3-70b-8192'); + } else if (phase === 'success') { + console.log('[GROQ] API call successful'); + if (details && typeof details === 'object' && 'choices' in details) { + const response = details as { choices?: Array<{ message?: { content?: string } }> }; + console.log('[GROQ] Response has content:', !!response.choices?.[0]?.message?.content); + console.log('[GROQ] Content length:', response.choices?.[0]?.message?.content?.length ?? 0); + } + } else if (phase === 'error') { + const err = details as Error & { status?: number; code?: string; headers?: Record }; + console.log('[GROQ] API error name:', err?.name); + console.log('[GROQ] API error message:', err?.message); + console.log('[GROQ] API error status:', err?.status); + console.log('[GROQ] API error code:', err?.code); + console.log('[GROQ] API error stack:', err?.stack?.substring(0, 500)); + } + console.log('====================================='); +} +// ============================================================================= + const rateLimiter = new Map(); const MAX_REQUESTS_PER_WINDOW = 10; const RATE_LIMIT_WINDOW_MS = 20 * 60 * 1000; @@ -91,20 +173,33 @@ function parseExplanationResponse(content: string): ExplanationResponse { } export async function POST(request: NextRequest) { + // DEBUG: Log environment diagnostics on every request + logEnvironmentDiagnostics(); + logRequestDiagnostics(request); + const apiKey = process.env.GROQ_API_KEY; if (!apiKey) { - console.error('GROQ_API_KEY is not configured'); - console.error('Available env vars starting with GROQ:', - Object.keys(process.env).filter(k => k.startsWith('GROQ')) - ); + console.error('[FATAL] GROQ_API_KEY is not configured'); + console.error('[DEBUG] All env var keys:', Object.keys(process.env).sort().join(', ')); return NextResponse.json( - { error: 'AI service not configured', code: 'SERVICE_UNAVAILABLE' }, + { + error: 'AI service not configured', + code: 'SERVICE_UNAVAILABLE', + debug: { + hasKey: false, + nodeEnv: process.env.NODE_ENV, + isNetlify: !!process.env.NETLIFY, + context: process.env.CONTEXT, + } + }, { status: 503 } ); } const clientIp = getClientIp(request) ?? 'unknown'; + console.log('[DEBUG] Client IP:', clientIp); const rateLimit = checkRateLimit(clientIp); + console.log('[DEBUG] Rate limit check:', rateLimit); if (!rateLimit.allowed) { const resetMinutes = Math.ceil(rateLimit.resetIn / 60000); @@ -128,7 +223,9 @@ export async function POST(request: NextRequest) { let body: unknown; try { body = await request.json(); - } catch { + logBodyParsingResult(true, body); + } catch (parseError) { + logBodyParsingResult(false, undefined, parseError); return NextResponse.json( { error: 'Invalid JSON body', code: 'INVALID_JSON' }, { status: 400 } @@ -137,6 +234,7 @@ export async function POST(request: NextRequest) { const validationResult = requestSchema.safeParse(body); if (!validationResult.success) { + console.log('[DEBUG] Validation failed:', validationResult.error.format()); return NextResponse.json( { error: 'Invalid request', @@ -148,12 +246,33 @@ export async function POST(request: NextRequest) { } const { term, context } = validationResult.data; + console.log('[DEBUG] Validated request - term:', term, 'context:', context?.substring(0, 50)); - const groq = new Groq({ apiKey }); + // DEBUG: Wrap Groq client initialization in try/catch + let groq: Groq; + try { + groq = new Groq({ apiKey }); + logGroqInitialization(true); + } catch (initError) { + logGroqInitialization(false, initError); + return NextResponse.json( + { + error: 'Failed to initialize AI client', + code: 'SDK_INIT_ERROR', + debug: { + errorName: initError instanceof Error ? initError.name : 'Unknown', + errorMessage: initError instanceof Error ? initError.message : String(initError), + } + }, + { status: 503 } + ); + } try { const prompt = createExplainPrompt({ term, context }); + console.log('[DEBUG] Prompt created, length:', prompt.length); + logGroqApiCall('start'); const chatCompletion = await groq.chat.completions.create({ messages: [ { @@ -166,14 +285,18 @@ export async function POST(request: NextRequest) { max_tokens: 1500, top_p: 1, }); + logGroqApiCall('success', chatCompletion); const content = chatCompletion.choices[0]?.message?.content; if (!content) { + console.error('[ERROR] No content in Groq response'); throw new Error('No content in response'); } + console.log('[DEBUG] Parsing response, raw content length:', content.length); const explanation = parseExplanationResponse(content); + console.log('[DEBUG] Successfully parsed explanation'); return NextResponse.json(explanation, { status: 200, @@ -184,17 +307,23 @@ export async function POST(request: NextRequest) { }, }); } catch (error) { + logGroqApiCall('error', error); console.error('Groq API error:', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const errorName = error instanceof Error ? error.name : 'UnknownError'; - console.error('Error details:', { + // Enhanced error logging + const errorDetails = { name: errorName, message: errorMessage, stack: error instanceof Error ? error.stack : undefined, - }); + status: (error as { status?: number }).status, + code: (error as { code?: string }).code, + type: (error as { type?: string }).type, + }; + console.error('[DEBUG] Full error details:', JSON.stringify(errorDetails, null, 2)); if (error instanceof Error) { if ( @@ -203,7 +332,11 @@ export async function POST(request: NextRequest) { error.message.includes('Invalid API Key') ) { return NextResponse.json( - { error: 'AI service authentication failed', code: 'AUTH_ERROR' }, + { + error: 'AI service authentication failed', + code: 'AUTH_ERROR', + debug: { keyPrefix: apiKey.substring(0, 4) } + }, { status: 503 } ); } @@ -232,10 +365,50 @@ export async function POST(request: NextRequest) { { error: 'Failed to generate explanation', code: 'AI_ERROR', - details: - process.env.NODE_ENV === 'development' ? errorMessage : undefined, + details: errorMessage, // Always include for debugging (remove in production) + debug: { + errorName, + nodeEnv: process.env.NODE_ENV, + isNetlify: !!process.env.NETLIFY, + } }, { status: 500 } ); } } + +// ============================================================================= +// DEBUG: GET handler for diagnostics (catches 405 errors, provides health check) +// Remove after debugging +// ============================================================================= +export async function GET(request: NextRequest) { + console.log('=== GET REQUEST TO /api/ai/explain (should be POST) ==='); + logEnvironmentDiagnostics(); + logRequestDiagnostics(request); + + const apiKey = process.env.GROQ_API_KEY; + + return NextResponse.json({ + message: 'This endpoint requires POST method', + debug: { + endpoint: '/api/ai/explain', + expectedMethod: 'POST', + receivedMethod: 'GET', + timestamp: new Date().toISOString(), + environment: { + hasGroqApiKey: !!apiKey, + groqApiKeyPrefix: apiKey ? apiKey.substring(0, 4) + '****' : null, + groqApiKeyLength: apiKey?.length ?? 0, + nodeEnv: process.env.NODE_ENV, + isNetlify: !!process.env.NETLIFY, + netlifyContext: process.env.CONTEXT, + awsLambdaFunction: process.env.AWS_LAMBDA_FUNCTION_NAME, + deployUrl: process.env.DEPLOY_URL, + runtime: 'nodejs', + }, + allGroqEnvVars: Object.keys(process.env).filter(k => + k.toLowerCase().includes('groq') + ), + } + }, { status: 200 }); // Return 200 for diagnostics, not 405 +} diff --git a/frontend/components/about/CommunitySection.tsx b/frontend/components/about/CommunitySection.tsx index bd402a44..91c38756 100644 --- a/frontend/components/about/CommunitySection.tsx +++ b/frontend/components/about/CommunitySection.tsx @@ -2,31 +2,34 @@ import { Github, ArrowRight, ExternalLink, MessageCircle } from "lucide-react" import Link from "next/link" +import { useTranslations } from "next-intl" import { TESTIMONIALS, type Testimonial } from "@/data/about" import { GradientBadge } from "@/components/ui/gradient-badge" import { SectionHeading } from "@/components/ui/section-heading" export function CommunitySection() { + const t = useTranslations("about.community") + return (
- +
- +
- - - + +
- +
@@ -60,7 +63,7 @@ export function CommunitySection() { hover:text-[#1e5eff] dark:hover:text-[#ff2d55]" > - Join Discussion + {t("joinDiscussion")}
@@ -77,7 +80,7 @@ export function CommunitySection() { hover:shadow-[0_0_30px_-5px_rgba(30,94,255,0.15)] dark:hover:shadow-[0_0_30px_-5px_rgba(255,45,85,0.15)]" > - Have a success story or feature request? + {t("successStory")} - Join Discussion + {t("joinDiscussion")}

- We read every single thread + {t("readThreads")}

diff --git a/frontend/components/about/FeaturesSection.tsx b/frontend/components/about/FeaturesSection.tsx index 84eec101..093bd2c9 100644 --- a/frontend/components/about/FeaturesSection.tsx +++ b/frontend/components/about/FeaturesSection.tsx @@ -3,7 +3,8 @@ import { useState, useEffect } from "react" import { motion, AnimatePresence, useReducedMotion } from "framer-motion" import { Link } from "@/i18n/routing" -import { +import { useTranslations } from "next-intl" +import { MessageCircle, Brain, Trophy, User, ShoppingBag, BookOpen, Globe, Cpu, Shield, Languages, Clock, Lightbulb, Target, Medal, Users, Flame, Zap, BarChart3, TrendingUp, Tag, History, @@ -15,8 +16,8 @@ import { SectionHeading } from "@/components/ui/section-heading" interface Feature { icon: LucideIcon - label: string - desc: string + labelKey: string + descKey: string x: number y: number size: number @@ -29,26 +30,6 @@ interface Page { features: Feature[] } -const translations: Record = { - "title": "Features designed to", - "titleHighlight": "make you unstoppable", - "subtitle": "Stop searching for scattered info. We curated the ultimate toolkit to crack technical interviews.", - "qa.title": "Q&A", - "qa.description": "No fluff. Just a massive library of real interview questions with precise, recruiter-approved answers.", - "quiz.title": "Quizzes", - "quiz.description": "Validate your confidence. Fast-paced interactive quizzes to spot your weak points before the interviewer does.", - "leaderboard.title": "Leaderboard", - "leaderboard.description": "Gamify your prep. Earn points for every correct answer, keep your streak alive, and rank up against others.", - "blog.title": "Blog", - "blog.description": "Stay updated with detailed articles on tech trends, coding tutorials, and industry insights to keep you ahead of the curve.", - "profile.title": "Analytics", - "profile.description": "Don't fly blind. Visualize your progress with detailed charts to see exactly which topics you've mastered.", - "shop.title": "Shop", - "shop.description": "Upgrade your setup. High-quality developer apparel, desk accessories, and digital assets available for purchase.", -} - -const t = (key: string) => translations[key] || key - const decorativeDots = [ { x: '5%', y: '20%', size: 8 }, { x: '10%', y: '75%', size: 6 }, @@ -70,10 +51,10 @@ const pages: Page[] = [ icon: MessageCircle, href: "/q&a", features: [ - { icon: Globe, label: "3 Languages", desc: "EN, UK & PL supported", x: -120, y: -120, size: 88 }, - { icon: Cpu, label: "AI Helper", desc: "Select text for AI explain", x: 120, y: -120, size: 88 }, - { icon: Lightbulb, label: "Smart Cache", desc: "Highlights learned terms", x: -120, y: 120, size: 88 }, - { icon: Search, label: "Tech Filter", desc: "React, Git, JS & more", x: 120, y: 120, size: 88 }, + { icon: Globe, labelKey: "bubbles.qa.languages.label", descKey: "bubbles.qa.languages.desc", x: -120, y: -120, size: 88 }, + { icon: Cpu, labelKey: "bubbles.qa.aiHelper.label", descKey: "bubbles.qa.aiHelper.desc", x: 120, y: -120, size: 88 }, + { icon: Lightbulb, labelKey: "bubbles.qa.smartCache.label", descKey: "bubbles.qa.smartCache.desc", x: -120, y: 120, size: 88 }, + { icon: Search, labelKey: "bubbles.qa.techFilter.label", descKey: "bubbles.qa.techFilter.desc", x: 120, y: 120, size: 88 }, ] }, { @@ -81,10 +62,10 @@ const pages: Page[] = [ icon: Brain, href: "/quizzes", features: [ - { icon: Clock, label: "Smart Timer", desc: "Race against the total time", x: -120, y: -120, size: 88 }, - { icon: Shield, label: "Anti-Cheat", desc: "Focus loss detection", x: 120, y: -120, size: 88 }, - { icon: Save, label: "Auto Sync", desc: "Saves progress post-login", x: -120, y: 120, size: 88 }, - { icon: BarChart3, label: "Tracking", desc: "Best scores & attempts", x: 120, y: 120, size: 88 }, + { icon: Clock, labelKey: "bubbles.quiz.smartTimer.label", descKey: "bubbles.quiz.smartTimer.desc", x: -120, y: -120, size: 88 }, + { icon: Shield, labelKey: "bubbles.quiz.antiCheat.label", descKey: "bubbles.quiz.antiCheat.desc", x: 120, y: -120, size: 88 }, + { icon: Save, labelKey: "bubbles.quiz.autoSync.label", descKey: "bubbles.quiz.autoSync.desc", x: -120, y: 120, size: 88 }, + { icon: BarChart3, labelKey: "bubbles.quiz.tracking.label", descKey: "bubbles.quiz.tracking.desc", x: 120, y: 120, size: 88 }, ] }, { @@ -92,10 +73,10 @@ const pages: Page[] = [ icon: Trophy, href: "/leaderboard", features: [ - { icon: Medal, label: "The Podium", desc: "Top 3 exclusive spotlight", x: -120, y: -120, size: 88 }, - { icon: Globe, label: "Global Rank", desc: "Compete worldwide", x: 120, y: -120, size: 88 }, - { icon: Zap, label: "XP System", desc: "Points for every answer", x: -120, y: 120, size: 88 }, - { icon: Activity, label: "Live Feed", desc: "Real-time rank updates", x: 120, y: 120, size: 88 }, + { icon: Medal, labelKey: "bubbles.leaderboard.podium.label", descKey: "bubbles.leaderboard.podium.desc", x: -120, y: -120, size: 88 }, + { icon: Globe, labelKey: "bubbles.leaderboard.globalRank.label", descKey: "bubbles.leaderboard.globalRank.desc", x: 120, y: -120, size: 88 }, + { icon: Zap, labelKey: "bubbles.leaderboard.xpSystem.label", descKey: "bubbles.leaderboard.xpSystem.desc", x: -120, y: 120, size: 88 }, + { icon: Activity, labelKey: "bubbles.leaderboard.liveFeed.label", descKey: "bubbles.leaderboard.liveFeed.desc", x: 120, y: 120, size: 88 }, ] }, { @@ -103,10 +84,10 @@ const pages: Page[] = [ icon: User, href: "/dashboard", features: [ - { icon: BarChart3, label: "Stats Hub", desc: "Visualize your growth", x: -120, y: -120, size: 88 }, - { icon: History, label: "History", desc: "Track learning streaks", x: 120, y: -120, size: 88 }, - { icon: UserCircle, label: "Identity", desc: "Manage role & profile", x: -120, y: 120, size: 88 }, - { icon: Bell, label: "Reminders", desc: "Finish incomplete quizzes", x: 120, y: 120, size: 88 }, + { icon: BarChart3, labelKey: "bubbles.profile.statsHub.label", descKey: "bubbles.profile.statsHub.desc", x: -120, y: -120, size: 88 }, + { icon: History, labelKey: "bubbles.profile.history.label", descKey: "bubbles.profile.history.desc", x: 120, y: -120, size: 88 }, + { icon: UserCircle, labelKey: "bubbles.profile.identity.label", descKey: "bubbles.profile.identity.desc", x: -120, y: 120, size: 88 }, + { icon: Bell, labelKey: "bubbles.profile.reminders.label", descKey: "bubbles.profile.reminders.desc", x: 120, y: 120, size: 88 }, ] }, { @@ -114,10 +95,10 @@ const pages: Page[] = [ icon: BookOpen, href: "/blog", features: [ - { icon: TrendingUp, label: "Tech Trends", desc: "Stay ahead of the curve", x: -120, y: -120, size: 88 }, - { icon: PenTool, label: "Tutorials", desc: "Step-by-step guides", x: 120, y: -120, size: 88 }, - { icon: BookOpen, label: "Deep Dives", desc: "In-depth analysis", x: -120, y: 120, size: 88 }, - { icon: Users, label: "Community", desc: "Written by developers", x: 120, y: 120, size: 88 }, + { icon: TrendingUp, labelKey: "bubbles.blog.techTrends.label", descKey: "bubbles.blog.techTrends.desc", x: -120, y: -120, size: 88 }, + { icon: PenTool, labelKey: "bubbles.blog.tutorials.label", descKey: "bubbles.blog.tutorials.desc", x: 120, y: -120, size: 88 }, + { icon: BookOpen, labelKey: "bubbles.blog.deepDives.label", descKey: "bubbles.blog.deepDives.desc", x: -120, y: 120, size: 88 }, + { icon: Users, labelKey: "bubbles.blog.community.label", descKey: "bubbles.blog.community.desc", x: 120, y: 120, size: 88 }, ] }, { @@ -125,24 +106,24 @@ const pages: Page[] = [ icon: ShoppingBag, href: "/shop", features: [ - { icon: Sparkles, label: "New Drops", desc: "Regular fresh content", x: -120, y: -120, size: 88 }, - { icon: Tag, label: "Curated", desc: "Dev-focused collections", x: 120, y: -120, size: 88 }, - { icon: CreditCard, label: "Checkout", desc: "Seamless Stripe flow", x: -120, y: 120, size: 88 }, - { icon: Package, label: "Premium", desc: "High-quality material", x: 120, y: 120, size: 88 }, + { icon: Sparkles, labelKey: "bubbles.shop.newDrops.label", descKey: "bubbles.shop.newDrops.desc", x: -120, y: -120, size: 88 }, + { icon: Tag, labelKey: "bubbles.shop.curated.label", descKey: "bubbles.shop.curated.desc", x: 120, y: -120, size: 88 }, + { icon: CreditCard, labelKey: "bubbles.shop.checkout.label", descKey: "bubbles.shop.checkout.desc", x: -120, y: 120, size: 88 }, + { icon: Package, labelKey: "bubbles.shop.premium.label", descKey: "bubbles.shop.premium.desc", x: 120, y: 120, size: 88 }, ] }, ] -function FeatureBubble({ feature, index, href, isMobile }: { feature: Feature; index: number; href: string; isMobile: boolean }) { +function FeatureBubble({ feature, index, href, isMobile, t }: { feature: Feature; index: number; href: string; isMobile: boolean; t: (key: string) => string }) { const Icon = feature.icon const floatDelay = index * 0.5 const floatDuration = 3 + index * 0.3 - + const scaleX = isMobile ? 0.55 : 1 const scaleY = isMobile ? 1.3 : 1 const posX = feature.x * scaleX const posY = feature.y * scaleY - + const shouldReduceMotion = useReducedMotion() return ( @@ -150,10 +131,10 @@ function FeatureBubble({ feature, index, href, isMobile }: { feature: Feature; i className="absolute" style={{ left: '50%', top: '50%' }} initial={{ x: '-50%', y: '-50%', opacity: 0, scale: 0 }} - animate={{ - x: `calc(-50% + ${posX}%)`, + animate={{ + x: `calc(-50% + ${posX}%)`, y: `calc(-50% + ${posY}%)`, - opacity: 1, + opacity: 1, scale: 1, }} exit={{ x: '-50%', y: '-50%', opacity: 0, scale: 0 }} @@ -164,20 +145,20 @@ function FeatureBubble({ feature, index, href, isMobile }: { feature: Feature; i scale: { delay: 0.1 + index * 0.08, duration: 0.5, type: "spring", bounce: 0.4 }, }} > -
-
-
{feature.label}
-
{feature.desc}
+
{t(feature.labelKey)}
+
{t(feature.descKey)}
@@ -275,16 +256,18 @@ function ConnectingLines() { ) } -function TabButton({ - page, - isActive, +function TabButton({ + page, + isActive, onClick, - onKeyDown -}: { - page: Page; - isActive: boolean; - onClick: () => void + onKeyDown, + t +}: { + page: Page; + isActive: boolean; + onClick: () => void onKeyDown: (e: React.KeyboardEvent) => void + t: (key: string) => string }) { return (
diff --git a/frontend/components/about/PricingSection.tsx b/frontend/components/about/PricingSection.tsx index d7490235..cd0d0233 100644 --- a/frontend/components/about/PricingSection.tsx +++ b/frontend/components/about/PricingSection.tsx @@ -3,46 +3,63 @@ import { useState } from "react" import { motion } from "framer-motion" import { Check, Heart, X, Sparkles, Server, ArrowRight } from "lucide-react" import Link from "next/link" -import type { Sponsor } from "@/lib/about/github-sponsors" +import { useTranslations } from "next-intl" +import type { Sponsor } from "@/lib/about/github-sponsors" import { SponsorsWall } from "./SponsorsWall" import { GradientBadge } from "@/components/ui/gradient-badge" import { SectionHeading } from "@/components/ui/section-heading" import { ParticleCanvas } from "@/components/ui/particle-canvas" + interface PricingSectionProps { sponsors?: Sponsor[] } export function PricingSection({ sponsors = [] }: PricingSectionProps) { + const t = useTranslations("about.pricing") const [activeShape, setActiveShape] = useState<"brackets" | "heart" | null>(null) + const juniorFeatures = [ + t("junior.features.unlimited"), + t("junior.features.fullAccess"), + t("junior.features.noCard"), + t("junior.features.noGuilt"), + ] + + const heroFeatures = [ + t("hero.features.servers"), + t("hero.features.coffee"), + t("hero.features.badge"), + t("hero.features.feeling"), + ] + return (
) diff --git a/frontend/components/about/SponsorsWall.tsx b/frontend/components/about/SponsorsWall.tsx index 6ea620c2..56497c08 100644 --- a/frontend/components/about/SponsorsWall.tsx +++ b/frontend/components/about/SponsorsWall.tsx @@ -4,6 +4,7 @@ import { motion } from "framer-motion" import Image from "next/image" import Link from "next/link" import { Plus, Crown, Sparkles } from "lucide-react" +import { useTranslations } from "next-intl" import { cn } from "@/lib/utils" import type { Sponsor } from "@/lib/about/github-sponsors" import { GradientBadge } from "@/components/ui/gradient-badge" @@ -15,15 +16,16 @@ interface SponsorsWallProps { const MAX_SPONSORS = 10 export function SponsorsWall({ sponsors = [] }: SponsorsWallProps) { + const t = useTranslations("about.sponsors") const displaySponsors = sponsors.slice(0, MAX_SPONSORS) return (
- +
- - You? + {t("emptySlot")}
)}
-