diff --git a/.gitignore b/.gitignore index e52d5b7..8fd475c 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ freelancer.instructions.md /lib/generated/prisma # Private Notes notes.md +desktop.ini diff --git a/README.md b/README.md index 7336d4c..8f87564 100644 --- a/README.md +++ b/README.md @@ -198,7 +198,7 @@ Comprehensive API endpoints with security-first design: | `/api/blog`       | Blog content management and retrieval             | Prisma + Zod                       | | `/api/github`     | Fetches GitHub profile + repos (filtered)         | Tokenized (env)                   | | `/api/pagespeed` | Surfaces PageSpeed metrics                         | Enhanced caching + error handling | -| `/api/chatbot`   | Interactive AI chatbot (Reem) for visitor queries | Gemini + Groq fallback             | +| `/api/chatbot`   | Interactive AI chatbot (Reem) for visitor queries | Groq API           | | `/api/admin`     | Administrative operations for content             | Secured endpoints                 | Controls: @@ -345,8 +345,6 @@ Refer to `LICENSE` & `COPYRIGHT` files for formal wording. --- ## 16. Contact -Services: service@yazan-abo-ayash.de -Support: support@yazan-abo-ayash.de Portfolio: https://www.coldbydefault.com Documentation: https://docs.coldbydefault.com/ For professional or security inquiries, reach out via the official channels listed above. diff --git a/SECURITY.md b/SECURITY.md index 3f9d4d8..9331676 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,21 +1,38 @@ # Security Policy +## Overview + +This security policy outlines how we handle security vulnerabilities and the security measures implemented in this portfolio project. + ## Supported Versions -Use this section to tell people about which versions of your project are -currently being supported with security updates. +Currently supported versions with security updates: -| Version | Supported | -| ------- | ------------------ | -| 5.0.x | :white_check_mark: | -| 4.0.x | :white_check_mark: | -| 3.0.x | :white_check_mark: | -| < 2.0 | :x: | +| Version | Supported | Notes | +| ------- | ------------------ | ------------------------------- | +| Latest | :white_check_mark: | Active development and security | +| < 3.0 | :x: | Legacy versions not supported | ## Reporting a Vulnerability -Use this section to tell people how to report a vulnerability. +If you discover a security vulnerability, please follow these steps: + +### Where to Report + +**Email**: See Contact Information. + +## Security Audit History + +| Date | Type | Status | Notes | +| ---------- | -------------- | ------ | ----------------------------- | +| 2026-02-16 | Internal Audit | ✅ | Security improvements applied | + +## Additional Resources + +- [OWASP Top 10](https://owasp.org/www-project-top-ten/) +- [Next.js Security](https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy) +- [Prisma Security](https://www.prisma.io/docs/guides/database/advanced-database-tasks/sql-injection) + +--- -Tell them where to go, how often they can expect to get an update on a -reported vulnerability, what to expect if the vulnerability is accepted or -declined, etc. +**Copyright © 2026 ColdByDefault. All Rights Reserved.** diff --git a/app/(legals)/privacy/page.tsx b/app/(legals)/privacy/page.tsx index dba0f46..f571d38 100644 --- a/app/(legals)/privacy/page.tsx +++ b/app/(legals)/privacy/page.tsx @@ -1,7 +1,7 @@ /** * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Separator } from "@/components/ui/separator"; @@ -92,9 +92,7 @@ export default async function Privacy() {

{t("dataProcessing.vercelTitle")}

-

- {t("dataProcessing.vercelDescription")} -

+

{t("dataProcessing.vercelDescription")}

@@ -112,19 +110,15 @@ export default async function Privacy() {

- {t("chatbot.geminiTitle")} -

-

- {t("chatbot.geminiDescription")} + {t("chatbot.apiTitle")}

+

{t("chatbot.apiDescription")}

{t("chatbot.temporaryTitle")}

-

- {t("chatbot.temporaryDescription")} -

+

{t("chatbot.temporaryDescription")}

@@ -144,9 +138,7 @@ export default async function Privacy() {

{t("booking.calendlyTitle")}

-

- {t("booking.calendlyDescription")} -

+

{t("booking.calendlyDescription")}

diff --git a/app/api/about/route.ts b/app/api/about/route.ts index fe51d9b..847a091 100644 --- a/app/api/about/route.ts +++ b/app/api/about/route.ts @@ -3,11 +3,39 @@ * @copyright 2026 ColdByDefault. All Rights Reserved. */ +import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { aboutData } from "@/data/main/aboutData"; import aboutProfile from "@/data/main/aboutProfile.json"; +import { RateLimiter } from "@/lib/security"; + +// Rate limiter instance: 30 requests per minute +const rateLimiter = new RateLimiter(60000, 30); + +function getClientIP(request: NextRequest): string { + const forwarded = request.headers.get("x-forwarded-for"); + const realIp = request.headers.get("x-real-ip"); + const cfConnectingIp = request.headers.get("cf-connecting-ip"); + return cfConnectingIp || realIp || forwarded?.split(",")[0] || "127.0.0.1"; +} + +export function GET(request: NextRequest) { + // Rate limiting check + const clientIP = getClientIP(request); + if (!rateLimiter.isAllowed(clientIP)) { + return NextResponse.json( + { error: "Too many requests" }, + { + status: 429, + headers: { + "Retry-After": "60", + "X-RateLimit-Limit": "30", + "X-RateLimit-Remaining": "0", + }, + }, + ); + } -export function GET() { try { const combinedData = { ...aboutData, @@ -27,13 +55,19 @@ export function GET() { status: 200, headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=86400", + "X-Content-Type-Options": "nosniff", }, }); } catch (error) { console.error("Error fetching about data:", error); return NextResponse.json( { error: "Failed to fetch about data" }, - { status: 500 } + { + status: 500, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, ); } -} \ No newline at end of file +} diff --git a/app/api/admin/blog/route.ts b/app/api/admin/blog/route.ts index e827e06..50d3a50 100644 --- a/app/api/admin/blog/route.ts +++ b/app/api/admin/blog/route.ts @@ -2,7 +2,7 @@ * Blog Admin API Route * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; @@ -59,7 +59,7 @@ function getClientIP(request: NextRequest): string { function isAuthorized(request: NextRequest): boolean { if (!ADMIN_TOKEN) { - console.error("ADMIN_TOKEN environment variable not set"); + // Security: Don't log sensitive information about environment configuration return false; } @@ -188,7 +188,7 @@ const updateBlogSchema = z.object({ }); export async function GET( - request: NextRequest + request: NextRequest, ): Promise> { if (!isAuthorized(request)) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -218,7 +218,7 @@ export async function GET( const page = parseInt(searchParams.get("page") || "1", 10); const limit = Math.min( parseInt(searchParams.get("limit") || "20", 10), - 50 + 50, ); const search = searchParams.get("search") || undefined; const language = searchParams.get("language") || undefined; @@ -226,14 +226,14 @@ export async function GET( searchParams.get("published") === "true" ? true : searchParams.get("published") === "false" - ? false - : undefined; + ? false + : undefined; const featured = searchParams.get("featured") === "true" ? true : searchParams.get("featured") === "false" - ? false - : undefined; + ? false + : undefined; const queryParams: Partial = { page, @@ -248,7 +248,7 @@ export async function GET( const result = await getAdminBlogs( context, - queryParams as BlogListQuery + queryParams as BlogListQuery, ); return NextResponse.json({ success: true, data: result }); @@ -258,7 +258,7 @@ export async function GET( if (!blogId) { return NextResponse.json( { error: "Blog ID is required" }, - { status: 400 } + { status: 400 }, ); } @@ -266,7 +266,7 @@ export async function GET( if (!blog) { return NextResponse.json( { error: "Blog not found" }, - { status: 404 } + { status: 404 }, ); } @@ -304,7 +304,7 @@ export async function GET( } export async function POST( - request: NextRequest + request: NextRequest, ): Promise> { if (!isAuthorized(request)) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); @@ -324,7 +324,7 @@ export async function POST( if (!contentType?.includes("application/json")) { return NextResponse.json( { error: "Content-Type must be application/json" }, - { status: 400 } + { status: 400 }, ); } @@ -334,7 +334,7 @@ export async function POST( } catch { return NextResponse.json( { error: "Invalid JSON in request body" }, - { status: 400 } + { status: 400 }, ); } @@ -349,13 +349,13 @@ export async function POST( error: "Validation failed", details: parseResult.error.issues.map((issue) => issue.message), }, - { status: 400 } + { status: 400 }, ); } const blog = await createBlog( context, - parseResult.data as CreateBlogRequest + parseResult.data as CreateBlogRequest, ); return NextResponse.json({ @@ -369,7 +369,7 @@ export async function POST( if (!blogId) { return NextResponse.json( { error: "Blog ID is required for update" }, - { status: 400 } + { status: 400 }, ); } @@ -379,17 +379,18 @@ export async function POST( { error: "Validation failed", details: parseResult.error.issues.map( - (issue: ZodIssue) => `${issue.path.join(".")}: ${issue.message}` + (issue: ZodIssue) => + `${issue.path.join(".")}: ${issue.message}`, ), }, - { status: 400 } + { status: 400 }, ); } const blog = await updateBlog( context, blogId, - parseResult.data as UpdateBlogRequest + parseResult.data as UpdateBlogRequest, ); return NextResponse.json({ @@ -403,7 +404,7 @@ export async function POST( if (!blogId) { return NextResponse.json( { error: "Blog ID is required for deletion" }, - { status: 400 } + { status: 400 }, ); } @@ -419,7 +420,7 @@ export async function POST( if (!blogId) { return NextResponse.json( { error: "Blog ID is required" }, - { status: 400 } + { status: 400 }, ); } @@ -438,7 +439,7 @@ export async function POST( if (!blogId) { return NextResponse.json( { error: "Blog ID is required" }, - { status: 400 } + { status: 400 }, ); } @@ -456,7 +457,7 @@ export async function POST( if (!blogId) { return NextResponse.json( { error: "Blog ID is required" }, - { status: 400 } + { status: 400 }, ); } @@ -473,7 +474,7 @@ export async function POST( if (!blogId) { return NextResponse.json( { error: "Blog ID is required" }, - { status: 400 } + { status: 400 }, ); } diff --git a/app/api/blog/[slug]/route.ts b/app/api/blog/[slug]/route.ts index 42303ca..4d094d8 100644 --- a/app/api/blog/[slug]/route.ts +++ b/app/api/blog/[slug]/route.ts @@ -1,7 +1,7 @@ /** * @author ColdByDefault * @copyright 2025 ColdByDefault. All Rights Reserved. -*/ + */ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; @@ -25,7 +25,7 @@ const blogSlugSchema = z.object({ export async function GET( request: NextRequest, - { params }: { params: Promise<{ slug: string }> } + { params }: { params: Promise<{ slug: string }> }, ) { try { // Rate limiting check @@ -40,8 +40,9 @@ export async function GET( status: 429, headers: { "Retry-After": "60", + "X-Content-Type-Options": "nosniff", }, - } + }, ); } @@ -55,7 +56,12 @@ export async function GET( error: "Invalid blog slug", details: parseResult.error.issues.map((issue) => issue.message), }, - { status: 400 } + { + status: 400, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, ); } @@ -63,7 +69,15 @@ export async function GET( const blog = await getBlogBySlug(slug); if (!blog) { - return NextResponse.json({ error: "Blog not found" }, { status: 404 }); + return NextResponse.json( + { error: "Blog not found" }, + { + status: 404, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, + ); } // Generate SEO metadata for the blog @@ -87,14 +101,20 @@ export async function GET( { headers: { "Cache-Control": "public, max-age=600, stale-while-revalidate=1200", // 10 min cache, 20 min stale + "X-Content-Type-Options": "nosniff", }, - } + }, ); } catch (error) { console.error("Error fetching blog:", error); return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } + { error: "Failed to fetch blog" }, + { + status: 500, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, ); } } diff --git a/app/api/blog/route.ts b/app/api/blog/route.ts index f879d7c..739dcb5 100644 --- a/app/api/blog/route.ts +++ b/app/api/blog/route.ts @@ -1,14 +1,41 @@ /** * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { getBlogs } from "@/lib/hubs/blogs"; import type { BlogListQuery, BlogLanguage } from "@/types/hubs/blogs"; +import { RateLimiter } from "@/lib/security"; + +// Rate limiter instance: 30 requests per minute +const rateLimiter = new RateLimiter(60000, 30); + +function getClientIP(request: NextRequest): string { + const forwarded = request.headers.get("x-forwarded-for"); + const realIp = request.headers.get("x-real-ip"); + const cfConnectingIp = request.headers.get("cf-connecting-ip"); + return cfConnectingIp || realIp || forwarded?.split(",")[0] || "127.0.0.1"; +} export async function GET(request: NextRequest) { + // Rate limiting check + const clientIP = getClientIP(request); + if (!rateLimiter.isAllowed(clientIP)) { + return NextResponse.json( + { error: "Too many requests" }, + { + status: 429, + headers: { + "Retry-After": "60", + "X-RateLimit-Limit": "30", + "X-RateLimit-Remaining": "0", + }, + }, + ); + } + try { const { searchParams } = new URL(request.url); @@ -32,26 +59,23 @@ export async function GET(request: NextRequest) { return NextResponse.json(result, { headers: { - "Cache-Control": "no-cache", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET", - "Access-Control-Allow-Headers": "Content-Type", + "Cache-Control": "public, s-maxage=60, stale-while-revalidate=300", + "X-Content-Type-Options": "nosniff", + "X-Robots-Tag": "noindex, nofollow", }, }); } catch (error) { console.error("Error fetching blogs:", error); return NextResponse.json( { - error: "Internal server error", - message: error instanceof Error ? error.message : "Unknown error", - stack: - process.env.NODE_ENV === "development" - ? error instanceof Error - ? error.stack - : undefined - : undefined, + error: "Failed to fetch blogs", + }, + { + status: 500, + headers: { + "X-Content-Type-Options": "nosniff", + }, }, - { status: 500 } ); } } diff --git a/app/api/chatbot/route.ts b/app/api/chatbot/route.ts index c376727..529c571 100644 --- a/app/api/chatbot/route.ts +++ b/app/api/chatbot/route.ts @@ -1,5 +1,5 @@ /** - * ChatBot API Route with Gemini AI Integration + * ChatBot API Route with Groq AI Integration * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. */ @@ -23,7 +23,6 @@ import { } from "@/data/main/chatbot-system-prompt"; // Environment configuration with validation -const GEMINI_API_KEY = process.env.GEMINI_KEY; const GROQ_API_KEY = process.env.GROQ_API_KEY; const GROQ_MODEL = process.env.GROQ_MODEL || "openai/gpt-oss-120b"; const CHATBOT_ENABLED = process.env.CHATBOT_ENABLED === "true"; @@ -31,23 +30,23 @@ const CHATBOT_ENABLED = process.env.CHATBOT_ENABLED === "true"; const chatbotConfig: ChatBotConfig = { maxMessagesPerSession: parseInt( process.env.CHATBOT_MAX_MESSAGES_PER_SESSION || "20", - 10 + 10, ), maxMessageLength: parseInt( process.env.CHATBOT_MAX_MESSAGE_LENGTH || "1000", - 10 + 10, ), rateLimitPerMinute: parseInt( process.env.CHATBOT_RATE_LIMIT_PER_MINUTE || "10", - 10 + 10, ), rateLimitPerHour: parseInt( process.env.CHATBOT_RATE_LIMIT_PER_HOUR || "50", - 10 + 10, ), sessionTimeoutMs: parseInt( process.env.CHATBOT_SESSION_TIMEOUT_MS || "1800000", - 10 + 10, ), systemPrompt: REEM_SYSTEM_PROMPT, }; @@ -145,7 +144,7 @@ function getRateLimitInfo(clientIP: string): { const minuteRemaining = Math.max( 0, - chatbotConfig.rateLimitPerMinute - limit.minute.count + chatbotConfig.rateLimitPerMinute - limit.minute.count, ); const nextMinuteReset = limit.minute.windowStart + 60000; @@ -159,7 +158,7 @@ function cleanupSessions(): void { const now = Date.now(); for (const [sessionId, messages] of sessions.entries()) { const lastActivity = Math.max( - ...messages.map((m) => m.timestamp.getTime()) + ...messages.map((m) => m.timestamp.getTime()), ); if (now - lastActivity > chatbotConfig.sessionTimeoutMs) { sessions.delete(sessionId); @@ -177,13 +176,13 @@ function cleanupRateLimits(): void { } } -// Groq API fallback when Gemini quota is exceeded +// Groq API primary implementation async function callGroqAPI( messages: ChatMessage[], - systemPrompt: string + systemPrompt: string, ): Promise { if (!GROQ_API_KEY) { - throw new Error("No fallback API available"); + throw new Error("Groq API key not configured"); } const groqMessages = [ @@ -210,7 +209,7 @@ async function callGroqAPI( temperature: 0.7, max_tokens: 1024, }), - } + }, ); if (!response.ok) { @@ -220,7 +219,7 @@ async function callGroqAPI( throw new Error( `Groq API error: ${response.status} - ${ errorData.error?.message || "Unknown error" - }` + }`, ); } @@ -235,130 +234,9 @@ async function callGroqAPI( return data.choices[0].message.content; } -async function callGeminiAPI(messages: ChatMessage[]): Promise { - if (!GEMINI_API_KEY) { - throw new Error("Gemini API key not configured"); - } - - // Check if this is the first user message (only 1 message = the user's first message) - const isFirstMessage = messages.length === 1; - - // Convert messages to Gemini format - const contents = messages - .filter((msg) => msg.role !== "system") - .map((msg) => ({ - role: msg.role === "assistant" ? "model" : "user", - parts: [{ text: msg.content }], - })); - - // Modify system prompt for first message to include greeting instruction - let systemPrompt = chatbotConfig.systemPrompt; - if (isFirstMessage) { - systemPrompt += `\n\nIMPORTANT: This is the user's FIRST message in this conversation. You MUST start your response with a casual greeting like "What's up!" or "Hola!" or "How you doing!" followed by a brief introduction about yourself and what you can help with.`; - } - - // Add system prompt as the first message - const systemMessage = { - role: "user" as const, - parts: [{ text: systemPrompt }], - }; - - const requestBody = { - contents: [systemMessage, ...contents], - generationConfig: { - temperature: 0.7, - topK: 40, - topP: 0.95, - maxOutputTokens: 1024, - }, - safetySettings: [ - { - category: "HARM_CATEGORY_HARASSMENT", - threshold: "BLOCK_MEDIUM_AND_ABOVE", - }, - { - category: "HARM_CATEGORY_HATE_SPEECH", - threshold: "BLOCK_MEDIUM_AND_ABOVE", - }, - { - category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", - threshold: "BLOCK_MEDIUM_AND_ABOVE", - }, - { - category: "HARM_CATEGORY_DANGEROUS_CONTENT", - threshold: "BLOCK_MEDIUM_AND_ABOVE", - }, - ], - }; - - try { - const response = await fetch( - `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(requestBody), - } - ); - - if (!response.ok) { - const errorData = (await response.json().catch(() => ({}))) as { - error?: { message?: string; code?: number }; - }; - - // Handle quota exceeded (429) specifically - if (response.status === 429) { - const retryMatch = - errorData.error?.message?.match(/retry in ([\d.]+)s/i); - const retryAfter = retryMatch?.[1] ? parseFloat(retryMatch[1]) : 60; - throw new Error( - `QUOTA_EXCEEDED:${retryAfter}:Gemini API quota exceeded. Please try again later.` - ); - } - - throw new Error( - `Gemini API error: ${response.status} - ${ - errorData.error?.message || "Unknown error" - }` - ); - } - - const data = (await response.json()) as { - candidates?: Array<{ - content?: { - parts?: Array<{ text?: string }>; - }; - }>; - }; - - if (!data.candidates?.[0]?.content?.parts?.[0]?.text) { - throw new Error("Invalid response format from Gemini API"); - } - - return data.candidates[0].content.parts[0].text; - } catch (error) { - console.error("Gemini API call failed:", error); - - // Try Groq as fallback if Gemini quota exceeded and Groq key available - if ( - GROQ_API_KEY && - error instanceof Error && - (error.message.includes("QUOTA_EXCEEDED") || - error.message.includes("429")) - ) { - console.log("Falling back to Groq API..."); - return callGroqAPI(messages, systemPrompt); - } - - throw error; - } -} - // API Routes export async function POST( - request: NextRequest + request: NextRequest, ): Promise> { // Check if chatbot is enabled if (!CHATBOT_ENABLED) { @@ -367,7 +245,7 @@ export async function POST( error: "ChatBot service is currently unavailable", code: "SERVICE_UNAVAILABLE", }, - { status: 503 } + { status: 503 }, ); } @@ -382,7 +260,7 @@ export async function POST( code: "RATE_LIMIT_EXCEEDED", details: rateLimitInfo, }, - { status: 429 } + { status: 429 }, ); } @@ -395,7 +273,7 @@ export async function POST( error: "Content-Type must be application/json", code: "INVALID_INPUT", }, - { status: 400 } + { status: 400 }, ); } @@ -416,7 +294,7 @@ export async function POST( : "Invalid request body", code: "INVALID_INPUT", }, - { status: 400 } + { status: 400 }, ); } @@ -433,7 +311,7 @@ export async function POST( error: `Maximum ${chatbotConfig.maxMessagesPerSession} messages per session exceeded`, code: "INVALID_INPUT", }, - { status: 400 } + { status: 400 }, ); } @@ -449,8 +327,17 @@ export async function POST( // Add user message to session sessionMessages.push(userMessage); - // Call Gemini AI - const aiResponse = await callGeminiAPI(sessionMessages); + // Check if this is the first user message (only 1 message = the user's first message) + const isFirstMessage = sessionMessages.length === 1; + + // Modify system prompt for first message to include greeting instruction + let systemPrompt = chatbotConfig.systemPrompt; + if (isFirstMessage) { + systemPrompt += `\n\nIMPORTANT: This is the user's FIRST message in this conversation. You MUST start your response with a casual greeting like "What's up!" or "Hola!" or "How you doing!" followed by a brief introduction about yourself and what you can help with.`; + } + + // Call Groq AI + const aiResponse = await callGroqAPI(sessionMessages, systemPrompt); // Create assistant message const assistantMessage: ChatMessage = { @@ -485,8 +372,6 @@ export async function POST( rateLimitInfo, }); } catch (error) { - console.error("ChatBot API error:", error); - // Check for quota exceeded error if (error instanceof Error && error.message.startsWith("QUOTA_EXCEEDED:")) { const [, retryAfter, message] = error.message.split(":"); @@ -503,7 +388,7 @@ export async function POST( headers: { "Retry-After": String(Math.ceil(retrySeconds || 60)), }, - } + }, ); } @@ -515,7 +400,7 @@ export async function POST( : "Internal server error", code: "SERVICE_UNAVAILABLE", }, - { status: 500 } + { status: 500 }, ); } } diff --git a/app/api/email-rewrite/analyze/route.ts b/app/api/email-rewrite/analyze/route.ts index 61b7989..10610b0 100644 --- a/app/api/email-rewrite/analyze/route.ts +++ b/app/api/email-rewrite/analyze/route.ts @@ -34,7 +34,7 @@ const analyzeRequestSchema = z.object({ .string() .max( MAX_CONTEXT_LENGTH, - `Context must be under ${MAX_CONTEXT_LENGTH} characters` + `Context must be under ${MAX_CONTEXT_LENGTH} characters`, ) .optional() .transform((val) => (val ? sanitizeChatInput(val) : undefined)), @@ -50,7 +50,7 @@ function getClientIP(request: NextRequest): string { async function callGroqAPI( email: string, systemPrompt: string, - context?: string + context?: string, ): Promise { if (!GROQ_API_KEY) { throw new Error("Groq API key not configured"); @@ -85,7 +85,7 @@ async function callGroqAPI( top_p: 1, stream: false, }), - } + }, ); if (!response.ok) { @@ -95,7 +95,7 @@ async function callGroqAPI( throw new Error( `Groq API error: ${response.status} - ${ errorData.error?.message || "Unknown error" - }` + }`, ); } @@ -115,7 +115,7 @@ export async function POST(request: NextRequest) { if (!REWRITER_ENABLED) { return NextResponse.json( { error: "Email analyzer service is currently disabled" }, - { status: 503 } + { status: 503 }, ); } @@ -123,7 +123,7 @@ export async function POST(request: NextRequest) { console.error("GROQ_API_KEY not configured"); return NextResponse.json( { error: "Service configuration error" }, - { status: 500 } + { status: 500 }, ); } @@ -136,7 +136,7 @@ export async function POST(request: NextRequest) { error: "Rate limit exceeded. Please try again later.", remaining: 0, }, - { status: 429 } + { status: 429 }, ); } @@ -145,7 +145,7 @@ export async function POST(request: NextRequest) { if (!body) { return NextResponse.json( { error: "Invalid request body" }, - { status: 400 } + { status: 400 }, ); } @@ -155,7 +155,7 @@ export async function POST(request: NextRequest) { const firstError = validationResult.error.issues[0]; return NextResponse.json( { error: firstError?.message || "Invalid request data" }, - { status: 400 } + { status: 400 }, ); } @@ -176,7 +176,7 @@ export async function POST(request: NextRequest) { console.error("Failed to parse AI response:", rawResponse); return NextResponse.json( { error: "Failed to parse AI response" }, - { status: 500 } + { status: 500 }, ); } @@ -192,8 +192,10 @@ export async function POST(request: NextRequest) { status: 200, headers: { "X-RateLimit-Remaining": remaining.toString(), + "X-Content-Type-Options": "nosniff", + "Cache-Control": "no-store, no-cache, must-revalidate", }, - } + }, ); } catch (error) { console.error("Email analyze error:", error); @@ -204,10 +206,23 @@ export async function POST(request: NextRequest) { { error: "AI service temporarily unavailable. Please try again later.", }, - { status: 503 } + { + status: 503, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, ); } - return NextResponse.json({ error: sanitizedError }, { status: 500 }); + return NextResponse.json( + { error: sanitizedError }, + { + status: 500, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, + ); } -} \ No newline at end of file +} diff --git a/app/api/email-rewrite/remaining/route.ts b/app/api/email-rewrite/remaining/route.ts index 6e011a5..bf3c66d 100644 --- a/app/api/email-rewrite/remaining/route.ts +++ b/app/api/email-rewrite/remaining/route.ts @@ -13,5 +13,13 @@ export async function GET() { const remaining = getRemainingUses(ip); - return Response.json({ remaining }); + return Response.json( + { remaining }, + { + headers: { + "X-Content-Type-Options": "nosniff", + "Cache-Control": "no-store, no-cache, must-revalidate", + }, + }, + ); } diff --git a/app/api/email-rewrite/rewriter/route.ts b/app/api/email-rewrite/rewriter/route.ts index d5f0fb8..af25755 100644 --- a/app/api/email-rewrite/rewriter/route.ts +++ b/app/api/email-rewrite/rewriter/route.ts @@ -2,7 +2,7 @@ * Email Rewriter API Route with Groq AI Integration * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; @@ -53,7 +53,7 @@ function getClientIP(request: NextRequest): string { */ async function callGroqAPI( email: string, - systemPrompt: string + systemPrompt: string, ): Promise { if (!GROQ_API_KEY) { throw new Error("Groq API key not configured"); @@ -83,7 +83,7 @@ async function callGroqAPI( top_p: 1, stream: false, }), - } + }, ); if (!response.ok) { @@ -93,7 +93,7 @@ async function callGroqAPI( throw new Error( `Groq API error: ${response.status} - ${ errorData.error?.message || "Unknown error" - }` + }`, ); } @@ -114,7 +114,7 @@ export async function POST(request: NextRequest) { if (!REWRITER_ENABLED) { return NextResponse.json( { error: "Email rewriter service is currently disabled" }, - { status: 503 } + { status: 503 }, ); } @@ -123,7 +123,7 @@ export async function POST(request: NextRequest) { console.error("GROQ_API_KEY not configured"); return NextResponse.json( { error: "Service configuration error" }, - { status: 500 } + { status: 500 }, ); } @@ -145,7 +145,7 @@ export async function POST(request: NextRequest) { "Retry-After": "86400", // 24 hours in seconds "X-RateLimit-Remaining": "0", }, - } + }, ); } @@ -155,7 +155,7 @@ export async function POST(request: NextRequest) { if (!body) { return NextResponse.json( { error: "Invalid request body" }, - { status: 400 } + { status: 400 }, ); } @@ -166,7 +166,7 @@ export async function POST(request: NextRequest) { const firstError = validationResult.error.issues[0]; return NextResponse.json( { error: firstError?.message || "Invalid request data" }, - { status: 400 } + { status: 400 }, ); } @@ -178,7 +178,7 @@ export async function POST(request: NextRequest) { if (!systemPrompt) { return NextResponse.json( { error: "Invalid tone configuration" }, - { status: 500 } + { status: 500 }, ); } @@ -198,8 +198,10 @@ export async function POST(request: NextRequest) { status: 200, headers: { "X-RateLimit-Remaining": remaining.toString(), + "X-Content-Type-Options": "nosniff", + "Cache-Control": "no-store, no-cache, must-revalidate", }, - } + }, ); } catch (error) { console.error("Email rewrite error:", error); @@ -215,18 +217,36 @@ export async function POST(request: NextRequest) { error: "AI service temporarily unavailable. Please try again later.", }, - { status: 503 } + { + status: 503, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, ); } if (error.message.includes("rate limit")) { return NextResponse.json( { error: "Service rate limit reached. Please try again later." }, - { status: 429 } + { + status: 429, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, ); } } - return NextResponse.json({ error: sanitizedError }, { status: 500 }); + return NextResponse.json( + { error: sanitizedError }, + { + status: 500, + headers: { + "X-Content-Type-Options": "nosniff", + }, + }, + ); } } diff --git a/app/api/pagespeed/refresh/route.ts b/app/api/pagespeed/refresh/route.ts deleted file mode 100644 index 5da05d0..0000000 --- a/app/api/pagespeed/refresh/route.ts +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -import { NextRequest, NextResponse } from "next/server"; -import type { - PageSpeedResult, - PageSpeedMetrics, -} from "@/types/configs/pagespeed"; - -interface RefreshResult { - strategy: "mobile" | "desktop"; - success: boolean; - data?: PageSpeedResult; - error?: string; -} - -interface RefreshErrorResult { - error: string; - success: false; -} - -const CRON_SECRET = process.env.CRON_SECRET; -const MAIN_URL = process.env.PORTFOLIO_URL || "https://www.coldbydefault.com"; - -export async function POST(request: NextRequest) { - try { - // Verify cron secret - const authHeader = request.headers.get("authorization"); - if (authHeader !== `Bearer ${CRON_SECRET}`) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - console.log("Starting automated PageSpeed refresh..."); - - // Use hardcoded base URL to prevent SSRF - const baseUrl = process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "https://www.coldbydefault.com"; - const strategies: ("mobile" | "desktop")[] = ["mobile", "desktop"]; - - const refreshPromises: Promise[] = strategies.map( - async (strategy) => { - try { - const response = await fetch( - `${baseUrl}/api/pagespeed?url=${encodeURIComponent( - MAIN_URL - )}&strategy=${strategy}&refresh=true`, - { - method: "GET", - headers: { - "User-Agent": "Vercel-Cron/1.0", - }, - signal: AbortSignal.timeout(60000), // 60 seconds timeout - } - ); - - if (response.ok) { - const data = (await response.json()) as PageSpeedResult; - - // Validate that we received the expected structure - if (!data?.metrics || typeof data.metrics !== "object") { - throw new Error("Invalid response structure from PageSpeed API"); - } - - const metrics: PageSpeedMetrics = data.metrics; - console.log("✅ Refreshed %s data for %s:", strategy, MAIN_URL, { - performance: metrics.performance, - accessibility: metrics.accessibility, - bestPractices: metrics.bestPractices, - seo: metrics.seo, - }); - return { strategy, success: true, data } satisfies RefreshResult; - } else { - console.error( - "❌ Failed to refresh %s data:", - strategy, - response.status, - response.statusText - ); - return { - strategy, - success: false, - error: `HTTP ${response.status}`, - } satisfies RefreshResult; - } - } catch (error) { - console.error("❌ Error refreshing %s data:", strategy, error); - return { - strategy, - success: false, - error: error instanceof Error ? error.message : "Unknown error", - } satisfies RefreshResult; - } - } - ); - - const results = await Promise.allSettled(refreshPromises); - - const successCount = results.filter( - (result) => result.status === "fulfilled" && result.value.success - ).length; - - console.log( - `Automated refresh completed: ${successCount}/${strategies.length} successful` - ); - - return NextResponse.json({ - success: true, - message: `Refreshed ${successCount}/${strategies.length} PageSpeed datasets`, - timestamp: new Date().toISOString(), - results: results.map((result): RefreshResult | RefreshErrorResult => - result.status === "fulfilled" - ? result.value - : { error: "Promise rejected", success: false } - ), - }); - } catch (error) { - console.error("Cron job error:", error); - return NextResponse.json( - { - success: false, - error: "Cron job failed", - timestamp: new Date().toISOString(), - }, - { status: 500 } - ); - } -} - -// GET method for manual testing -export async function GET(request: NextRequest) { - const { searchParams } = new URL(request.url); - const secret = searchParams.get("secret"); - - if (secret !== CRON_SECRET) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } - - // Create a new request with authorization header for POST method - const newRequest = new NextRequest(request.url, { - method: "POST", - headers: { - ...request.headers, - authorization: `Bearer ${CRON_SECRET}`, - }, - }); - - return POST(newRequest); -} diff --git a/app/api/pagespeed/route.ts b/app/api/pagespeed/route.ts deleted file mode 100644 index fd0161a..0000000 --- a/app/api/pagespeed/route.ts +++ /dev/null @@ -1,469 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; -import { z } from "zod"; -import type { - PageSpeedMetrics, - PageSpeedResult, - PageSpeedApiRawResponse, -} from "@/types/configs/pagespeed"; - -// Zod schema for SSRF protection -const pageSpeedRequestSchema = z.object({ - url: z - .string() - .url("Invalid URL format") - .refine((url) => { - try { - const parsed = new URL(url); - - // Only allow HTTP/HTTPS protocols - if (!["http:", "https:"].includes(parsed.protocol)) { - return false; - } - - // Block localhost and private IP ranges to prevent SSRF - const hostname = parsed.hostname.toLowerCase(); - - // Block localhost variants - if (["localhost", "127.0.0.1", "::1"].includes(hostname)) { - return false; - } - - // Block private IP ranges (simplified check) - if ( - hostname.match(/^10\.|^172\.(1[6-9]|2[0-9]|3[0-1])\.|^192\.168\./) - ) { - return false; - } - - // Block link-local addresses - if (hostname.match(/^169\.254\.|^fe80:/)) { - return false; - } - - // Block internal domains - if (hostname.includes(".local") || hostname.includes(".internal")) { - return false; - } - - return true; - } catch { - return false; - } - }, "URL not allowed for security reasons"), - strategy: z.enum(["mobile", "desktop"]).default("mobile"), - refresh: z.boolean().default(false), -}); - -// In-memory cache with automatic expiration -interface CacheEntry { - data: PageSpeedResult; - timestamp: number; - isRefreshing?: boolean; - timeoutCount?: number; // Track consecutive timeout failures -} - -class PageSpeedCache { - private static instance: PageSpeedCache; - private cache = new Map(); - private readonly CACHE_DURATION = 12 * 60 * 60 * 1000; // 12 hours - private readonly STALE_WHILE_REVALIDATE = 24 * 60 * 60 * 1000; // 24 hours - private readonly MAX_TIMEOUT_COUNT = 3; // Max consecutive timeouts before circuit breaker - - static getInstance(): PageSpeedCache { - if (!PageSpeedCache.instance) { - PageSpeedCache.instance = new PageSpeedCache(); - } - return PageSpeedCache.instance; - } - - getCacheKey(url: string, strategy: string): string { - return `${url}_${strategy}`; - } - - get(url: string, strategy: string): CacheEntry | null { - const key = this.getCacheKey(url, strategy); - const entry = this.cache.get(key); - - if (!entry) return null; - - const now = Date.now(); - const age = now - entry.timestamp; - - // If data is stale beyond revalidate time, remove it - if (age > this.STALE_WHILE_REVALIDATE) { - this.cache.delete(key); - return null; - } - - return entry; - } - - set(url: string, strategy: string, data: PageSpeedResult): void { - const key = this.getCacheKey(url, strategy); - this.cache.set(key, { - data, - timestamp: Date.now(), - isRefreshing: false, - timeoutCount: 0, // Reset timeout count on successful fetch - }); - } - - incrementTimeoutCount(url: string, strategy: string): void { - const key = this.getCacheKey(url, strategy); - const entry = this.cache.get(key); - if (entry) { - entry.timeoutCount = (entry.timeoutCount || 0) + 1; - } - } - - shouldSkipFetch(url: string, strategy: string): boolean { - const entry = this.get(url, strategy); - return !!entry && (entry.timeoutCount || 0) >= this.MAX_TIMEOUT_COUNT; - } - - isStale(url: string, strategy: string): boolean { - const entry = this.get(url, strategy); - if (!entry) return true; - - const age = Date.now() - entry.timestamp; - return age > this.CACHE_DURATION; - } - - setRefreshing(url: string, strategy: string, refreshing: boolean): void { - const key = this.getCacheKey(url, strategy); - const entry = this.cache.get(key); - if (entry) { - entry.isRefreshing = refreshing; - } - } - - isRefreshing(url: string, strategy: string): boolean { - const entry = this.get(url, strategy); - return entry?.isRefreshing || false; - } - - // Clean up old entries periodically - cleanup(): void { - const now = Date.now(); - for (const [key, entry] of this.cache.entries()) { - if (now - entry.timestamp > this.STALE_WHILE_REVALIDATE) { - this.cache.delete(key); - } - } - } -} - -const cache = PageSpeedCache.getInstance(); - -// Background refresh function -async function backgroundRefresh( - url: string, - strategy: "mobile" | "desktop" -): Promise { - if (cache.isRefreshing(url, strategy)) { - return; // Already refreshing - } - - cache.setRefreshing(url, strategy, true); - - try { - const result = await fetchPageSpeedData(url, strategy); - if (result) { - cache.set(url, strategy, result); - } - } catch (error) { - console.error( - "Background refresh failed for %s (%s):", - url, - strategy, - error - ); - } finally { - cache.setRefreshing(url, strategy, false); - } -} - -// Extracted PageSpeed API fetch function -async function fetchPageSpeedData( - url: string, - strategy: "mobile" | "desktop" -): Promise { - const apiKey = process.env.PAGESPEED_INSIGHTS_API_KEY; - if (!apiKey) { - throw new Error("PageSpeed Insights API key not configured"); - } - - const pageSpeedUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent( - url - )}&key=${apiKey}&strategy=${strategy}&category=performance&category=accessibility&category=best-practices&category=seo`; - - // Use conservative timeout to prevent Vercel function timeout - // Desktop analysis typically takes longer than mobile - const timeoutMs = process.env.NODE_ENV === "production" ? 40000 : 45000; // 40s for prod, 45s for dev - - const response = await fetch(pageSpeedUrl, { - method: "GET", - headers: { - Accept: "application/json", - "User-Agent": "Mozilla/5.0 (compatible; Portfolio/1.0)", - }, - signal: AbortSignal.timeout(timeoutMs), - }); - - if (!response.ok) { - if (response.status === 429) { - throw new Error("Rate limit exceeded"); - } - if (response.status === 403) { - throw new Error( - "PageSpeed API access denied - API key may be invalid, restricted, or the API not enabled in Google Cloud Console" - ); - } - if (response.status >= 500) { - throw new Error("PageSpeed service unavailable"); - } - throw new Error(`PageSpeed API error: ${response.status}`); - } - - const data = (await response.json()) as PageSpeedApiRawResponse; - - if (!data?.lighthouseResult?.categories) { - throw new Error("Invalid response from PageSpeed API"); - } - - const categories = data.lighthouseResult.categories; - const metrics: PageSpeedMetrics = { - performance: Math.round((categories.performance?.score ?? 0) * 100), - accessibility: Math.round((categories.accessibility?.score ?? 0) * 100), - bestPractices: Math.round((categories["best-practices"]?.score ?? 0) * 100), - seo: Math.round((categories.seo?.score ?? 0) * 100), - }; - - if (categories.pwa?.score !== undefined && categories.pwa?.score !== null) { - metrics.pwa = Math.round(categories.pwa.score * 100); - } - - return { - url: data.id ?? url, - strategy, - metrics, - }; -} - -export async function GET(request: NextRequest) { - try { - // Skip PageSpeed API calls in development - if (process.env.NODE_ENV !== "production") { - return NextResponse.json( - { - url: "https://www.coldbydefault.com", - strategy: "mobile", - metrics: { - performance: 0, - accessibility: 0, - bestPractices: 0, - seo: 0, - }, - disabled: true, - message: "PageSpeed API is disabled in development mode", - }, - { status: 200 } - ); - } - - const { searchParams } = new URL(request.url); - - // Parse and validate request parameters with Zod - const parseResult = pageSpeedRequestSchema.safeParse({ - url: searchParams.get("url") || "https://www.coldbydefault.com", - strategy: searchParams.get("strategy") || "mobile", - refresh: searchParams.get("refresh") === "true", - }); - - if (!parseResult.success) { - return NextResponse.json( - { - error: "Invalid request parameters", - details: parseResult.error.issues.map((issue) => issue.message), - }, - { status: 400 } - ); - } - - const { url, strategy, refresh: forceRefresh } = parseResult.data; - - // Check cache first - const cachedEntry = cache.get(url, strategy); - const isStale = cache.isStale(url, strategy); - - const desktopStaleTolerance = - strategy === "desktop" && - cachedEntry && - Date.now() - cachedEntry.timestamp < 24 * 60 * 60 * 1000; // 24 hours for desktop - - // If we have fresh data, return it immediately - if (cachedEntry && !isStale && !forceRefresh) { - return NextResponse.json(cachedEntry.data, { - headers: { - "Cache-Control": "public, max-age=43200", // 12 hours browser cache - "X-Cache": "HIT", - }, - }); - } - - // If we have stale data and not force refreshing, return stale data and trigger background refresh - // For desktop, should act more aggressive about returning stale data - if (cachedEntry && (!forceRefresh || desktopStaleTolerance)) { - // Trigger background refresh (fire and forget) only if not too recent - if (isStale) { - backgroundRefresh(url, strategy).catch(console.error); - } - - return NextResponse.json(cachedEntry.data, { - headers: { - "Cache-Control": "public, max-age=300", // 5 minutes browser cache for stale data - "X-Cache": desktopStaleTolerance ? "STALE-DESKTOP" : "STALE", - }, - }); - } - - // Check if we should skip fetching due to circuit breaker - if (cache.shouldSkipFetch(url, strategy) && cachedEntry) { - return NextResponse.json(cachedEntry.data, { - headers: { - "Cache-Control": "public, max-age=1800", // 30 minutes for circuit breaker - "X-Cache": "CIRCUIT-BREAKER", - }, - }); - } - - // If no cache or force refresh, fetch fresh data - try { - const result = await fetchPageSpeedData(url, strategy); - - if (result) { - cache.set(url, strategy, result); - - return NextResponse.json(result, { - headers: { - "Cache-Control": "public, max-age=43200", // 12 hours - "X-Cache": "MISS", - }, - }); - } - } catch (error) { - console.error("Fresh fetch failed:", error); - - // Track timeout for circuit breaker - if (error instanceof Error) { - const isTimeout = - error.name === "TimeoutError" || - error.name === "AbortError" || - error.message.includes("timeout") || - error.message.includes("timed out"); - - if (isTimeout) { - cache.incrementTimeoutCount(url, strategy); - } - } - - // If fresh fetch fails but we have stale data, return the stale data - if (cachedEntry) { - return NextResponse.json(cachedEntry.data, { - headers: { - "Cache-Control": "public, max-age=300", - "X-Cache": "STALE-ERROR", - }, - }); - } - - // No cache and fetch failed - if (error instanceof Error) { - const isTimeout = - error.name === "TimeoutError" || - error.name === "AbortError" || - error.message.includes("timeout") || - error.message.includes("timed out"); - - if (isTimeout) { - // Start background refresh for next time - backgroundRefresh(url, strategy).catch(console.error); - - return NextResponse.json( - { - error: - "PageSpeed analysis timed out. The website may be slow to load. Try refreshing in a few minutes.", - retryAfter: 300, - }, - { - status: 504, - headers: { - "Retry-After": "300", - }, - } - ); - } - - if (error.message.includes("Rate limit")) { - return NextResponse.json( - { - error: "Too many requests. Please wait before trying again.", - retryAfter: 60, - }, - { - status: 429, - headers: { - "Retry-After": "60", - }, - } - ); - } - - if (error.message.includes("not configured")) { - return NextResponse.json( - { error: "PageSpeed API key is not configured" }, - { status: 500 } - ); - } - - if (error.message.includes("service unavailable")) { - return NextResponse.json( - { error: "Google PageSpeed service is temporarily unavailable" }, - { status: 503 } - ); - } - } - - return NextResponse.json( - { - error: "PageSpeed service is temporarily unavailable", - details: - error instanceof Error ? error.message : "Unknown error occurred", - }, - { status: 503 } - ); - } - - return NextResponse.json( - { error: "Unable to analyze page speed" }, - { status: 500 } - ); - } catch (error) { - console.error("PageSpeed API error:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); - } -} - -// Cleanup old cache entries periodically -setInterval(() => { - cache.cleanup(); -}, 60 * 60 * 1000); // Every hour diff --git a/app/page.tsx b/app/page.tsx index a63d8e8..4e4b655 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,7 +1,7 @@ /** * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ "use client"; import { Hero } from "@/components/hero"; @@ -22,7 +22,7 @@ const Capabilities = dynamic( { loading: () => , ssr: false, - } + }, ); const CertificationShowcase = dynamic( @@ -33,18 +33,7 @@ const CertificationShowcase = dynamic( { loading: () => , ssr: false, - } -); - -const PageSpeedInsights = dynamic( - () => - import("@/components/pagespeed").then((mod) => ({ - default: mod.PageSpeedInsights, - })), - { - loading: () => , - ssr: false, - } + }, ); const ClientBackground = dynamic( @@ -55,7 +44,7 @@ const ClientBackground = dynamic( { loading: () => null, ssr: false, - } + }, ); export default function Home() { @@ -155,27 +144,6 @@ export default function Home() { - - - - } - > -
-
-

- Website Performance -

- -
-
-
- }> diff --git a/components/cer/CertificationShowcaseDesktop.tsx b/components/cer/CertificationShowcaseDesktop.tsx index fd18e6c..dad16ca 100644 --- a/components/cer/CertificationShowcaseDesktop.tsx +++ b/components/cer/CertificationShowcaseDesktop.tsx @@ -1,7 +1,7 @@ /** * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ import React from "react"; import Image from "next/image"; @@ -14,6 +14,7 @@ interface Certification { readonly id: number; readonly title: string; readonly issuer: string; + readonly issuerKey?: string; readonly date: string; readonly description: string; readonly descriptionKey: string; @@ -32,6 +33,7 @@ export function CertificationShowcaseDesktop({ }: CertificationShowcaseDesktopProps) { const t = useTranslations("Certifications"); const tDescriptions = useTranslations("Certifications.descriptions"); + const tIssuers = useTranslations("Certifications.issuers"); const renderDesktopCard = (cert: Certification) => { return ( @@ -59,7 +61,8 @@ export function CertificationShowcaseDesktop({

- {t("issuedBy")} {cert.issuer} + {t("issuedBy")}{" "} + {cert.issuerKey ? tIssuers(cert.issuerKey) : cert.issuer}

{t("date")} {cert.date} diff --git a/components/cer/CertificationShowcaseMobile.tsx b/components/cer/CertificationShowcaseMobile.tsx index 719f10d..c5148be 100644 --- a/components/cer/CertificationShowcaseMobile.tsx +++ b/components/cer/CertificationShowcaseMobile.tsx @@ -1,7 +1,7 @@ /** * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ import React from "react"; import Image from "next/image"; @@ -15,6 +15,7 @@ interface Certification { readonly id: number; readonly title: string; readonly issuer: string; + readonly issuerKey?: string; readonly date: string; readonly description: string; readonly descriptionKey: string; @@ -34,6 +35,7 @@ export function CertificationShowcaseMobile({ }: CertificationShowcaseMobileProps) { const t = useTranslations("Certifications"); const tDescriptions = useTranslations("Certifications.descriptions"); + const tIssuers = useTranslations("Certifications.issuers"); const renderTabletCard = (cert: Certification) => { const isExpanded = logic.expandedCards.has(cert.id); @@ -68,7 +70,8 @@ export function CertificationShowcaseMobile({

{cert.title}

- {cert.issuer} • {cert.date} + {cert.issuerKey ? tIssuers(cert.issuerKey) : cert.issuer} •{" "} + {cert.date}

@@ -105,7 +108,7 @@ export function CertificationShowcaseMobile({
{t("issuedBy")} - {cert.issuer} + {cert.issuerKey ? tIssuers(cert.issuerKey) : cert.issuer}
@@ -160,7 +163,8 @@ export function CertificationShowcaseMobile({

{cert.title}

- {cert.issuer} • {cert.date} + {cert.issuerKey ? tIssuers(cert.issuerKey) : cert.issuer} •{" "} + {cert.date}

@@ -197,7 +201,7 @@ export function CertificationShowcaseMobile({
{t("issuedBy")} - {cert.issuer} + {cert.issuerKey ? tIssuers(cert.issuerKey) : cert.issuer}
diff --git a/components/chatbot/ChatBot.constants.ts b/components/chatbot/ChatBot.constants.ts index 9781121..c5f5628 100644 --- a/components/chatbot/ChatBot.constants.ts +++ b/components/chatbot/ChatBot.constants.ts @@ -2,7 +2,7 @@ * ChatBot Constants and Configuration * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ export const CHATBOT_CONFIG = { // UI Constants @@ -143,10 +143,31 @@ export const CHATBOT_TRANSLATION_KEYS = { INPUT_PLACEHOLDER: "input.placeholder", INPUT_CHARACTER_LIMIT: "input.characterLimit", + // Errors + ERROR_GENERIC: "errors.generic", + ERROR_NETWORK: "errors.network", + ERROR_RATE_LIMIT: "errors.rateLimit", + // Accessibility ACCESSIBILITY_SEND_MESSAGE: "accessibility.sendMessage", }; +// Fallback messages (used when translations are not available) +export const CHATBOT_FALLBACK_MESSAGES = { + GENERIC_ERROR: + "I'm having trouble responding right now. Please try again in a moment.", + NETWORK_ERROR: + "I can't reach the server at the moment. Please check your internet connection and try again.", + RATE_LIMIT_ERROR: + "I'm receiving too many requests. Please wait a moment before sending another message.", + QUOTA_EXCEEDED: + "I'm taking a short break. Please try again in a few moments.", + SERVER_ERROR: "Something went wrong on my end. Please try again later.", + VALIDATION_ERROR: + "I couldn't process your message. Please try rephrasing it.", + TIMEOUT_ERROR: "The request is taking too long. Please try again.", +}; + // Type exports for better type safety export type ChatBotPosition = keyof typeof CHATBOT_CONFIG.POSITION_CLASSES; export type ChatBotTranslationKey = diff --git a/components/chatbot/ChatBot.tsx b/components/chatbot/ChatBot.tsx index e9bba23..3a96273 100644 --- a/components/chatbot/ChatBot.tsx +++ b/components/chatbot/ChatBot.tsx @@ -2,7 +2,7 @@ * Professional Floating ChatBot Component * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ "use client"; @@ -34,7 +34,7 @@ export function ChatBot({ const [isOpen, setIsOpen] = useState(false); const [isVisible, setIsVisible] = useState(false); const [bottomOffset, setBottomOffset] = useState( - CHATBOT_CONFIG.DEFAULT_BOTTOM_OFFSET + CHATBOT_CONFIG.DEFAULT_BOTTOM_OFFSET, ); const messagesEndRef = useRef(null); const chatInputRef = useRef(null); @@ -118,9 +118,8 @@ export function ChatBot({ const handleSendMessage = async (message: string) => { try { await sendMessage(message); - } catch (err) { - // Error is handled by the hook - console.error("Failed to send message:", err); + } catch { + // Error is handled by the hook and displayed to user } }; diff --git a/components/chatbot/ChatInput.tsx b/components/chatbot/ChatInput.tsx index e6039ad..de4eb68 100644 --- a/components/chatbot/ChatInput.tsx +++ b/components/chatbot/ChatInput.tsx @@ -2,7 +2,7 @@ * ChatInput Component - Message input form with validation * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ "use client"; @@ -27,7 +27,7 @@ export interface ChatInputProps { export const ChatInput = React.memo( React.forwardRef(function ChatInput( { onSendMessage, isLoading, disabled = false, className = "" }, - ref + ref, ) { const [inputValue, setInputValue] = useState(""); const t = useTranslations("ChatBot"); @@ -48,9 +48,8 @@ export const ChatInput = React.memo( try { await onSendMessage(sanitizedMessage); - } catch (err) { + } catch { // Error is handled by the parent component - console.error("Failed to send message:", err); } }; @@ -69,7 +68,7 @@ export const ChatInput = React.memo( >
{ - handleSubmit(e).catch(console.error); + void handleSubmit(e); }} className="flex gap-2 sm:gap-3" aria-label="Send message to assistant" @@ -120,5 +119,5 @@ export const ChatInput = React.memo(
); - }) + }), ); diff --git a/components/contact/ContactSheet.tsx b/components/contact/ContactSheet.tsx index 56443bc..2071d27 100644 --- a/components/contact/ContactSheet.tsx +++ b/components/contact/ContactSheet.tsx @@ -17,7 +17,7 @@ import { import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; -import { MessageSquare, Mails } from "lucide-react"; +import { MessageSquare } from "lucide-react"; import { FaGithub, FaLinkedin, @@ -93,14 +93,6 @@ export default function ContactSheet({ /> - {/* Email Section */} -
- -

- service@yazan-abo-ayash.de -

-
- {/* Social Media Section */}

@@ -181,6 +173,12 @@ export default function ContactSheet({ Freelancer

+
+ Position: + + Botgenossen GmbH + +
{t("training")}: diff --git a/components/footer/Footer.tsx b/components/footer/Footer.tsx index 0fa6224..0edf8eb 100644 --- a/components/footer/Footer.tsx +++ b/components/footer/Footer.tsx @@ -1,17 +1,15 @@ /** * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ import { Links } from "@/components/footer"; import { legalLinks, developerLinks, - socialLinks, footerNavLinks, } from "@/data/main/footerLinks"; import { SiVercel } from "react-icons/si"; -import { Mails } from "lucide-react"; import Link from "next/link"; import Image from "next/image"; @@ -64,15 +62,8 @@ export default function Footer() {
-
- -
- - service@yazan-abo-ayash.de -
-
{/* Powered By Section */} -
+
Powered by diff --git a/components/pagespeed/PageSpeedInsights.tsx b/components/pagespeed/PageSpeedInsights.tsx deleted file mode 100644 index e45a1be..0000000 --- a/components/pagespeed/PageSpeedInsights.tsx +++ /dev/null @@ -1,348 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -"use client"; - -import { useState, useEffect } from "react"; -import { - Card, - CardContent, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Progress } from "@/components/ui/progress"; -import { Separator } from "@/components/ui/separator"; -import { SiGooglecloud } from "react-icons/si"; -import { HiDesktopComputer } from "react-icons/hi"; -import { HiDevicePhoneMobile } from "react-icons/hi2"; -import type { - PageSpeedResult, - PageSpeedInsightsProps, -} from "@/types/configs/pagespeed"; -import { usePageSpeedData } from "@/hooks/use-pageSpeed-data"; - -const getScoreBadgeColor = (score: number): string => { - if (score >= 90) - return "bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400 dark:border-emerald-800"; - if (score >= 50) - return "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-900/20 dark:text-amber-400 dark:border-amber-800"; - return "bg-red-100 text-red-800 border-red-200 dark:bg-red-900/20 dark:text-red-400 dark:border-red-800"; -}; - -const getCacheStatusInfo = ( - status: "fresh" | "updating" | "updated" | null -) => { - switch (status) { - case "fresh": - return { - label: "Fresh", - className: - "bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-900/20 dark:text-emerald-400", - }; - case "updating": - return { - label: "Updating", - className: - "bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-900/20 dark:text-amber-400", - }; - case "updated": - return { - label: "Updated", - className: - "bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-900/20 dark:text-blue-400", - }; - default: - return null; - } -}; - -const MetricsSkeleton = () => ( -
- {Array.from({ length: 4 }).map((_, i) => ( -
- - -
- ))} -
-); - -const LoadingSkeleton = ({ - progress, - progressLabel, -}: { - progress: number; - progressLabel: string; -}) => ( - - -
-
- - -
- -
- -
- -
- - -
- -
-
- {progressLabel} - {progress}% -
- -
- -
- - - -
-); - -const MetricsDisplay = ({ data }: { data: PageSpeedResult }) => ( -
-

- Performance Metrics -

-
- {Object.entries(data.metrics).map(([key, score]) => ( -
- - {key === "bestPractices" ? "Best Practices" : key} - - - {score as number} - -
- ))} -
-
-); - -export default function PageSpeedInsights({ - url = "https://www.coldbydefault.com", - showRefreshButton = true, - showBothStrategies = true, -}: PageSpeedInsightsProps) { - const [activeStrategy, setActiveStrategy] = useState<"mobile" | "desktop">( - "desktop" - ); - const [progress, setProgress] = useState(0); - const [progressLabel, setProgressLabel] = useState("Initializing..."); - - const { - mobileData, - desktopData, - loading, - cacheStatus, - lastUpdated, - refresh, - } = usePageSpeedData({ url, showBothStrategies }); - - // Progress simulation based on loading state - useEffect(() => { - if (loading) { - // Start progress - const updateProgress = (value: number, label: string) => { - setProgress(value); - setProgressLabel(label); - }; - - updateProgress(0, "Connecting to PageSpeed API..."); - - const progressTimer = setTimeout(() => { - updateProgress(25, "Analyzing website performance..."); - - const midTimer = setTimeout(() => { - updateProgress(60, "Processing metrics..."); - }, 800); - - return () => clearTimeout(midTimer); - }, 300); - - return () => clearTimeout(progressTimer); - } else { - // Complete progress when data is loaded - const finalizeProgress = () => { - setProgress(100); - setProgressLabel("Analysis complete!"); - }; - finalizeProgress(); - return undefined; - } - }, [loading]); - - const getCurrentData = () => { - return activeStrategy === "mobile" ? mobileData : desktopData; - }; - - const data = getCurrentData(); - - if (loading) { - return ( - - ); - } - - // Error case removed - we always show data (mock or real) - // The hook handles errors silently and shows mock data instead - - if (!data) { - // This should never happen since we always have mock data - return ( - - ); - } - - const cacheInfo = getCacheStatusInfo(cacheStatus); - - return ( - - -
- - - PageSpeed Insights - - Powered by Google - - - {showBothStrategies && ( -
- - -
- )} - {!showBothStrategies && ( -
- {data.strategy === "desktop" ? ( - - ) : ( - - )} - - {data.strategy} - -
- )} -
-

{data.url}

-
- - - - - {showRefreshButton && ( - <> - -
- {cacheInfo && ( -
- Cache Status: - - {cacheInfo.label} - -
- )} - -
- - )} -
- - -
-
-
- {/* Green status dot with inline styles as fallback */} -
-
-
- {lastUpdated ? ( - <> - API Online • Last updated:{" "} - {new Date(lastUpdated).toLocaleDateString()} at{" "} - {new Date(lastUpdated).toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - })} - - ) : ( - "Loading..." - )} -
-
- {cacheStatus === "updating" && ( -

- 📡 Auto-refreshing in background -

- )} -
-
-
-
- ); -} diff --git a/components/pagespeed/index.ts b/components/pagespeed/index.ts deleted file mode 100644 index 74d5054..0000000 --- a/components/pagespeed/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -export { default as PageSpeedInsights } from "./PageSpeedInsights"; -export { usePageSpeedData } from "../../hooks/use-pageSpeed-data"; diff --git a/data/hubs/portfolio-section.data.ts b/data/hubs/portfolio-section.data.ts index fac5b3e..6e37492 100644 --- a/data/hubs/portfolio-section.data.ts +++ b/data/hubs/portfolio-section.data.ts @@ -1,7 +1,7 @@ /** * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ import { Code, @@ -80,7 +80,7 @@ export const dataNodes: ArchitectureNode[] = [ { icon: Target, title: "API Routes Structure", - subtitle: "RESTful Endpoints + GitHub API + PageSpeed API", + subtitle: "RESTful Endpoints + GitHub API", color: "bg-orange-500/10 text-orange-600", }, { @@ -332,46 +332,6 @@ export function CertificationShowcaseMobile({ ); -}`, - }, - { - title: "API Data Hook", - language: "TypeScript", - code: `// Data fetching with caching and error handling -export function usePageSpeedData({ - url, - showBothStrategies = true, -}: UsePageSpeedDataProps): UsePageSpeedDataReturn { - const [mobileData, setMobileData] = useState(null); - const [desktopData, setDesktopData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [cacheStatus, setCacheStatus] = useState< - "fresh" | "updating" | "updated" | null - >(null); - - const fetchStrategy = useCallback(async ( - strategy: "mobile" | "desktop", - forceRefresh = false - ): Promise => { - try { - const queryParams = new URLSearchParams({ url, strategy }); - if (forceRefresh) queryParams.append("refresh", "true"); - - const response = await fetch(\`/api/pagespeed?\${queryParams}\`); - const result = await response.json(); - - if (strategy === "mobile") { - setMobileData(result); - } else { - setDesktopData(result); - } - } catch (err) { - setError(err instanceof Error ? err.message : "An error occurred"); - } - }, [url]); - - return { mobileData, desktopData, loading, error, cacheStatus, refresh }; }`, }, ]; @@ -415,7 +375,6 @@ export const routeStructure = { "api/blog/*", "api/chatbot/*", "api/github/*", - "api/pagespeed/*", ], }; @@ -436,7 +395,7 @@ export const componentStructure = { { folder: "hooks/", description: "Global reusable hooks", - examples: ["use-mobile.ts", "use-language.ts", "use-pageSpeed-data.ts"], + examples: ["use-mobile.ts", "use-language.ts"], }, { folder: "lib/", diff --git a/data/main/certificationsData.ts b/data/main/certificationsData.ts index 87482ad..3ec02a3 100644 --- a/data/main/certificationsData.ts +++ b/data/main/certificationsData.ts @@ -1,9 +1,20 @@ /** * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ export const certifications = [ + { + id: 4, + title: "Computer Science Expert", + image: "/default.png", + issuer: "IHK Rhein-Neckar (German Chamber of Commerce and Industry)", + issuerKey: "ihkRheinNeckar", + description: + "Subject Area: Software Development and AI, as per § 37 German Vocational Training Act (BBiG).", + descriptionKey: "computerScienceExpert", + date: "2026", + }, { id: 1, title: "Python PCEP", diff --git a/hooks/use-chatbot.ts b/hooks/use-chatbot.ts index 978a5d5..888ede5 100644 --- a/hooks/use-chatbot.ts +++ b/hooks/use-chatbot.ts @@ -2,7 +2,7 @@ * ChatBot Custom Hook - Manages state and API communication * @author ColdByDefault * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ + */ "use client"; @@ -12,6 +12,7 @@ import type { ChatBotResponse, ChatBotApiError, } from "@/types/configs/chatbot"; +import { CHATBOT_FALLBACK_MESSAGES } from "@/components/chatbot/ChatBot.constants"; interface UseChatBotReturn { messages: ChatMessage[]; @@ -48,7 +49,7 @@ export function useChatBot(): UseChatBotReturn { (msg) => ({ ...msg, timestamp: new Date(msg.timestamp), - }) + }), ); setMessages(messagesWithDates); } @@ -56,9 +57,8 @@ export function useChatBot(): UseChatBotReturn { if (savedSession) { sessionIdRef.current = savedSession; } - } catch (error) { - console.error("Failed to load chat history from localStorage:", error); - // Clear corrupted data + } catch { + // Clear corrupted data silently localStorage.removeItem(STORAGE_KEY_MESSAGES); localStorage.removeItem(STORAGE_KEY_SESSION); } @@ -70,8 +70,8 @@ export function useChatBot(): UseChatBotReturn { if (typeof window !== "undefined" && messages.length > 0) { try { localStorage.setItem(STORAGE_KEY_MESSAGES, JSON.stringify(messages)); - } catch (error) { - console.error("Failed to save chat history to localStorage:", error); + } catch { + // Storage quota exceeded or disabled - fail silently } } }, [messages]); @@ -81,8 +81,8 @@ export function useChatBot(): UseChatBotReturn { if (typeof window !== "undefined" && sessionIdRef.current) { try { localStorage.setItem(STORAGE_KEY_SESSION, sessionIdRef.current); - } catch (error) { - console.error("Failed to save session ID to localStorage:", error); + } catch { + // Storage quota exceeded or disabled - fail silently } } }, []); @@ -174,8 +174,8 @@ export function useChatBot(): UseChatBotReturn { // Update user message status setMessages((prev) => prev.map((msg) => - msg.id === userMessage.id ? { ...msg, status: "sent" } : msg - ) + msg.id === userMessage.id ? { ...msg, status: "sent" } : msg, + ), ); if (!response.ok || !("success" in data) || !data.success) { @@ -185,7 +185,7 @@ export function useChatBot(): UseChatBotReturn { if (errorData.code === "QUOTA_EXCEEDED" && errorData.retryAfter) { const retrySeconds = Math.ceil(errorData.retryAfter); throw new Error( - `Reem is taking a short break. Please try again in ${retrySeconds} seconds.` + `Reem is taking a short break. Please try again in ${retrySeconds} seconds.`, ); } @@ -203,8 +203,8 @@ export function useChatBot(): UseChatBotReturn { if (typeof window !== "undefined") { try { localStorage.setItem(STORAGE_KEY_SESSION, data.data.sessionId); - } catch (error) { - console.error("Failed to save session ID to localStorage:", error); + } catch { + // Storage quota exceeded or disabled - fail silently } } @@ -222,32 +222,53 @@ export function useChatBot(): UseChatBotReturn { setIsConnected(true); } catch (err) { - console.error("ChatBot error:", err); - // Update user message status to error setMessages((prev) => prev.map((msg) => - msg.id === userMessage.id ? { ...msg, status: "error" } : msg - ) + msg.id === userMessage.id ? { ...msg, status: "error" } : msg, + ), ); - // Set error message - const errorMessage = - err instanceof Error ? err.message : "Failed to send message"; - setError(errorMessage); - - // Check if it's a connection issue - if ( - errorMessage.includes("fetch") || - errorMessage.includes("network") - ) { - setIsConnected(false); + // Determine appropriate fallback message + let errorMessage = CHATBOT_FALLBACK_MESSAGES.GENERIC_ERROR; + + if (err instanceof Error) { + const errMsg = err.message.toLowerCase(); + + if ( + errMsg.includes("network") || + errMsg.includes("fetch") || + errMsg.includes("connection") + ) { + errorMessage = CHATBOT_FALLBACK_MESSAGES.NETWORK_ERROR; + setIsConnected(false); + } else if ( + errMsg.includes("quota") || + errMsg.includes("rate limit") || + errMsg.includes("too many") + ) { + errorMessage = CHATBOT_FALLBACK_MESSAGES.RATE_LIMIT_ERROR; + } else if (errMsg.includes("taking a short break")) { + errorMessage = CHATBOT_FALLBACK_MESSAGES.QUOTA_EXCEEDED; + } else if (errMsg.includes("timeout")) { + errorMessage = CHATBOT_FALLBACK_MESSAGES.TIMEOUT_ERROR; + } else if ( + errMsg.includes("validation") || + errMsg.includes("invalid") + ) { + errorMessage = CHATBOT_FALLBACK_MESSAGES.VALIDATION_ERROR; + } else if (err.message && !errMsg.includes("failed to send")) { + // Use the original error message if it's user-friendly + errorMessage = err.message; + } } + + setError(errorMessage); } finally { setIsLoading(false); } }, - [isLoading] + [isLoading], ); return { diff --git a/hooks/use-pageSpeed-data.ts b/hooks/use-pageSpeed-data.ts deleted file mode 100644 index a09cd28..0000000 --- a/hooks/use-pageSpeed-data.ts +++ /dev/null @@ -1,247 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. - */ - -"use client"; - -import { useState, useEffect, useCallback, useRef } from "react"; -import type { - PageSpeedResult, - PageSpeedApiResponse, -} from "@/types/configs/pagespeed"; - -interface UsePageSpeedDataProps { - url: string; - showBothStrategies?: boolean; -} - -interface UsePageSpeedDataReturn { - mobileData: PageSpeedResult | null; - desktopData: PageSpeedResult | null; - loading: boolean; - error: string | null; - cacheStatus: "fresh" | "updating" | "updated" | null; - lastUpdated: string | null; - refresh: () => void; -} - -// Fallback mock data - shown immediately while real data loads -const createMockData = ( - strategy: "mobile" | "desktop", - url: string, -): PageSpeedResult => ({ - url, - strategy, - metrics: { - performance: strategy === "desktop" ? 91 : 94, - accessibility: 93, - bestPractices: 98, - seo: 100, - }, -}); - -export function usePageSpeedData({ - url, - showBothStrategies = true, -}: UsePageSpeedDataProps): UsePageSpeedDataReturn { - // Initialize with mock data immediately - users see data right away - const [mobileData, setMobileData] = useState(() => - createMockData("mobile", url), - ); - const [desktopData, setDesktopData] = useState(() => - createMockData("desktop", url), - ); - // Start with loading=false since we have mock data - const [loading] = useState(false); - // Never expose errors to users - always null - const [error] = useState(null); - const [cacheStatus, setCacheStatus] = useState< - "fresh" | "updating" | "updated" | null - >("fresh"); - const [lastUpdated, setLastUpdated] = useState(() => - new Date().toISOString(), - ); - - // Track if we've fetched real data - const hasRealData = useRef({ mobile: false, desktop: false }); - const isDevModeDisabled = useRef(false); // Track if API is disabled in dev - const retryTimeoutRef = useRef | null>(null); - const fetchFnRef = useRef<((forceRefresh?: boolean) => Promise) | null>( - null, - ); - - const fetchStrategy = useCallback( - async ( - strategy: "mobile" | "desktop", - forceRefresh = false, - ): Promise => { - try { - const queryParams = new URLSearchParams({ - url, - strategy, - }); - - if (forceRefresh) { - queryParams.append("refresh", "true"); - } - - const response = await fetch( - `/api/pagespeed?${queryParams.toString()}`, - { - headers: { "X-Client-ID": "pagespeed-component" }, - signal: AbortSignal.timeout(50000), - }, - ); - - // Extract and simplify cache status - const xCache = response.headers.get("X-Cache"); - if (xCache) { - if (xCache === "HIT") setCacheStatus("fresh"); - else if (xCache.includes("STALE")) setCacheStatus("updating"); - else setCacheStatus("updated"); - } - - if (!response.ok) { - // Silently fail - keep showing mock/cached data - console.warn( - `PageSpeed API returned ${response.status} for ${strategy}`, - ); - return false; - } - - const result = (await response.json()) as PageSpeedApiResponse; - - // Check if API is disabled (dev mode) or data is invalid - if (!result?.metrics || (result as any).disabled) { - // Keep showing mock data in dev mode - console.warn( - `PageSpeed API ${(result as any).disabled ? "disabled in dev mode" : "returned invalid data"} for ${strategy}`, - ); - // Mark as disabled to prevent infinite retries - if ((result as any).disabled) { - isDevModeDisabled.current = true; - // Mark as having "data" (mock) so we don't retry - hasRealData.current.mobile = true; - hasRealData.current.desktop = true; - } - return false; - } - - // Check if metrics are all zeros (invalid real data) - const hasValidMetrics = Object.values(result.metrics).some( - (value) => value > 0, - ); - if (!hasValidMetrics) { - console.warn(`PageSpeed returned zero metrics for ${strategy}`); - return false; - } - - const validatedResult: PageSpeedResult = { - url: result.url || url, - strategy: (result.strategy as "mobile" | "desktop") || strategy, - metrics: result.metrics, - ...(result.loadingExperience && { - loadingExperience: result.loadingExperience, - }), - }; - - // Update with real data - if (strategy === "mobile") { - setMobileData(validatedResult); - hasRealData.current.mobile = true; - } else { - setDesktopData(validatedResult); - hasRealData.current.desktop = true; - } - - setLastUpdated(new Date().toISOString()); - setCacheStatus("fresh"); - return true; - } catch (err) { - // Silently fail - keep showing mock/cached data - console.warn(`PageSpeed fetch failed (${strategy}):`, err); - return false; - } - }, - [url], - ); - - // Fetch data with silent background retry on failure - const fetchAllDataWithRetry = useCallback( - async (forceRefresh = false): Promise => { - if (!url) return; - - // Skip fetching if API is disabled in dev mode (unless force refresh) - if (isDevModeDisabled.current && !forceRefresh) { - return; - } - - setCacheStatus("updating"); - - try { - const mobileSuccess = await fetchStrategy("mobile", forceRefresh); - let needsRetry = !mobileSuccess; - - if (showBothStrategies) { - const desktopSuccess = await fetchStrategy("desktop", forceRefresh); - needsRetry = !mobileSuccess && !desktopSuccess; - } - - // Schedule silent retry if needed (only if we don't have real data yet and API is not disabled) - if ( - needsRetry && - !hasRealData.current.mobile && - !hasRealData.current.desktop && - !isDevModeDisabled.current - ) { - if (retryTimeoutRef.current) { - clearTimeout(retryTimeoutRef.current); - } - retryTimeoutRef.current = setTimeout(() => { - console.log("Silent retry: Attempting to fetch PageSpeed data..."); - // Use ref to call the latest version of the function - void fetchFnRef.current?.(false); - }, 30000); - } - } catch (fetchError) { - console.warn("Failed to fetch PageSpeed data:", fetchError); - } - }, - [url, showBothStrategies, fetchStrategy], - ); - - // Keep the ref updated with the latest function - useEffect(() => { - fetchFnRef.current = fetchAllDataWithRetry; - }, [fetchAllDataWithRetry]); - - const refresh = useCallback(() => { - void fetchAllDataWithRetry(true); - }, [fetchAllDataWithRetry]); - - useEffect(() => { - // Defer fetch to next tick to avoid synchronous setState in effect - const timeoutId = setTimeout(() => { - void fetchAllDataWithRetry(false); - }, 0); - - // Cleanup retry timeout on unmount - return () => { - clearTimeout(timeoutId); - if (retryTimeoutRef.current) { - clearTimeout(retryTimeoutRef.current); - } - }; - }, [fetchAllDataWithRetry]); - - return { - mobileData, - desktopData, - loading, - error, - cacheStatus, - lastUpdated, - refresh, - }; -} diff --git a/messages/de.json b/messages/de.json index 965278a..94d2143 100644 --- a/messages/de.json +++ b/messages/de.json @@ -119,6 +119,9 @@ "title": "Meine Zertifikate", "issuedBy": "Ausgestellt von:", "date": "Datum:", + "issuers": { + "ihkRheinNeckar": "IHK Rhein-Neckar (Industrie- und Handelskammer)" + }, "descriptions": { "pythonPcep": "Die PCEP (Python Certified Entry-Level Programmer) Zertifizierung erhalten, die grundlegende Kenntnisse der Python-Programmierung nachweist.", "udemyPythonBootcamp": "Den Kurs \"100 Days of Code - The Complete Python Pro Bootcamp\" abgeschlossen und Python vom Anfänger- bis zum Fortgeschrittenenniveau gemeistert.", @@ -126,7 +129,8 @@ "udemyHtmlCss": "Den Kurs abgeschlossen und die Grundlagen von HTML und CSS gelernt, um eine Website zu erstellen und zu veröffentlichen.", "gitGithubBootcamp": "Einen umfassenden Git- und GitHub-Kurs abgeschlossen, der Versionskontrolle, Branching und Kollaborations-Workflows abdeckt.", "fullStackWebDev": "Frontend- und Backend-Webentwicklungskurs, der HTML, CSS, JavaScript, Node.js und React abdeckt.", - "kiKompetenzSchulung": "KI-Kompetenzschulung gemäß Artikel 4 des EU AI Acts, die verantwortungsvollen KI-Einsatz und Compliance behandelt." + "kiKompetenzSchulung": "KI-Kompetenzschulung gemäß Artikel 4 des EU AI Acts, die verantwortungsvollen KI-Einsatz und Compliance behandelt.", + "computerScienceExpert": "Fachinformatiker, Fachrichtung: Anwendungsentwicklung, Einsatzgebiet: KI, gemäß § 37 Berufsbildungsgesetz (BBiG)." } }, "Projects": { @@ -222,8 +226,8 @@ "chatbot": { "title": "KI-Chatbot-Service", "description": "Diese Website enthält einen KI-Chatbot für interaktive Unterstützung und Informationen.", - "geminiTitle": "Groq API Integration", - "geminiDescription": "Der Chatbot wird von Groq betrieben. Durch die Nutzung des Chatbots akzeptieren Sie die Nutzungsbedingungen und Datenschutzrichtlinien von Groq. Chat-Anfragen werden über die Groq API verarbeitet.", + "apiTitle": "Groq API Integration", + "apiDescription": "Der Chatbot wird von Groq betrieben. Durch die Nutzung des Chatbots akzeptieren Sie die Nutzungsbedingungen und Datenschutzrichtlinien von Groq. Chat-Anfragen werden über die Groq API verarbeitet.", "temporaryTitle": "Keine Datenspeicherung", "temporaryDescription": "Ich speichere keine Chat-Gespräche, Verläufe oder persönlichen Informationen. Alle Chat-Daten werden ausschließlich in Ihrem Browser (Web-Storage) gespeichert und verbleiben vollständig unter Ihrer Kontrolle." }, diff --git a/messages/en.json b/messages/en.json index fc2459e..796e33a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -119,6 +119,9 @@ "title": "My Certifications", "issuedBy": "Issued by:", "date": "Date:", + "issuers": { + "ihkRheinNeckar": "IHK Rhein-Neckar (German Chamber of Commerce and Industry)" + }, "descriptions": { "pythonPcep": "Earned the PCEP (Python Certified Entry-Level Programmer) certification, demonstrating foundational knowledge of Python programming.", "udemyPythonBootcamp": "Completed the \"100 Days of Code - The Complete Python Pro Bootcamp,\" mastering Python from beginner to advanced levels.", @@ -126,7 +129,8 @@ "udemyHtmlCss": "Completed the course, learning the fundamentals of HTML and CSS to build and deploy a website.", "gitGithubBootcamp": "Completed a comprehensive Git and GitHub course, covering version control, branching, and collaboration workflows.", "fullStackWebDev": "Frontend and Backend Web Development course, covering HTML, CSS, JavaScript, Node.js, and React.", - "kiKompetenzSchulung": "AI Competence Training according to Article 4 of the EU AI Act, covering responsible AI usage and compliance." + "kiKompetenzSchulung": "AI Competence Training according to Article 4 of the EU AI Act, covering responsible AI usage and compliance.", + "computerScienceExpert": "Computer Science Expert, Subject Area: Software Development and AI, as per § 37 German Vocational Training Act (BBiG)." } }, "Projects": { @@ -222,8 +226,8 @@ "chatbot": { "title": "AI Chatbot Service", "description": "This website includes an AI chatbot for interactive assistance and information.", - "geminiTitle": "Groq API Integration", - "geminiDescription": "The chatbot is powered by Groq. By using the chatbot, you accept Groq's terms of service and privacy policy. Chat requests are processed via the Groq API.", + "apiTitle": "Groq API Integration", + "apiDescription": "The chatbot is powered by Groq. By using the chatbot, you accept Groq's terms of service and privacy policy. Chat requests are processed via the Groq API.", "temporaryTitle": "No Data Storage", "temporaryDescription": "I do not save any chat conversations, history, or personal information. All chat data is stored exclusively in your browser (web storage) and remains entirely under your control." }, diff --git a/messages/es.json b/messages/es.json index 441c19b..2be00ba 100644 --- a/messages/es.json +++ b/messages/es.json @@ -119,6 +119,9 @@ "title": "Mis certificaciones", "issuedBy": "Emitido por:", "date": "Fecha:", + "issuers": { + "ihkRheinNeckar": "IHK Rhein-Neckar (Cámara de Comercio e Industria Alemana)" + }, "descriptions": { "pythonPcep": "Obtuve la certificación PCEP (Python Certified Entry-Level Programmer), demostrando conocimientos fundamentales de programación en Python.", "udemyPythonBootcamp": "Completé el curso \"100 Days of Code - The Complete Python Pro Bootcamp\", dominando Python desde nivel principiante hasta avanzado.", @@ -126,7 +129,8 @@ "udemyHtmlCss": "Completé el curso, aprendiendo los fundamentos de HTML y CSS para construir y publicar un sitio web.", "gitGithubBootcamp": "Completé un curso integral de Git y GitHub, cubriendo control de versiones, ramificación y flujos de trabajo colaborativos.", "fullStackWebDev": "Curso de desarrollo web Frontend y Backend, cubriendo HTML, CSS, JavaScript, Node.js y React.", - "kiKompetenzSchulung": "Formación en Competencias de IA según el Artículo 4 de la Ley de IA de la UE, cubriendo uso responsable de IA y cumplimiento." + "kiKompetenzSchulung": "Formación en Competencias de IA según el Artículo 4 de la Ley de IA de la UE, cubriendo uso responsable de IA y cumplimiento.", + "computerScienceExpert": "Experto en Informática, Área Temática: Desarrollo de Software e IA, según el § 37 de la Ley Alemana de Formación Profesional (BBiG)." } }, "Projects": { @@ -222,8 +226,8 @@ "chatbot": { "title": "Servicio de Chatbot IA", "description": "Este sitio web incluye un chatbot de IA para asistencia interactiva e información.", - "geminiTitle": "Integración de Groq API", - "geminiDescription": "El chatbot funciona con Groq. Al usar el chatbot, aceptas los términos de servicio y la política de privacidad de Groq. Las solicitudes del chat se procesan a través de la API de Groq.", + "apiTitle": "Integración de Groq API", + "apiDescription": "El chatbot funciona con Groq. Al usar el chatbot, aceptas los términos de servicio y la política de privacidad de Groq. Las solicitudes del chat se procesan a través de la API de Groq.", "temporaryTitle": "Sin Almacenamiento de Datos", "temporaryDescription": "No guardo ninguna conversación, historial o información personal. Todos los datos del chat se almacenan exclusivamente en tu navegador (almacenamiento web) y permanecen completamente bajo tu control." }, diff --git a/messages/fr.json b/messages/fr.json index c62c0a0..986d03b 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -119,6 +119,9 @@ "title": "Mes Certifications", "issuedBy": "Délivré par :", "date": "Date :", + "issuers": { + "ihkRheinNeckar": "IHK Rhein-Neckar (Chambre de Commerce et d'Industrie Allemande)" + }, "descriptions": { "pythonPcep": "Obtention de la certification PCEP (Python Certified Entry-Level Programmer), démontrant des connaissances fondamentales en programmation Python.", "udemyPythonBootcamp": "Complété « 100 Days of Code - The Complete Python Pro Bootcamp », maîtrisant Python du niveau débutant à avancé.", @@ -126,7 +129,8 @@ "udemyHtmlCss": "Complété le cours, apprenant les bases du HTML et du CSS pour construire et déployer un site web.", "gitGithubBootcamp": "Complété un cours complet sur Git et GitHub, couvrant le contrôle de version, le branching et les flux de collaboration.", "fullStackWebDev": "Cours de développement web Frontend et Backend, couvrant HTML, CSS, JavaScript, Node.js et React.", - "kiKompetenzSchulung": "Formation aux compétences en IA selon l'Article 4 de la loi européenne sur l'IA, couvrant l'utilisation responsable de l'IA et la conformité." + "kiKompetenzSchulung": "Formation aux compétences en IA selon l'Article 4 de la loi européenne sur l'IA, couvrant l'utilisation responsable de l'IA et la conformité.", + "computerScienceExpert": "Expert en Informatique, Domaine : Développement Logiciel et IA, selon le § 37 de la loi allemande sur la formation professionnelle (BBiG)." } }, "Projects": { @@ -222,8 +226,8 @@ "chatbot": { "title": "Service de Chatbot IA", "description": "Ce site web inclut un chatbot IA pour l'assistance interactive et l'information.", - "geminiTitle": "Intégration de l'API Groq", - "geminiDescription": "Le chatbot est alimenté par Groq. En utilisant le chatbot, vous acceptez les conditions d'utilisation et la politique de confidentialité de Groq. Les requêtes de chat sont traitées via l'API Groq.", + "apiTitle": "Intégration de l'API Groq", + "apiDescription": "Le chatbot est alimenté par Groq. En utilisant le chatbot, vous acceptez les conditions d'utilisation et la politique de confidentialité de Groq. Les requêtes de chat sont traitées via l'API Groq.", "temporaryTitle": "Aucun Stockage de Données", "temporaryDescription": "Je ne sauvegarde aucune conversation, historique ou information personnelle. Toutes les données de chat sont stockées exclusivement dans votre navigateur (stockage web) et restent entièrement sous votre contrôle." }, diff --git a/messages/sv.json b/messages/sv.json index 53ac5e1..1aeb572 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -119,6 +119,9 @@ "title": "Mina certifieringar", "issuedBy": "Utfärdat av:", "date": "Datum:", + "issuers": { + "ihkRheinNeckar": "IHK Rhein-Neckar (Tysk Handels- och Industrikammare)" + }, "descriptions": { "pythonPcep": "Erhållit certifieringen PCEP (Python Certified Entry-Level Programmer), vilket visar grundläggande kunskaper i Python-programmering.", "udemyPythonBootcamp": "Genomfört \"100 Days of Code - The Complete Python Pro Bootcamp\" och behärskat Python från nybörjar- till avancerad nivå.", @@ -126,7 +129,8 @@ "udemyHtmlCss": "Genomfört kursen och lärt mig grunderna i HTML och CSS för att bygga och lansera en webbplats.", "gitGithubBootcamp": "Genomfört en omfattande Git- och GitHub-kurs som täckte versionshantering, branching och samarbetsarbetsflöden.", "fullStackWebDev": "Kurs i frontend- och backendutveckling som täcker HTML, CSS, JavaScript, Node.js och React.", - "kiKompetenzSchulung": "AI-kompetensutbildning enligt artikel 4 i EU:s AI-förordning, som täcker ansvarsfull AI-användning och efterlevnad." + "kiKompetenzSchulung": "AI-kompetensutbildning enligt artikel 4 i EU:s AI-förordning, som täcker ansvarsfull AI-användning och efterlevnad.", + "computerScienceExpert": "Datavetenskapsexpert, Ämnesområde: Mjukvaruutveckling och AI, enligt § 37 av den tyska yrkesutbildningslagen (BBiG)." } }, "Projects": { @@ -222,8 +226,8 @@ "chatbot": { "title": "AI Chatbot-tjänst", "description": "Denna webbplats inkluderar en AI-chatbot för interaktiv assistans och information.", - "geminiTitle": "Groq API Integration", - "geminiDescription": "Chatboten drivs av Groq. Genom att använda chatboten accepterar du Groqs användarvillkor och integritetspolicy. Chattförfrågningar bearbetas via Groq API.", + "apiTitle": "Groq API Integration", + "apiDescription": "Chatboten drivs av Groq. Genom att använda chatboten accepterar du Groqs användarvillkor och integritetspolicy. Chattförfrågningar bearbetas via Groq API.", "temporaryTitle": "Ingen Datalagring", "temporaryDescription": "Jag sparar inga chattkonversationer, historik eller personlig information. All chattdata lagras exklusivt i din webbläsare (webblagring) och förblir helt under din kontroll." }, diff --git a/next.config.ts b/next.config.ts index fd27412..eaaa9cc 100644 --- a/next.config.ts +++ b/next.config.ts @@ -99,11 +99,13 @@ const nextConfig: NextConfig = { key: "Content-Security-Policy", value: [ "default-src 'self'", + // TODO: Remove 'unsafe-inline' and 'unsafe-eval' by implementing CSP nonces for Next.js + // Current limitation: Required for Next.js runtime and Vercel Analytics "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://vercel.live https://va.vercel-scripts.com", "style-src 'self' 'unsafe-inline'", // Allow inline styles for Tailwind and components "img-src 'self' data: blob: https://avatars.githubusercontent.com https://github.com", "font-src 'self' data:", - "connect-src 'self' https://api.github.com https://www.googleapis.com https://generativelanguage.googleapis.com https://vercel.live https://vitals.vercel-analytics.com", + "connect-src 'self' https://api.github.com https://www.googleapis.com https://generativelanguage.googleapis.com https://vercel.live https://vitals.vercel-analytics.com https://api.groq.com", "frame-src 'none'", "object-src 'none'", "base-uri 'self'", diff --git a/package-lock.json b/package-lock.json index 6b2d231..f4c9f8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "coldbydefault-portfolio", - "version": "5.3.18", + "version": "6.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "coldbydefault-portfolio", - "version": "5.3.18", + "version": "6.0.1", "hasInstallScript": true, "license": "Copyright 2026 Yazan Abo-Ayash. All Rights Reserved.", "dependencies": { @@ -39,7 +39,7 @@ "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.12", "lucide-react": "^0.542.0", - "next": "^16.0.10", + "next": "^16.1.6", "next-intl": "^4.6.0", "next-themes": "^0.4.6", "react": "^19.2.3", @@ -65,7 +65,7 @@ "@types/react-dom": "^19.2.3", "@types/three": "^0.179.0", "eslint": "^9.39.2", - "eslint-config-next": "^16.0.10", + "eslint-config-next": "^16.1.6", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", @@ -122,7 +122,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -402,8 +401,7 @@ "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", "devOptional": true, - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", @@ -1831,9 +1829,9 @@ "license": "MIT" }, "node_modules/@next/env": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.1.tgz", - "integrity": "sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1847,9 +1845,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.1.tgz", - "integrity": "sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", "cpu": [ "arm64" ], @@ -1863,9 +1861,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.1.tgz", - "integrity": "sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", "cpu": [ "x64" ], @@ -1879,9 +1877,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.1.tgz", - "integrity": "sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", "cpu": [ "arm64" ], @@ -1895,9 +1893,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.1.tgz", - "integrity": "sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", "cpu": [ "arm64" ], @@ -1911,9 +1909,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.1.tgz", - "integrity": "sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", "cpu": [ "x64" ], @@ -1927,9 +1925,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.1.tgz", - "integrity": "sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", "cpu": [ "x64" ], @@ -1943,9 +1941,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.1.tgz", - "integrity": "sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", "cpu": [ "arm64" ], @@ -1959,9 +1957,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.1.tgz", - "integrity": "sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", "cpu": [ "x64" ], @@ -2380,9 +2378,9 @@ "license": "Apache-2.0" }, "node_modules/@prisma/config": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.3.0.tgz", - "integrity": "sha512-QyMV67+eXF7uMtKxTEeQqNu/Be7iH+3iDZOQZW5ttfbSwBamCSdwPszA0dum+Wx27I7anYTPLmRmMORKViSW1A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.0.tgz", + "integrity": "sha512-EnNrZMwZ9+O6UlG+YO9SP3VhVw4zwMahDRzQm3r0DQn9KeU5NwzmaDAY+BzACrgmaU71Id1/0FtWIDdl7xQp9g==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -2441,70 +2439,70 @@ "license": "Apache-2.0" }, "node_modules/@prisma/engines": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.3.0.tgz", - "integrity": "sha512-cWRQoPDXPtR6stOWuWFZf9pHdQ/o8/QNWn0m0zByxf5Kd946Q875XdEJ52pEsX88vOiXUmjuPG3euw82mwQNMg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.0.tgz", + "integrity": "sha512-H+dgpbbY3VN/j5hOSVP1LXsv/rU0w/4C2zh5PZUwo/Q3NqZjOvBlVvkhtziioRmeEZ3SBAqPCsf1sQ74sI3O/w==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.3.0", - "@prisma/engines-version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", - "@prisma/fetch-engine": "7.3.0", - "@prisma/get-platform": "7.3.0" + "@prisma/debug": "7.4.0", + "@prisma/engines-version": "7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57", + "@prisma/fetch-engine": "7.4.0", + "@prisma/get-platform": "7.4.0" } }, "node_modules/@prisma/engines-version": { - "version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735.tgz", - "integrity": "sha512-IH2va2ouUHihyiTTRW889LjKAl1CusZOvFfZxCDNpjSENt7g2ndFsK0vdIw/72v7+jCN6YgkHmdAP/BI7SDgyg==", + "version": "7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57.tgz", + "integrity": "sha512-5o3/bubIYdUeg38cyNf+VDq+LVtxvvi2393Fd1Uru52LPfkGJnmVbCaX1wBOAncgKR3BCloMJFD+Koog9LtYqQ==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/debug": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.3.0.tgz", - "integrity": "sha512-yh/tHhraCzYkffsI1/3a7SHX8tpgbJu1NPnuxS4rEpJdWAUDHUH25F1EDo6PPzirpyLNkgPPZdhojQK804BGtg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.0.tgz", + "integrity": "sha512-fZicwzgFHvvPMrRLCUinrsBTdadJsi/1oirzShjmFvNLwtu2DYlkxwRVy5zEGhp85mrEGnLeS/PdNRCdE027+Q==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.3.0.tgz", - "integrity": "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.0.tgz", + "integrity": "sha512-fOUIoGzAPgtjHVs4DsVSnEDPBEauAmFeZr4Ej3tMwxywam7hHdRtCzgKagQBKcYIJuya8gzYrTqUoukzXtWJaA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.3.0" + "@prisma/debug": "7.4.0" } }, "node_modules/@prisma/fetch-engine": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.3.0.tgz", - "integrity": "sha512-Mm0F84JMqM9Vxk70pzfNpGJ1lE4hYjOeLMu7nOOD1i83nvp8MSAcFYBnHqLvEZiA6onUR+m8iYogtOY4oPO5lQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.0.tgz", + "integrity": "sha512-IXPOYskT89UTVsntuSnMTiKRWCuTg5JMWflgEDV1OSKFpuhwP5vqbfF01/iwo9y6rCjR0sDIO+jdV5kq38/hgA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.3.0", - "@prisma/engines-version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", - "@prisma/get-platform": "7.3.0" + "@prisma/debug": "7.4.0", + "@prisma/engines-version": "7.4.0-20.ab56fe763f921d033a6c195e7ddeb3e255bdbb57", + "@prisma/get-platform": "7.4.0" } }, "node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.3.0.tgz", - "integrity": "sha512-yh/tHhraCzYkffsI1/3a7SHX8tpgbJu1NPnuxS4rEpJdWAUDHUH25F1EDo6PPzirpyLNkgPPZdhojQK804BGtg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.0.tgz", + "integrity": "sha512-fZicwzgFHvvPMrRLCUinrsBTdadJsi/1oirzShjmFvNLwtu2DYlkxwRVy5zEGhp85mrEGnLeS/PdNRCdE027+Q==", "devOptional": true, "license": "Apache-2.0" }, "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.3.0.tgz", - "integrity": "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.0.tgz", + "integrity": "sha512-fOUIoGzAPgtjHVs4DsVSnEDPBEauAmFeZr4Ej3tMwxywam7hHdRtCzgKagQBKcYIJuya8gzYrTqUoukzXtWJaA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.3.0" + "@prisma/debug": "7.4.0" } }, "node_modules/@prisma/get-platform": { @@ -3711,7 +3709,6 @@ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.4.2.tgz", "integrity": "sha512-H4B4+FDNHpvIb4FmphH4ubxOfX5bxmfOw0+3pkQwR9u9wFiyMS7wUDkNn0m4RqQuiLWeia9jfN1eBvtyAVGEog==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.17.8", "@types/react-reconciler": "^0.32.0", @@ -4387,7 +4384,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4398,7 +4394,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4423,7 +4418,6 @@ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.179.0.tgz", "integrity": "sha512-VgbFG2Pgsm84BqdegZzr7w2aKbQxmgzIu4Dy7/75ygiD/0P68LKmp5ie08KMPNqGTQwIge8s6D1guZf1RnZE0A==", "license": "MIT", - "peer": true, "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", @@ -4491,7 +4485,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -5093,7 +5086,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5494,7 +5486,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5841,9 +5832,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "devOptional": true, "license": "MIT" }, @@ -6190,8 +6181,7 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/embla-carousel-autoplay": { "version": "8.6.0", @@ -6514,7 +6504,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6570,13 +6559,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.1.tgz", - "integrity": "sha512-55nTpVWm3qeuxoQKLOjQVciKZJUphKrNM0fCcQHAIOGl6VFXgaqeMfv0aKJhs7QtcnlAPhNVqsqRfRjeKBPIUA==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", + "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.1.1", + "@next/eslint-plugin-next": "16.1.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -6597,9 +6586,9 @@ } }, "node_modules/eslint-config-next/node_modules/@next/eslint-plugin-next": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.1.tgz", - "integrity": "sha512-Ovb/6TuLKbE1UiPcg0p39Ke3puyTCIKN9hGbNItmpQsp+WX3qrjO3WaMVSi6JHr9X1NrmthqIguVHodMJbh/dw==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz", + "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6743,7 +6732,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7772,7 +7760,6 @@ "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", "devOptional": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -9001,9 +8988,9 @@ } }, "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", "dev": true, "license": "MIT", "dependencies": { @@ -10084,13 +10071,12 @@ } }, "node_modules/next": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.1.tgz", - "integrity": "sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==", + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", "license": "MIT", - "peer": true, "dependencies": { - "@next/env": "16.1.1", + "@next/env": "16.1.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001579", @@ -10104,14 +10090,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.1", - "@next/swc-darwin-x64": "16.1.1", - "@next/swc-linux-arm64-gnu": "16.1.1", - "@next/swc-linux-arm64-musl": "16.1.1", - "@next/swc-linux-x64-gnu": "16.1.1", - "@next/swc-linux-x64-musl": "16.1.1", - "@next/swc-win32-arm64-msvc": "16.1.1", - "@next/swc-win32-x64-msvc": "16.1.1", + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", "sharp": "^0.34.4" }, "peerDependencies": { @@ -10271,9 +10257,9 @@ "license": "MIT" }, "node_modules/nypm": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz", - "integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", + "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -10289,9 +10275,9 @@ } }, "node_modules/nypm/node_modules/citty": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz", - "integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", + "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", "devOptional": true, "license": "MIT" }, @@ -10588,7 +10574,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -10817,17 +10802,16 @@ } }, "node_modules/prisma": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.3.0.tgz", - "integrity": "sha512-ApYSOLHfMN8WftJA+vL6XwAPOh/aZ0BgUyyKPwUFgjARmG6EBI9LzDPf6SWULQMSAxydV9qn5gLj037nPNlg2w==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.0.tgz", + "integrity": "sha512-n2xU9vSaH4uxZF/l2aKoGYtKtC7BL936jM9Q94Syk1zOD39t/5hjDUxMgaPkVRDX5wWEMsIqvzQxoebNIesOKw==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { - "@prisma/config": "7.3.0", + "@prisma/config": "7.4.0", "@prisma/dev": "0.20.0", - "@prisma/engines": "7.3.0", + "@prisma/engines": "7.4.0", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" @@ -10976,7 +10960,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10986,7 +10969,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -11005,7 +10987,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.69.0.tgz", "integrity": "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12180,7 +12161,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -12496,7 +12476,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -13079,7 +13058,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 4ab9597..d7997e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coldbydefault-portfolio", - "version": "5.3.18", + "version": "6.0.1", "description": "Professional portfolio of Yazan Abo-Ayash (ColdByDefault™) - Full Stack Developer specializing in AI and automation, Next.js and modern web technologies.", "keywords": [ "portfolio", @@ -25,7 +25,7 @@ "vercel", "modern-web-technologies" ], - "author": "Yazan Abo-Ayash (ColdByDefault™) ", + "author": "Yazan Abo-Ayash (ColdByDefault™)", "license": "Copyright 2026 Yazan Abo-Ayash. All Rights Reserved.", "private": true, "scripts": { @@ -76,7 +76,7 @@ "embla-carousel-react": "^8.6.0", "framer-motion": "^12.23.12", "lucide-react": "^0.542.0", - "next": "^16.0.10", + "next": "^16.1.6", "next-intl": "^4.6.0", "next-themes": "^0.4.6", "react": "^19.2.3", @@ -102,7 +102,7 @@ "@types/react-dom": "^19.2.3", "@types/three": "^0.179.0", "eslint": "^9.39.2", - "eslint-config-next": "^16.0.10", + "eslint-config-next": "^16.1.6", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-react": "^7.37.5", diff --git a/temp-not-in-use/aboutPorto/PortoCardComponents.tsx.backup b/temp-not-in-use/aboutPorto/PortoCardComponents.tsx.backup deleted file mode 100644 index e0a656a..0000000 --- a/temp-not-in-use/aboutPorto/PortoCardComponents.tsx.backup +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -import React from "react"; -import { useTranslations } from "next-intl"; -import { Badge } from "@/components/ui/badge"; -import type { - FeatureItemProps, - DeviceType, - PortoCardFeature, -} from "@/types/aboutPorto"; - -export const FeatureItem = React.memo(function FeatureItem({ - icon, - title, - description: _description, - badges: _badges, - compact = false, -}: FeatureItemProps) { - if (compact) { - return ( -
- -
-

- {title} -

-
-
- ); - } - - return ( -
- -
-

- {title} -

-
-
- ); -}); - -interface FeatureGridProps { - features: PortoCardFeature[]; - deviceType: DeviceType; - gridClasses: string; -} - -export const FeatureGrid = React.memo(function FeatureGrid({ - features, - deviceType, - gridClasses, -}: FeatureGridProps) { - const t = useTranslations("PortfolioAbout.features"); - const isCompact = deviceType === "mobile"; - - return ( -
- {features.map((feature, index) => ( - - ))} -
- ); -}); - -interface TechHighlightsProps { - techs: string[]; - deviceType: DeviceType; -} - -export const TechHighlights = React.memo(function TechHighlights({ - techs, - deviceType, -}: TechHighlightsProps) { - const t = useTranslations("PortfolioAbout"); - - if (deviceType === "mobile") { - return null; // Don't show on mobile to save space - } - - return ( -
-
-

- {t("techHighlights")} -

-
- {techs.map((tech, index) => ( - - {tech} - - ))} -
-
-
- ); -}); diff --git a/temp-not-in-use/aboutPorto/index.ts.backup b/temp-not-in-use/aboutPorto/index.ts.backup deleted file mode 100644 index 348f398..0000000 --- a/temp-not-in-use/aboutPorto/index.ts.backup +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -export { default as PortoCard } from "./portoCard"; -export { default } from "./portoCard"; -export * from "./portoCard.utils"; -export * from "./PortoCardComponents"; diff --git a/temp-not-in-use/aboutPorto/portoCard.tsx.backup b/temp-not-in-use/aboutPorto/portoCard.tsx.backup deleted file mode 100644 index b86194c..0000000 --- a/temp-not-in-use/aboutPorto/portoCard.tsx.backup +++ /dev/null @@ -1,194 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - - -/** This component has types and data in portoCard.utils.ts - * NOT as other components in @/types @/data -*/ - -"use client"; - -import React from "react"; -import { useTranslations } from "next-intl"; -import Link from "next/link"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Code2, ExternalLink, CheckCircle } from "lucide-react"; -import type { PortoCardProps } from "@/types/aboutPorto"; -import { - useResponsiveConfig, - getFeatureGridClasses, - getTechStackHighlights, - PortoCardUtils, -} from "./portoCard.utils"; -import { FeatureGrid, TechHighlights } from "@/components/aboutPorto"; -import VersionDisplay from "@/components/VersionDisplay"; - -export default React.memo(function PortoCard({ className }: PortoCardProps) { - const t = useTranslations("PortfolioAbout"); - const { deviceType, containerClasses, cardClasses, featuresConfig } = - useResponsiveConfig(); - - // Memoize responsive configurations to prevent recalculation - const gridClasses = React.useMemo( - () => getFeatureGridClasses(deviceType), - [deviceType] - ); - const techHighlights = React.useMemo( - () => getTechStackHighlights(deviceType), - [deviceType] - ); - const headerLayout = React.useMemo( - () => PortoCardUtils.getHeaderLayout(deviceType), - [deviceType] - ); - const shouldShowTechHighlights = React.useMemo( - () => PortoCardUtils.shouldShowSection("techHighlights", deviceType), - [deviceType] - ); - const descriptionLength = React.useMemo( - () => PortoCardUtils.getDescriptionLength(deviceType), - [deviceType] - ); - - // Memoize description processing to avoid repeated string operations - const description = React.useMemo(() => { - const fullDescription = t("description"); - if (descriptionLength === "short") { - // Truncate description for mobile - const sentences = fullDescription.split(". "); - return sentences.slice(0, 2).join(". ") + "."; - } - return fullDescription; - }, [t, descriptionLength]); - - return ( -
- - -
-
- - {description} - - - {/* Performance Badge */} -
-
-
- - -
-

- {t("featuresTitle")} -

- -
- - {/* Tech Stack Highlights - Hidden on mobile */} - {shouldShowTechHighlights && ( - - )} - - {/* Read More Section - Compact on mobile */} -
-

- {t("actionsTitle")} -

-
- -
-
-
- -
- - Portfolio{" "} - - -
-
-
-
- ); -}); diff --git a/temp-not-in-use/aboutPorto/portoCard.utils.ts.backup b/temp-not-in-use/aboutPorto/portoCard.utils.ts.backup deleted file mode 100644 index 47bc22d..0000000 --- a/temp-not-in-use/aboutPorto/portoCard.utils.ts.backup +++ /dev/null @@ -1,257 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -import React from "react"; -import { - Atom, - Database, - Zap, - Layout, - GitBranch, - Languages, - FolderTree, - Bot, -} from "lucide-react"; -import { debounce } from "perfect-debounce"; -import type { - DeviceType, - PortoCardFeature, - ResponsiveConfig, -} from "@/types/aboutPorto"; - -export function useResponsiveConfig(): ResponsiveConfig { - const [deviceType, setDeviceType] = React.useState("desktop"); - - React.useEffect(() => { - const checkDeviceType = (): void => { - const width = window.innerWidth; - if (width < 768) { - setDeviceType("mobile"); - } else if (width < 1024) { - setDeviceType("tablet"); - } else { - setDeviceType("desktop"); - } - }; - - // Debounce resize handler to improve performance - const debouncedHandler = debounce(checkDeviceType, 150) as () => void; - - // Set initial device type - checkDeviceType(); - - window.addEventListener("resize", debouncedHandler); - return () => { - window.removeEventListener("resize", debouncedHandler); - }; - }, []); - - // Memoize computed values to prevent unnecessary recalculations - const containerClasses = React.useMemo( - () => getContainerClasses(deviceType), - [deviceType] - ); - const cardClasses = React.useMemo( - () => getCardClasses(deviceType), - [deviceType] - ); - const featuresConfig = React.useMemo( - () => getFeaturesConfig(deviceType), - [deviceType] - ); - - return React.useMemo( - () => ({ - deviceType, - containerClasses, - cardClasses, - featuresConfig, - }), - [deviceType, containerClasses, cardClasses, featuresConfig] - ); -} - -/** - * Gets container CSS classes based on device type - */ -function getContainerClasses(deviceType: DeviceType): string { - switch (deviceType) { - case "mobile": - return "container mx-auto px-4 py-6"; - case "tablet": - return "container mx-auto px-6 py-8"; - case "desktop": - return "container mx-auto px-4 py-8"; - default: - return "container mx-auto px-4 py-8"; - } -} - -/** - * Gets card CSS classes based on device type - */ -function getCardClasses(deviceType: DeviceType): string { - switch (deviceType) { - case "mobile": - return "w-full mx-auto"; - case "tablet": - return "w-full max-w-5xl mx-auto"; - case "desktop": - return "w-full max-w-6xl mx-auto"; - default: - return "w-full max-w-6xl mx-auto"; - } -} - -/** - * Gets features configuration based on device type - */ -function getFeaturesConfig(deviceType: DeviceType): { - features: PortoCardFeature[]; - showAll: boolean; -} { - const allFeatures: PortoCardFeature[] = [ - { - key: "techStack", - icon: React.createElement(Atom, { className: "h-4 w-4" }), - badges: ["Next.js 15.5.1", "React 19", "TypeScript", "App Router"], - priority: 1, - }, - { - key: "performance", - icon: React.createElement(Zap, { className: "h-4 w-4" }), - badges: ["95+ Lighthouse", "SEO 100/100", "A11y Optimized"], - priority: 1, - }, - { - key: "cleanArchitecture", - icon: React.createElement(FolderTree, { className: "h-4 w-4" }), - badges: ["/lib", "/data", "/hooks", "/types"], - priority: 2, - }, - { - key: "database", - icon: React.createElement(Database, { className: "h-4 w-4" }), - badges: ["PostgreSQL", "Prisma ORM", "Neon DB"], - priority: 2, - }, - { - key: "aiChatbot", - icon: React.createElement(Bot, { className: "h-4 w-4" }), - badges: ["AI Assistant", "Portfolio Guide", "Interactive Help"], - priority: 1, - }, - { - key: "mainFeatures", - icon: React.createElement(Layout, { className: "h-4 w-4" }), - badges: ["Blog System", "Media Gallery", "Content Library"], - priority: 3, - }, - { - key: "techFeatures", - icon: React.createElement(GitBranch, { className: "h-4 w-4" }), - badges: ["MCP GitHub", "Live PageSpeed", "CI/CD Automation"], - priority: 3, - }, - { - key: "localization", - icon: React.createElement(Languages, { className: "h-4 w-4" }), - badges: ["5 Languages", "Light/Dark Themes", "Auto-detection"], - priority: 3, - }, - ]; - - switch (deviceType) { - case "mobile": - // Show only top priority features on mobile - return { - features: allFeatures.filter((f) => f.priority === 1), - showAll: false, - }; - case "tablet": - // Show top 2 priority levels on tablet - return { - features: allFeatures.filter((f) => f.priority <= 2), - showAll: false, - }; - case "desktop": - // Show all features on desktop - return { - features: allFeatures, - showAll: true, - }; - default: - return { - features: allFeatures, - showAll: true, - }; - } -} - -/** - * Gets grid configuration for features display - */ -export function getFeatureGridClasses(deviceType: DeviceType): string { - switch (deviceType) { - case "mobile": - return "grid grid-cols-1 gap-3"; - case "tablet": - return "grid grid-cols-2 gap-4"; - case "desktop": - return "grid grid-cols-3 gap-4"; - default: - return "grid grid-cols-3 gap-4"; - } -} - -/** - * Gets the tech stack highlights for display - */ -export function getTechStackHighlights(deviceType: DeviceType): string[] { - const allTechs = [ - "Tailwind CSS", - "shadcnUI", - "Framer Motion", - "Embla Carousel", - "next-intl", - "Vercel Edge", - "Zod Validation", - "ESLint 9.x", - ]; - - switch (deviceType) { - case "mobile": - return allTechs.slice(0, 4); // Show only 4 on mobile - case "tablet": - return allTechs.slice(0, 6); // Show 6 on tablet - case "desktop": - return allTechs; // Show all on desktop - default: - return allTechs; - } -} - -/** - * Component-specific utilities - */ -export const PortoCardUtils = { - getHeaderLayout(deviceType: DeviceType): "vertical" | "horizontal" { - return deviceType === "mobile" ? "vertical" : "horizontal"; - }, - - shouldShowSection( - section: "techHighlights" | "readMore", - deviceType: DeviceType - ): boolean { - if (section === "techHighlights") { - return deviceType !== "mobile"; // Hide tech highlights on mobile to reduce height - } - return true; - }, - - getDescriptionLength(deviceType: DeviceType): "short" | "full" { - return deviceType === "mobile" ? "short" : "full"; - }, -}; diff --git a/temp-not-in-use/assets/cer2.png b/temp-not-in-use/assets/cer2.png deleted file mode 100644 index 713abfd..0000000 Binary files a/temp-not-in-use/assets/cer2.png and /dev/null differ diff --git a/temp-not-in-use/assets/githubC.png b/temp-not-in-use/assets/githubC.png deleted file mode 100644 index 4b29c8d..0000000 Binary files a/temp-not-in-use/assets/githubC.png and /dev/null differ diff --git a/temp-not-in-use/assets/htmlC.png b/temp-not-in-use/assets/htmlC.png deleted file mode 100644 index 964ac15..0000000 Binary files a/temp-not-in-use/assets/htmlC.png and /dev/null differ diff --git a/temp-not-in-use/assets/nodecer.jpg b/temp-not-in-use/assets/nodecer.jpg deleted file mode 100644 index 390c06b..0000000 Binary files a/temp-not-in-use/assets/nodecer.jpg and /dev/null differ diff --git a/temp-not-in-use/tech/Technologies.logic.ts.backup b/temp-not-in-use/tech/Technologies.logic.ts.backup deleted file mode 100644 index 77899c5..0000000 --- a/temp-not-in-use/tech/Technologies.logic.ts.backup +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. - * - * BACKUP - Original Technologies.logic.ts file moved here for reference -*/ - -import { techGroups } from "@/data/tech"; -import type { KeyboardEvent } from "react"; - -/** - * Configuration object for carousel dimensions and layout - */ -interface CarouselConfig { - /** Height of the carousel container in pixels */ - readonly height: number; - /** CSS grid classes for responsive layout */ - readonly gridClasses: string; -} - -/** - * Handler functions for card interactions - */ -interface CardInteractionHandlers { - /** Handle mouse enter event */ - readonly onMouseEnter: () => void; - /** Handle mouse leave event */ - readonly onMouseLeave: () => void; - /** Handle keyboard navigation */ - readonly onKeyDown: (e: KeyboardEvent) => void; -} - -export function calculateCarouselConfig( - cardsPerSlide: number, - maxItems: number -): CarouselConfig { - // Responsive carousel height - smaller on mobile devices - const baseHeight = - cardsPerSlide === 1 ? 240 : cardsPerSlide === 2 ? 260 : 280; - const itemHeight = cardsPerSlide === 1 ? 20 : 25; // Even tighter on mobile - const height = Math.max(baseHeight, baseHeight + (maxItems - 4) * itemHeight); - - // Grid classes based on cards per slide - const gridClasses = - cardsPerSlide === 1 - ? "grid-cols-1" - : cardsPerSlide === 2 - ? "grid-cols-2" - : "grid-cols-3"; - - return { height, gridClasses }; -} - - -export function generateSlides(cardsPerSlide: number): (typeof techGroups)[] { - const slides: (typeof techGroups)[] = []; - for (let i = 0; i < techGroups.length; i += cardsPerSlide) { - slides.push(techGroups.slice(i, i + cardsPerSlide)); - } - return slides; -} - - -export function getMaxItemsCount(): number { - return Math.max(...techGroups.map((group) => group.items.length)); -} - -export function createCardInteractionHandlers( - categoryId: string, - currentHoveredCard: string | null, - setHoveredCard: (card: string | null) => void -): CardInteractionHandlers { - const isCurrentCardHovered = currentHoveredCard === categoryId; - - return { - onMouseEnter: () => setHoveredCard(categoryId), - onMouseLeave: () => setHoveredCard(null), - onKeyDown: (e: KeyboardEvent) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - setHoveredCard(isCurrentCardHovered ? null : categoryId); - } - }, - }; -} diff --git a/temp-not-in-use/tech/Technologies.tsx.backup b/temp-not-in-use/tech/Technologies.tsx.backup deleted file mode 100644 index 780aff9..0000000 --- a/temp-not-in-use/tech/Technologies.tsx.backup +++ /dev/null @@ -1,213 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. - * - * BACKUP 1 - Carousel-based layout version - * - Uses techGroups data structure - * - Carousel with slides and navigation buttons - * - useResponsiveCarousel hook for responsive card counts - * - Complex hover state with animated overlays - * - Technologies.logic helper functions - * - More elaborate accessibility features -*/ - -"use client"; - -import { motion } from "framer-motion"; -import type { techGroups } from "@/data/tech"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { useState } from "react"; -import { useTranslations } from "next-intl"; -import { - getCardHoverClasses, - getOverlayStyles, -} from "@/components/visuals/card-animations"; -import { - Carousel, - CarouselContent, - CarouselItem, - CarouselNext, - CarouselPrevious, - type CarouselApi, -} from "@/components/ui/carousel"; -import { useResponsiveCarousel } from "@/hooks/use-responsive-carousel"; -import { - calculateCarouselConfig, - generateSlides, - getMaxItemsCount, - createCardInteractionHandlers, -} from "./Technologies.logic"; - -export default function Technologies() { - const [hoveredCard, setHoveredCard] = useState(null); - const [currentSlide, setCurrentSlide] = useState(0); - const t = useTranslations("Technologies"); - const tCategories = useTranslations("Technologies.categories"); - const { cardsPerSlide } = useResponsiveCarousel(); - - // Calculate carousel configuration using logic functions - const maxItems = getMaxItemsCount(); - const carouselConfig = calculateCarouselConfig(cardsPerSlide, maxItems); - const slides = generateSlides(cardsPerSlide); - - const renderTechCard = (group: (typeof techGroups)[0]) => { - const isCurrentCardHovered = hoveredCard === group.category; - const cardHandlers = createCardInteractionHandlers( - group.category, - hoveredCard, - setHoveredCard - ); - - return ( - - - - {tCategories(group.categoryKey)} - - - -
- {group.items.map(({ name, Icon }) => ( - - - ))} -
-
-
- - ); - }; - - return ( -
- - - - {t("title")} - - - - {/* Hidden instructions for screen readers */} - - {/* Live region for slide announcements */} -
- Slide {currentSlide + 1} of {slides.length} -
-
- { - if (api) { - api.on("select", () => { - setCurrentSlide(api.selectedScrollSnap()); - }); - } - }} - > - - {slides.map((slide, slideIndex) => ( - -
- {slide.map((group) => renderTechCard(group))} -
-
- ))} -
- - {/* Custom Navigation Buttons - Enhanced for better theming */} - - -
-
-
-
- -

- {t("manyMoreTechnologies")} -

-
-
- ); -} diff --git a/temp-not-in-use/tech/Technologies.tsx.backup2 b/temp-not-in-use/tech/Technologies.tsx.backup2 deleted file mode 100644 index a6badb3..0000000 --- a/temp-not-in-use/tech/Technologies.tsx.backup2 +++ /dev/null @@ -1,120 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. - * - * BACKUP 2 - Simple vertical scroll layout version - * - Uses serviceGroups data structure (with subcategories) - * - No carousel - stacked vertical sections - * - Simpler animation (fade + slide up) - * - Nested subcategory cards - * - Cleaner, more minimal code (~114 lines vs ~207) -*/ - -"use client"; - -import { motion } from "framer-motion"; -import { serviceGroups } from "@/data/tech"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { useTranslations } from "next-intl"; - -export default function Technologies() { - const t = useTranslations("Technologies"); - const tCategories = useTranslations("Technologies.categories"); - const tDescriptions = useTranslations("Technologies.descriptions"); - const tSubCategories = useTranslations("Technologies.subCategories"); - - return ( -
- - - - {t("title")} - - - - {serviceGroups.map((service, index) => ( - - {/* Service Header */} -
-

- {tCategories(service.categoryKey)} -

-

- {tDescriptions(service.descriptionKey)} -

-
- - {/* Subcategories Grid */} -
- {service.subCategories.map((subCategory) => ( - - - - {tSubCategories(subCategory.nameKey)} - - - -
- {subCategory.items.map(({ name, Icon }) => ( - - - ))} -
-
-
- ))} -
- - {/* Divider between services (except last) */} - {index < serviceGroups.length - 1 && ( -
-
-
- )} - - ))} - - - -

- {t("manyMoreTechnologies")} -

-
-
- ); -} diff --git a/temp-not-in-use/tech/index.ts.backup b/temp-not-in-use/tech/index.ts.backup deleted file mode 100644 index 20f89e7..0000000 --- a/temp-not-in-use/tech/index.ts.backup +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. - * - * BACKUP - Original index.ts file moved here for reference -*/ - -export { default as Technologies } from "./Technologies"; diff --git a/temp-not-in-use/tech/tech.ts.backup b/temp-not-in-use/tech/tech.ts.backup deleted file mode 100644 index 7987946..0000000 --- a/temp-not-in-use/tech/tech.ts.backup +++ /dev/null @@ -1,244 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. - * - * BACKUP - Original tech.ts file moved here for reference -*/ - -import { - SiReact, - SiNextdotjs, - SiVite, - SiTailwindcss, - SiJavascript, - SiTypescript, - SiN8N, - SiNodedotjs, - SiPython, - SiPrisma, - SiPostgresql, - SiSupabase, - SiOllama, - SiDocker, - SiGooglecloud, - SiFirebase, - SiVercel, - SiNetlify, - SiGithubactions, - SiLangchain, - SiGit, - SiGithub, - SiBitbucket, - SiJira, - SiNotion, - SiMiro, - SiWebstorm, - SiPycharm, - SiMarkdown, - SiLatex, - SiGoogleanalytics, -} from "react-icons/si"; -import { - FaBrain, - FaRobot, - FaFigma, - FaUsers, - FaLightbulb, - FaSearch, - FaVideo, - FaImage, - FaBook, - FaDatabase, - FaCode, - FaNetworkWired, - FaCogs, - FaLayerGroup, - FaPuzzlePiece, - FaShieldAlt, - FaLock, - FaKey, - FaUserShield, -} from "react-icons/fa"; -import { RiOpenaiFill, RiFlowChart } from "react-icons/ri"; -import { BiLogoVisualStudio } from "react-icons/bi"; -import { DiScrum } from "react-icons/di"; -import { FiServer } from "react-icons/fi"; -import { TbAutomation, TbVectorTriangle, TbBinaryTree } from "react-icons/tb"; -import { - Box, - Database, - MessageCircle, - Clock, - FileText, - Network, - Monitor, - MonitorSpeaker, - Cpu, - Workflow, -} from "lucide-react"; - -export interface TechItem { - name: string; - Icon: React.ComponentType<{ size?: number; className?: string }>; -} - -export interface TechGroup { - category: string; - categoryKey: string; - items: TechItem[]; -} - -export const techGroups: TechGroup[] = [ - { - category: "UI & Frontend", - categoryKey: "uiFrontend", - items: [ - { name: "React", Icon: SiReact }, - { name: "Next.js", Icon: SiNextdotjs }, - { name: "Vite", Icon: SiVite }, - { name: "TailwindCSS", Icon: SiTailwindcss }, - { name: "JavaScript", Icon: SiJavascript }, - { name: "TypeScript", Icon: SiTypescript }, - { name: "shadcnUI", Icon: Box }, - ], - }, - { - category: "Backend & APIs", - categoryKey: "backendApis", - items: [ - { name: "Node.js", Icon: SiNodedotjs }, - { name: "Python", Icon: SiPython }, - { name: "Prisma", Icon: SiPrisma }, - { name: "REST APIs", Icon: FiServer }, - { name: "LangChain", Icon: SiLangchain }, - ], - }, - { - category: "Core Programming Concepts", - categoryKey: "coreProgrammingConcepts", - items: [ - { name: "Object-Oriented Programming", Icon: FaLayerGroup }, - { name: "Data Structures", Icon: TbBinaryTree }, - { name: "Algorithms", Icon: Cpu }, - { name: "Design Patterns", Icon: FaPuzzlePiece }, - ], - }, - { - category: "Architecture & System Design", - categoryKey: "architectureSystemDesign", - items: [ - { name: "Software Architecture Patterns", Icon: Workflow }, - { name: "Basic Networking", Icon: FaNetworkWired }, - { name: "System Design", Icon: FaCogs }, - { name: "Code Quality & Clean Code", Icon: FaCode }, - ], - }, - { - category: "Databases & Storage", - categoryKey: "databasesStorage", - items: [ - { name: "PostgreSQL", Icon: SiPostgresql }, - { name: "Supabase", Icon: SiSupabase }, - { name: "Neon", Icon: Database }, - { name: "Vector Databases", Icon: TbVectorTriangle }, - { name: "Data Stack", Icon: FaDatabase }, - ], - }, - { - category: "Infrastructure & DevOps", - categoryKey: "infrastructureDevops", - items: [ - { name: "Docker", Icon: SiDocker }, - { name: "Google Cloud", Icon: SiGooglecloud }, - { name: "Firebase", Icon: SiFirebase }, - { name: "Vercel", Icon: SiVercel }, - { name: "Netlify", Icon: SiNetlify }, - { name: "Windows OS", Icon: Monitor }, - ], - }, - { - category: "AI & Automation", - categoryKey: "aiAutomation", - items: [ - { name: "ChatGPT", Icon: RiOpenaiFill }, - { name: "LLM Integration", Icon: FaBrain }, - { name: "Ollama", Icon: SiOllama }, - { name: "RAG Systems", Icon: Network }, - { name: "MCPs", Icon: FileText }, - { name: "n8n", Icon: SiN8N }, - { name: "GitHub Actions", Icon: SiGithubactions }, - { name: "LangFlow", Icon: TbAutomation }, - { name: "Workflow Automation", Icon: FaRobot }, - ], - }, - { - category: "Development Workflow", - categoryKey: "developmentWorkflow", - items: [ - { name: "Git", Icon: SiGit }, - { name: "GitHub", Icon: SiGithub }, - { name: "Bitbucket", Icon: SiBitbucket }, - { name: "VSCode", Icon: BiLogoVisualStudio }, - { name: "WebStorm", Icon: SiWebstorm }, - { name: "PyCharm", Icon: SiPycharm }, - ], - }, - { - category: "Design & Creative", - categoryKey: "designCreative", - items: [ - { name: "Figma", Icon: FaFigma }, - { name: "Photo Editing", Icon: FaImage }, - { name: "Video Editing", Icon: FaVideo }, - ], - }, - { - category: "Business & Productivity Tools", - categoryKey: "businessProductivity", - items: [ - { name: "Jira", Icon: SiJira }, - { name: "Notion", Icon: SiNotion }, - { name: "Miro", Icon: SiMiro }, - { name: "SEO & Google Console", Icon: SiGoogleanalytics }, - ], - }, - { - category: "Documentation & Technical Writing", - categoryKey: "documentationTechnicalWriting", - items: [ - { name: "Diagrams & Flowcharts", Icon: RiFlowChart }, - { name: "LaTeX", Icon: SiLatex }, - { name: "Markdown", Icon: SiMarkdown }, - { name: "PowerPoint Presentations", Icon: MonitorSpeaker }, - ], - }, - { - category: "Professional Skills", - categoryKey: "professionalSkills", - items: [ - { name: "SCRUM/Agile", Icon: DiScrum }, - { name: "Research & Information Finding", Icon: FaSearch }, - { name: "Technical Documentation", Icon: FaBook }, - ], - }, - { - category: "Soft Skills", - categoryKey: "softSkills", - items: [ - { name: "Team Collaboration", Icon: FaUsers }, - { name: "Communication", Icon: MessageCircle }, - { name: "Problem Solving", Icon: FaLightbulb }, - { name: "Time Management", Icon: Clock }, - ], - }, - { - category: "Security & Privacy", - categoryKey: "securityPrivacy", - items: [ - { name: "Data Protection", Icon: FaShieldAlt }, - { name: "Authentication & Authorization", Icon: FaLock }, - { name: "API Security", Icon: FaKey }, - { name: "Privacy Compliance", Icon: FaUserShield }, - ], - }, -]; diff --git a/temp-not-in-use/tech/translations.backup.json b/temp-not-in-use/tech/translations.backup.json deleted file mode 100644 index 84ad08a..0000000 --- a/temp-not-in-use/tech/translations.backup.json +++ /dev/null @@ -1,117 +0,0 @@ -{ - "en": { - "Technologies": { - "title": "Skills, Technologies & Expertise", - "manyMoreTechnologies": "...and many more skills & technologies in my toolkit", - "categories": { - "uiFrontend": "UI & Frontend", - "backendApis": "Backend & APIs", - "softwareEngineeringFundamentals": "Software Engineering Fundamentals", - "coreProgrammingConcepts": "Core Programming Concepts", - "architectureSystemDesign": "Architecture & System Design", - "databasesStorage": "Databases & Storage", - "infrastructureDevops": "Infrastructure & DevOps", - "aiAutomation": "AI & Automation", - "developmentWorkflow": "Development Workflow", - "designCreative": "Design & Creative", - "businessProductivity": "Business & Productivity Tools", - "documentationTechnicalWriting": "Documentation & Technical Writing", - "professionalSkills": "Professional Skills", - "softSkills": "Soft Skills", - "securityPrivacy": "Security & Privacy" - } - } - }, - "de": { - "Technologies": { - "title": "Fähigkeiten, Technologien & Expertise", - "manyMoreTechnologies": "...und viele weitere Fähigkeiten & Technologien in meinem Toolkit", - "categories": { - "uiFrontend": "UI & Frontend", - "backendApis": "Backend & APIs", - "softwareEngineeringFundamentals": "Software-Engineering Grundlagen", - "coreProgrammingConcepts": "Programmierkonzepte", - "architectureSystemDesign": "Architektur & Systemdesign", - "databasesStorage": "Datenbanken & Speicher", - "infrastructureDevops": "Infrastruktur & DevOps", - "aiAutomation": "KI & Automatisierung", - "developmentWorkflow": "Entwicklungsworkflow", - "designCreative": "Design & Kreativ", - "businessProductivity": "Business & Produktivitäts-Tools", - "documentationTechnicalWriting": "Dokumentation & Technisches Schreiben", - "professionalSkills": "Berufliche Fähigkeiten", - "softSkills": "Soft Skills", - "securityPrivacy": "Sicherheit & Datenschutz" - } - } - }, - "es": { - "Technologies": { - "title": "Habilidades, Tecnologías y Experiencia", - "manyMoreTechnologies": "...y muchas más habilidades y tecnologías en mi caja de herramientas", - "categories": { - "uiFrontend": "UI & Frontend", - "backendApis": "Backend & APIs", - "softwareEngineeringFundamentals": "Fundamentos de Ingeniería de Software", - "coreProgrammingConcepts": "Conceptos de Programación", - "architectureSystemDesign": "Arquitectura y Diseño de Sistemas", - "databasesStorage": "Bases de datos & Almacenamiento", - "infrastructureDevops": "Infraestructura & DevOps", - "aiAutomation": "IA & Automatización", - "developmentWorkflow": "Flujo de Desarrollo", - "designCreative": "Diseño & Creatividad", - "businessProductivity": "Herramientas de Negocio & Productividad", - "documentationTechnicalWriting": "Documentación & Escritura Técnica", - "professionalSkills": "Habilidades Profesionales", - "softSkills": "Habilidades Blandas", - "securityPrivacy": "Seguridad y Privacidad" - } - } - }, - "fr": { - "Technologies": { - "title": "Compétences, Technologies et Expertise", - "manyMoreTechnologies": "...et bien d'autres compétences et technologies dans ma boîte à outils", - "categories": { - "uiFrontend": "UI & Frontend", - "backendApis": "Backend & APIs", - "softwareEngineeringFundamentals": "Fondamentaux du Génie Logiciel", - "coreProgrammingConcepts": "Concepts de Programmation", - "architectureSystemDesign": "Architecture & Conception de Systèmes", - "databasesStorage": "Bases de données & Stockage", - "infrastructureDevops": "Infrastructure & DevOps", - "aiAutomation": "IA & Automatisation", - "developmentWorkflow": "Flux de Développement", - "designCreative": "Design & Créativité", - "businessProductivity": "Outils Business & Productivité", - "documentationTechnicalWriting": "Documentation & Rédaction Technique", - "professionalSkills": "Compétences Professionnelles", - "softSkills": "Compétences Douces", - "securityPrivacy": "Sécurité et Confidentialité" - } - } - }, - "sv": { - "Technologies": { - "title": "Färdigheter, Teknologier & Expertis", - "manyMoreTechnologies": "...och många fler färdigheter och teknologier i min verktygslåda", - "categories": { - "uiFrontend": "UI & Frontend", - "backendApis": "Backend & API:er", - "softwareEngineeringFundamentals": "Mjukvaruteknik Grunderna", - "coreProgrammingConcepts": "Programmeringskoncept", - "architectureSystemDesign": "Arkitektur & Systemdesign", - "databasesStorage": "Databaser & Lagring", - "infrastructureDevops": "Infrastruktur & DevOps", - "aiAutomation": "AI & Automatisering", - "developmentWorkflow": "Utvecklingsflöde", - "designCreative": "Design & Kreativitet", - "businessProductivity": "Business & Produktivitetsverktyg", - "documentationTechnicalWriting": "Dokumentation & Teknisk Skrivande", - "professionalSkills": "Professionella Färdigheter", - "softSkills": "Mjuka Färdigheter", - "securityPrivacy": "Säkerhet & Integritet" - } - } - } -} diff --git a/temp-not-in-use/tech/use-responsive-carousel.ts.backup b/temp-not-in-use/tech/use-responsive-carousel.ts.backup deleted file mode 100644 index e68bdfa..0000000 --- a/temp-not-in-use/tech/use-responsive-carousel.ts.backup +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -"use client"; - -import { useState, useEffect } from "react"; - -/** - * Responsive breakpoint configuration for carousel display - */ -interface ResponsiveCarouselConfig { - /** Number of cards to display per slide */ - readonly cardsPerSlide: number; - /** Whether the current view is mobile */ - readonly isMobile: boolean; -} - -/** - * Hook to manage responsive carousel behavior based on screen size - * @returns Configuration object with cardsPerSlide and isMobile flags - */ -export function useResponsiveCarousel(): ResponsiveCarouselConfig { - const [cardsPerSlide, setCardsPerSlide] = useState(3); - const [isMobile, setIsMobile] = useState(false); - - useEffect(() => { - const checkScreenSize = (): void => { - const width = window.innerWidth; - if (width < 640) { - // mobile - setCardsPerSlide(1); - setIsMobile(true); - } else if (width < 1024) { - // tablet - setCardsPerSlide(2); - setIsMobile(false); - } else { - // desktop - setCardsPerSlide(3); - setIsMobile(false); - } - }; - - checkScreenSize(); - window.addEventListener("resize", checkScreenSize); - return () => window.removeEventListener("resize", checkScreenSize); - }, []); - - return { cardsPerSlide, isMobile }; -} diff --git a/types/configs/pagespeed.ts b/types/configs/pagespeed.ts deleted file mode 100644 index 045bcfe..0000000 --- a/types/configs/pagespeed.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * PageSpeed Interface Types - * @author ColdByDefault - * @copyright 2026 ColdByDefault. All Rights Reserved. -*/ - -export interface PageSpeedMetrics { - performance: number; - accessibility: number; - bestPractices: number; - seo: number; - pwa?: number; -} - -export interface PageSpeedResult { - url: string; - strategy: "mobile" | "desktop"; - metrics: PageSpeedMetrics; - loadingExperience?: { - metrics: { - FIRST_CONTENTFUL_PAINT_MS?: { percentile: number }; - FIRST_INPUT_DELAY_MS?: { percentile: number }; - LARGEST_CONTENTFUL_PAINT_MS?: { percentile: number }; - CUMULATIVE_LAYOUT_SHIFT_SCORE?: { percentile: number }; - }; - }; -} - -export interface PageSpeedApiResponse { - error?: string; - details?: string; - url?: string; - strategy?: string; - metrics?: PageSpeedMetrics; - loadingExperience?: PageSpeedResult["loadingExperience"]; - retryAfter?: number; -} - -export interface PageSpeedInsightsProps { - url?: string; - showRefreshButton?: boolean; - showBothStrategies?: boolean; -} - -export interface PageSpeedLighthouseCategory { - id: string; - title: string; - score: number | null; -} - -export interface PageSpeedLighthouseResult { - categories: { - performance?: PageSpeedLighthouseCategory; - accessibility?: PageSpeedLighthouseCategory; - "best-practices"?: PageSpeedLighthouseCategory; - seo?: PageSpeedLighthouseCategory; - pwa?: PageSpeedLighthouseCategory; - }; -} - -export interface PageSpeedApiRawResponse { - id?: string; - lighthouseResult?: PageSpeedLighthouseResult; -}