diff --git a/.github/workflows/lighthouse-ci.yml b/.github/workflows/lighthouse-ci.yml new file mode 100644 index 0000000..251b648 --- /dev/null +++ b/.github/workflows/lighthouse-ci.yml @@ -0,0 +1,58 @@ +name: Lighthouse CI + +on: + pull_request: + branches: ["main"] + push: + branches: ["main"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: lighthouse-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + lighthouse: + name: Lighthouse budgets + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + ADMIN_TOKEN: lhci-local-admin-token + CHATBOT_ENABLED: "false" + CI: "true" + DATABASE_URL: postgresql://lhci:lhci@localhost:5432/lhci?schema=public + NEXT_TELEMETRY_DISABLED: "1" + REWRITER_ENABLED: "false" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build production app + run: npm run build + + - name: Run Lighthouse CI + run: npm run lhci + + - name: Upload Lighthouse reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: lighthouse-ci-reports + path: | + .lighthouseci + lighthouse/reports + if-no-files-found: ignore + retention-days: 14 diff --git a/.gitignore b/.gitignore index dac5795..80913cb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ !.yarn/versions # testing /coverage +/test-results +/playwright-report +/blob-report +/.lighthouseci +/lighthouse/reports # next.js /.next/ /out/ diff --git a/AGENTS.md b/AGENTS.md index f784206..dc6b231 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -62,7 +62,7 @@ public/assets/ Images, icons, use-case visuals agents/ Custom VS Code Copilot agents instructions/ Auto-loaded instructions for .ts/.tsx files skills/ On-demand skills (i18n-checker, portfolio-architecture) - workflows/ GitHub Actions (bump-version, codeql, dependency-review) + workflows/ GitHub Actions (bump-version, codeql, dependency-review, lighthouse-ci) copilot-instructions.md VS Code Copilot always-on context CODEOWNERS All files owned by @ColdByDefault ``` @@ -154,5 +154,6 @@ components/[Feature]/ | `bump-version.yml` | push to `main` | Bumps patch version in `package.json` + `README.md`, tags release | | `codeql.yml` | push / PR / schedule | CodeQL security analysis | | `dependency-review.yml` | PR | Dependency vulnerability check | +| `lighthouse-ci.yml` | push / PR / manual | Lighthouse CI performance, accessibility, and resource budgets | The bump workflow commits with `[skip ci]` to prevent loops. diff --git a/README.md b/README.md index 716310f..d66621e 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,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 | Groq API           | +| `/api/chatbot`   | Interactive AI chatbot (Reem) for visitor queries | OpenAI Responses API | | `/api/admin`     | Administrative operations for content             | Secured endpoints                 | Controls: @@ -256,6 +256,13 @@ Automated security and quality workflows ensuring code integrity and vulnerabili * **Features**: Blocks PRs with vulnerable dependencies, provides detailed security reports in PR comments * **Integration**: Automated comments on pull requests with dependency security analysis +**Lighthouse CI Performance Budgets:** + +* **Triggers**: Pushes and pull requests to main branch, plus manual dispatch +* **Purpose**: Fails regressions before deployment when Lighthouse scores, Core Web Vitals lab metrics, or resource budgets cross configured thresholds +* **Budgets**: Performance score >= 0.80, accessibility score >= 0.95, LCP <= 2.5s, CLS <= 0.1, TBT <= 300ms, script <= 375 KiB, image <= 1,250 KiB, total page weight <= 2,000 KiB +* **Reports**: Saved as GitHub Actions artifacts from local filesystem output; no public temporary Lighthouse upload required + **Vercel CRON Jobs & Automation:** * **PageSpeed Data Refresh**: Automated background refresh every 12 hours (`0 */12 * * *`) diff --git a/app/(legals)/layout.tsx b/app/(legals)/layout.tsx index f03e92d..44311a8 100644 --- a/app/(legals)/layout.tsx +++ b/app/(legals)/layout.tsx @@ -7,7 +7,7 @@ import type { Metadata } from "next"; import { getLocale } from "next-intl/server"; import { generateLegalPageSEO } from "@/lib/configs/seo"; -import { Background } from "@/components/visuals"; +import { ClientBackground } from "@/components/visuals"; export async function generateMetadata(): Promise { const locale = await getLocale(); @@ -23,7 +23,7 @@ export default function LegalsGroupLayout({ return (
{children} - +
); } diff --git a/app/(live-tools)/automation-audit/page.tsx b/app/(live-tools)/automation-audit/page.tsx new file mode 100644 index 0000000..3569f13 --- /dev/null +++ b/app/(live-tools)/automation-audit/page.tsx @@ -0,0 +1,15 @@ +/** + * @author © ColdByDefault + * @license Copyright (c) 2026 ColdByDefault. All rights reserved. + * @version 6.x.x + */ + +import { AuditWizard } from "@/components/live-tools/automation-audit"; + +export default function AutomationAuditPage() { + return ( +
+ +
+ ); +} diff --git a/app/(live-tools)/layout.tsx b/app/(live-tools)/layout.tsx index f03e92d..44311a8 100644 --- a/app/(live-tools)/layout.tsx +++ b/app/(live-tools)/layout.tsx @@ -7,7 +7,7 @@ import type { Metadata } from "next"; import { getLocale } from "next-intl/server"; import { generateLegalPageSEO } from "@/lib/configs/seo"; -import { Background } from "@/components/visuals"; +import { ClientBackground } from "@/components/visuals"; export async function generateMetadata(): Promise { const locale = await getLocale(); @@ -23,7 +23,7 @@ export default function LegalsGroupLayout({ return (
{children} - +
); } diff --git a/app/(media)/about/page.tsx b/app/(media)/about/page.tsx index 3c3e9e0..9f08197 100644 --- a/app/(media)/about/page.tsx +++ b/app/(media)/about/page.tsx @@ -6,13 +6,21 @@ "use client"; +import dynamic from "next/dynamic"; import { Badge } from "@/components/ui/badge"; import { Card } from "@/components/ui/card"; -import { Background } from "@/components/visuals/motion-background"; import type { AboutTranslations } from "@/types/configs/i18n"; import Image from "next/image"; import { useTranslations } from "next-intl"; +const Background = dynamic( + () => + import("@/components/visuals/motion-background").then((mod) => ({ + default: mod.Background, + })), + { loading: () => null, ssr: false }, +); + export default function AboutPage() { const t = useTranslations("About"); const light = "from-black/90 to-gray-500"; diff --git a/app/(media)/blog/page.tsx b/app/(media)/blog/page.tsx index bfd1170..27a4b2e 100644 --- a/app/(media)/blog/page.tsx +++ b/app/(media)/blog/page.tsx @@ -7,6 +7,8 @@ import { BlogPageClient } from "@/components/blog"; import { getBlogs } from "@/lib/hubs/blogs"; +export const revalidate = 60; + export default async function BlogsPage() { let blogs: Awaited>["blogs"] = []; diff --git a/app/(media)/layout.tsx b/app/(media)/layout.tsx index e8f6b0b..60df932 100644 --- a/app/(media)/layout.tsx +++ b/app/(media)/layout.tsx @@ -7,7 +7,7 @@ import type { Metadata } from "next"; import { getLocale } from "next-intl/server"; import { generateMediaSectionSEO } from "@/lib/configs/seo"; -import { Background } from "@/components/visuals"; +import { ClientBackground } from "@/components/visuals"; export async function generateMetadata(): Promise { const locale = await getLocale(); @@ -23,7 +23,7 @@ export default function MediaGroupLayout({ return (
{children} - +
); } diff --git a/app/(media)/projects/page.tsx b/app/(media)/projects/page.tsx index af613a6..5b29d4c 100644 --- a/app/(media)/projects/page.tsx +++ b/app/(media)/projects/page.tsx @@ -4,7 +4,6 @@ * @version 6.x.x */ -"use client"; import { ProjectsShowcase } from "@/components/projects"; export default function LibraryPage() { diff --git a/app/(media)/services/page.tsx b/app/(media)/services/page.tsx index d4f1c6a..f15d429 100644 --- a/app/(media)/services/page.tsx +++ b/app/(media)/services/page.tsx @@ -6,18 +6,18 @@ "use client"; +import dynamic from "next/dynamic"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent } from "@/components/ui/card"; import { CTAButton } from "@/components/ui/cta-button"; import { PackageCard } from "@/components/services"; -import { Background } from "@/components/visuals/motion-background"; import { servicePackages, processSteps, trustSignals, } from "@/data/hubs/servicesData"; import type { ProcessStep, TrustSignal } from "@/types/hubs/services"; -import { motion } from "framer-motion"; +import { m } from "framer-motion"; import { MessageSquare, Target, @@ -30,6 +30,14 @@ import { } from "lucide-react"; import { useTranslations } from "next-intl"; +const Background = dynamic( + () => + import("@/components/visuals/motion-background").then((mod) => ({ + default: mod.Background, + })), + { loading: () => null, ssr: false }, +); + // Icon mapping for dynamic rendering const iconMap: Record = { MessageSquare, @@ -42,16 +50,15 @@ const iconMap: Record = { Plug, }; -// Animation variants +// Animation variants — no opacity in hidden so LCP element is never invisible const fadeInUp = { - hidden: { opacity: 0, y: 20 }, + hidden: { y: 20 }, visible: { opacity: 1, y: 0 }, }; const staggerChildren = { - hidden: { opacity: 0 }, + hidden: {}, visible: { - opacity: 1, transition: { staggerChildren: 0.1, }, @@ -73,7 +80,7 @@ function ProcessStepCard({ const IconComponent = iconMap[step.icon]; return ( - +
@@ -95,13 +102,14 @@ function ProcessStepCard({

- + ); } /** * Trust Signal Card Component */ + function TrustCard({ signal, t, @@ -112,7 +120,7 @@ function TrustCard({ const IconComponent = iconMap[signal.icon]; return ( - +
@@ -130,7 +138,7 @@ function TrustCard({
-
+ ); } @@ -147,36 +155,37 @@ export default function ServicesPage() { {/* Hero Section */}
- - + Services - - + {t("hero.title")} - - + +

{t("hero.subtitle")}

-
-
+ +
{/* Packages Section - ON TOP as per user requirement */}
- {t("packages.title")} + ( ))} - +
{/* Process Section */}
- - +

{t("process.title")}

{t("process.subtitle")}

-
+
{processSteps.map((step, index) => ( @@ -219,40 +228,40 @@ export default function ServicesPage() { ))}
-
+
{/* Trust Signals Section */}
- - +

{t("trust.title")}

{t("trust.subtitle")}

-
+
{trustSignals.map((signal) => ( ))}
-
+
{/* Final CTA Section */}
- - {t("cta.title")} - - + {t("cta.subtitle")} - - + + - + - +
diff --git a/app/admin/blocked/page.tsx b/app/admin/blocked/page.tsx index f1e4195..86ea2c5 100644 --- a/app/admin/blocked/page.tsx +++ b/app/admin/blocked/page.tsx @@ -13,14 +13,14 @@ export default function AdminBlockedPage() {
- +
- + Access Blocked
-

+

Nuh you ain't support to be doing this... NOW BANNNN!.

diff --git a/app/api/admin/blog/route.ts b/app/api/admin/blog/route.ts index 27e5761..7a42ad3 100644 --- a/app/api/admin/blog/route.ts +++ b/app/api/admin/blog/route.ts @@ -323,9 +323,10 @@ export async function GET( } } catch (error) { console.error("Blog Admin API error:", error); - const errorMessage = - error instanceof Error ? error.message : "Internal server error"; - return NextResponse.json({ error: errorMessage }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } @@ -527,8 +528,9 @@ export async function POST( } } catch (error) { console.error("Blog Admin API error:", error); - const errorMessage = - error instanceof Error ? error.message : "Internal server error"; - return NextResponse.json({ error: errorMessage }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 }, + ); } } diff --git a/app/api/automation-audit/route.ts b/app/api/automation-audit/route.ts new file mode 100644 index 0000000..2fa2370 --- /dev/null +++ b/app/api/automation-audit/route.ts @@ -0,0 +1,197 @@ +/** + * @author © ColdByDefault + * @license Copyright (c) 2026 ColdByDefault. All rights reserved. + * @version 6.x.x + */ + +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { z } from "zod"; +import { checkAuditRateLimit } from "@/lib/live-tools/audit-rate-limit"; +import { + AUDIT_SYSTEM_PROMPT, + AUDIT_QUESTION_CONTEXT, + QUESTION_IDS, +} from "@/data/live-tools/automation-audit"; +import { sanitizeErrorMessage } from "@/lib/security"; +import type { + AuditApiResponse, + AuditResult, + AuditAnswers, +} from "@/types/live-tools/automation-audit"; + +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const OPENAI_CHAT_MODEL = process.env.OPENAI_CHAT_MODEL; +const AUDIT_ENABLED = process.env.AUDIT_ENABLED !== "false"; // Default: enabled + +const optionEnum = z.enum(["a", "b", "c", "d"]); + +const auditRequestSchema = z.object({ + answers: z.object({ + q1: optionEnum, + q2: optionEnum, + q3: optionEnum, + q4: optionEnum, + q5: optionEnum, + q6: optionEnum, + q7: optionEnum, + q8: optionEnum, + }), +}); + +interface OpenAIChatResponse { + choices?: Array<{ + message?: { + content?: string | null; + }; + }>; + error?: { + message?: string; + type?: string; + code?: string; + }; +} + +function getClientIP(request: NextRequest): string { + const cfConnectingIp = request.headers.get("cf-connecting-ip"); + const realIp = request.headers.get("x-real-ip"); + const forwarded = request.headers.get("x-forwarded-for"); + return cfConnectingIp ?? realIp ?? forwarded?.split(",")[0] ?? "127.0.0.1"; +} + +function buildUserMessage(answers: AuditAnswers): string { + const lines = QUESTION_IDS.map((qid) => { + const ctx = AUDIT_QUESTION_CONTEXT[qid]; + const selectedOption = answers[qid]; + return `- ${ctx.label}: ${ctx.options[selectedOption]}`; + }); + + return `Here are my business answers:\n\n${lines.join("\n")}\n\nPlease provide personalised automation recommendations.`; +} + +async function callOpenAI(userMessage: string): Promise { + if (!OPENAI_API_KEY) { + throw new Error("OpenAI API key not configured"); + } + + if (!OPENAI_CHAT_MODEL) { + throw new Error("OpenAI chat model not configured"); + } + + const response = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${OPENAI_API_KEY}`, + }, + body: JSON.stringify({ + model: OPENAI_CHAT_MODEL, + messages: [ + { role: "system", content: AUDIT_SYSTEM_PROMPT }, + { role: "user", content: userMessage }, + ], + response_format: { type: "json_object" }, + max_tokens: 1024, + }), + }); + + if (!response.ok) { + const errorData = (await response + .json() + .catch(() => ({}))) as OpenAIChatResponse; + const message = errorData.error?.message ?? "Unknown error"; + + if (response.status === 429) { + const retryAfter = response.headers.get("retry-after") ?? "60"; + throw new Error(`QUOTA_EXCEEDED:${retryAfter}:${message}`); + } + + throw new Error(`OpenAI API error: ${response.status} - ${message}`); + } + + const data = (await response.json()) as OpenAIChatResponse; + const content = data.choices?.[0]?.message?.content; + + if (!content) { + throw new Error("Empty response from OpenAI API"); + } + + const parsed = JSON.parse(content) as AuditResult; + + if ( + !parsed.summary || + !Array.isArray(parsed.topAutomations) || + !parsed.ctaMessage + ) { + throw new Error("Invalid JSON structure from OpenAI API"); + } + + return parsed; +} + +export async function POST(request: NextRequest): Promise { + if (!AUDIT_ENABLED) { + return NextResponse.json( + { error: "Audit tool is temporarily disabled." }, + { status: 503 }, + ); + } + + const clientIP = getClientIP(request); + const { allowed, remaining } = checkAuditRateLimit(clientIP); + + if (!allowed) { + return NextResponse.json( + { + error: + "Rate limit exceeded. You have used all your free audits for today.", + }, + { + status: 429, + headers: { "X-RateLimit-Remaining": "0", "Retry-After": "86400" }, + }, + ); + } + + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json({ error: "Invalid JSON body." }, { status: 400 }); + } + + const parsed = auditRequestSchema.safeParse(body); + if (!parsed.success) { + return NextResponse.json( + { error: "Invalid request data.", details: parsed.error.flatten() }, + { status: 400 }, + ); + } + + const answers = parsed.data.answers as AuditAnswers; + const userMessage = buildUserMessage(answers); + + try { + const result = await callOpenAI(userMessage); + const responseBody: AuditApiResponse = { result, remaining }; + + return NextResponse.json(responseBody, { + headers: { "X-RateLimit-Remaining": String(remaining) }, + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Unknown error"; + + if (message.startsWith("QUOTA_EXCEEDED:")) { + return NextResponse.json( + { error: "OpenAI quota exceeded. Please try again later." }, + { status: 503 }, + ); + } + + console.error("[automation-audit] OpenAI call failed:", message); + return NextResponse.json( + { error: sanitizeErrorMessage(error) }, + { status: 500 }, + ); + } +} diff --git a/app/api/chatbot/route.ts b/app/api/chatbot/route.ts index 8d510ae..3cd40e3 100644 --- a/app/api/chatbot/route.ts +++ b/app/api/chatbot/route.ts @@ -30,8 +30,8 @@ import { import { prisma } from "@/lib/configs/prisma"; // Environment configuration with validation -const GROQ_API_KEY = process.env.GROQ_API_KEY; -const GROQ_MODEL = process.env.GROQ_MODEL || "openai/gpt-oss-120b"; +const OPENAI_API_KEY = process.env.OPENAI_API_KEY; +const OPENAI_CHAT_MODEL = process.env.OPENAI_CHAT_MODEL; const CHATBOT_ENABLED = process.env.CHATBOT_ENABLED === "true"; const chatbotConfig: ChatBotConfig = { @@ -185,62 +185,105 @@ function cleanupRateLimits(): void { } } -// Groq API primary implementation -async function callGroqAPI( +interface OpenAIResponseContent { + type?: string; + text?: string; +} + +interface OpenAIResponseOutput { + type?: string; + role?: string; + content?: OpenAIResponseContent[]; +} + +interface OpenAIResponsePayload { + output_text?: string; + output?: OpenAIResponseOutput[]; + error?: { + message?: string; + }; +} + +function extractOpenAIResponseText(data: OpenAIResponsePayload): string | null { + if (typeof data.output_text === "string" && data.output_text.trim()) { + return data.output_text.trim(); + } + + const outputText = data.output + ?.flatMap((item) => item.content ?? []) + .filter((content) => content.type === "output_text") + .map((content) => content.text) + .filter((text): text is string => typeof text === "string") + .join("\n") + .trim(); + + return outputText || null; +} + +// OpenAI Responses API implementation +async function callOpenAIAPI( messages: ChatMessage[], systemPrompt: string, ): Promise { - if (!GROQ_API_KEY) { - throw new Error("Groq API key not configured"); + if (!OPENAI_API_KEY) { + throw new Error("OpenAI API key not configured"); } - const groqMessages = [ - { role: "system" as const, content: systemPrompt }, - ...messages - .filter((msg) => msg.role !== "system") - .map((msg) => ({ - role: msg.role as "user" | "assistant", - content: msg.content, - })), - ]; - - const response = await fetch( - "https://api.groq.com/openai/v1/chat/completions", - { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${GROQ_API_KEY}`, - }, - body: JSON.stringify({ - model: GROQ_MODEL, - messages: groqMessages, - temperature: 0.7, - max_tokens: 1024, - }), + if (!OPENAI_CHAT_MODEL) { + throw new Error("OpenAI chat model not configured"); + } + + const openAIMessages = messages + .filter((msg) => msg.role !== "system") + .map((msg) => ({ + role: msg.role as "user" | "assistant", + content: msg.content, + })); + + const response = await fetch("https://api.openai.com/v1/responses", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${OPENAI_API_KEY}`, }, - ); + body: JSON.stringify({ + model: OPENAI_CHAT_MODEL, + instructions: systemPrompt, + input: openAIMessages, + max_output_tokens: 1024, + store: false, + text: { + format: { + type: "text", + }, + }, + }), + }); if (!response.ok) { - const errorData = (await response.json().catch(() => ({}))) as { - error?: { message?: string }; - }; + const errorData = (await response + .json() + .catch(() => ({}))) as OpenAIResponsePayload; + const message = errorData.error?.message || "Unknown error"; + + if (response.status === 429) { + const retryAfter = response.headers.get("retry-after") || "60"; + throw new Error(`QUOTA_EXCEEDED:${retryAfter}:${message}`); + } + throw new Error( - `Groq API error: ${response.status} - ${ - errorData.error?.message || "Unknown error" - }`, + `OpenAI API error: ${response.status} - ${message}`, ); } - const data = (await response.json()) as { - choices?: Array<{ message?: { content?: string } }>; - }; + const data = (await response.json()) as OpenAIResponsePayload; + const responseText = extractOpenAIResponseText(data); - if (!data.choices?.[0]?.message?.content) { - throw new Error("Invalid response from Groq API"); + if (!responseText) { + throw new Error("Invalid response from OpenAI API"); } - return data.choices[0].message.content; + return responseText; } /** @@ -435,11 +478,11 @@ export async function POST( // 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.`; + systemPrompt += `\n\nIMPORTANT: This is the user's first message in this conversation. Start warmly and briefly introduce yourself only if it helps. If the user picked a guided starter like website, automation, projects, pricing, or contact, route them directly and include the most relevant source links.`; } - // Call Groq AI - const aiResponse = await callGroqAPI(sessionMessages, systemPrompt); + // Call OpenAI + const aiResponse = await callOpenAIAPI(sessionMessages, systemPrompt); // Create assistant message const assistantMessage: ChatMessage = { diff --git a/app/global-error.tsx b/app/global-error.tsx index 6a5a1c2..64eb52c 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -16,6 +16,49 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { NextIntlClientProvider, useTranslations } from "next-intl"; +import messages from "@/messages/en.json"; + +function ErrorContent({ reset }: { reset: () => void }) { + const t = useTranslations("GlobalError"); + + return ( +
+ + +
+
+ +
+
+
+ {t("title")} + {t("description")} +
+
+ +
+ + +
+
+

{t("persist")}

+
+
+
+
+ ); +} export default function GlobalError({ error, @@ -31,52 +74,9 @@ export default function GlobalError({ return ( -
- - -
-
- -
-
-
- - Something went wrong! - - - We encountered an unexpected error. Please try again or return - to the home page. - -
-
- -
- - -
-
-

- If this problem persists, please contact our support team. -

-
-
-
-
+ + + ); diff --git a/app/layout.tsx b/app/layout.tsx index e689aac..363c803 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -32,6 +32,7 @@ import { Urbanist } from "next/font/google"; import { ThemeConfigInitializer } from "@/components/theme/theme-config-initializer"; import { ViewportRenderer } from "@/components/theme/viewport-renderer"; import Link from "next/link"; +import { MotionProvider } from "@/components/visuals/MotionProvider"; const urbanist = Urbanist({ subsets: ["latin"], @@ -199,18 +200,20 @@ export default async function RootLayout({ disableTransitionOnChange scriptProps={{ type: "application/json" }} > - -
- {children} -
-