Skip to content

Feat: 404 pages#11

Open
ruvasqm wants to merge 7 commits intomainfrom
feat/404-pages
Open

Feat: 404 pages#11
ruvasqm wants to merge 7 commits intomainfrom
feat/404-pages

Conversation

@ruvasqm
Copy link
Copy Markdown
Member

@ruvasqm ruvasqm commented Sep 14, 2025

Description

We are moving to Nextjs and implementing what was discussed with @plaingabriel about including themes for each willing member of the community.

Related Issues

None

Changes

  • Move to nextjs
  • Use experimental view-transition api (WIP)
  • Theme and i18n setup
  • Fetch github projects
  • Cloudflare turnstile for contact form

How to Test

  • Check out this branch.

  • Run npm install && npm run dev

  • Verify that the switchers work and everything looks neat

Checklist

Before you merge, ensure you have completed the following.

[ ] I have read the contribution guidelines and followed them.

[ ] My code follows the project's style guidelines.

[ ] I have performed a self-review of my code.

[ ] I have added comments to my code, particularly in hard-to-understand areas.

[ ] I have added tests that prove my fix is effective or my feature works.

[ ] New and existing unit tests pass locally with my changes.

[ ] I have updated the documentation where necessary.

[ ] I have run a local build and confirmed it runs without errors.

[ ] I have removed any unnecessary console.log() statements.

Summary by CodeRabbit

  • New Features

    • Migrated to Next.js with locale routing (/en, /es) and root redirect.
    • Themed UI with a Theme Switcher (multiple themes) and Language Switcher.
    • Server-rendered landing content with GitHub projects and testimonials.
    • New contact form API with CAPTCHA verification and Discord webhook notifications.
    • Added localized content (EN/ES) and global styles/animations.
  • Refactor

    • Replaced legacy client/server stack and custom UI kit with a theme-driven Next.js App Router architecture.
    • Centralized types and dynamic theme loading.
  • Documentation

    • Added README with Next.js setup and dev instructions.
  • Chores

    • Updated .gitignore, PostCSS/Tailwind setup; removed Dependabot.

@vercel
Copy link
Copy Markdown

vercel bot commented Sep 14, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
web Error Error Sep 14, 2025 4:17pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 14, 2025

Walkthrough

yo frfr this PR is 🔥: removes the old Vite/Express client/server, adds a Next.js App Router app with i18n ([locale]) layouts, theme system, SSR data fetching, a contact POST API with Turnstile + Discord webhook, new theme packages (kayron, nicosup), i18n dictionaries, and updated configs (.gitignore, biome, PostCSS, Next config, package.json).

Changes

Cohort / File(s) Summary
Housekeeping & Config
/.github/dependabot.yml, /.gitignore, /biome.json, /README.md, /components.json, /drizzle.config.ts, /postcss.config.mjs, /next.config.ts, /package.json
Remove Dependabot; overhaul .gitignore; tweak Biome (includes, indentWidth, linter domains); add README; remove shadcn config; drop Drizzle config; add PostCSS for Tailwind; add Next config (viewTransition, redirect / → /es); switch package to Next.js stack.
Legacy Client Removal
/client/index.html, /client/src/**/* (App, pages, components, ui/, hooks, lib/, main.tsx, styles)
Remove entire Vite React app, UI kit (shadcn/radix wrappers), hooks (auth, toast), Firebase stub, query client, utilities, and global CSS.
Legacy Server Removal
/server/*, /shared/schema.ts
Remove Express server, Vite middleware/static serving, storage layer, routes, and shared Drizzle schema.
Next.js App Structure
/src/app/layout.tsx, /src/app/page.tsx, /src/app/globals.css, /src/app/[locale]/layout.tsx, /src/app/[locale]/page.tsx
Add root and locale-aware layouts; minimal pages; global styles with dark mode and view transitions; SSR locale layout with data prep and theming.
API Route
/src/app/api/contact/route.ts
New POST handler: validate/sanitize inputs, Turnstile verify (with dev bypass), optional Discord webhook notify, structured errors.
Theming Core
/src/components/ThemeProvider.tsx, /src/components/ThemeSwitcher.tsx, /src/components/BaseLayout.tsx, /src/components/LanguageSwitcher.tsx
Add theme context/provider with dynamic loaders, theme switcher UI, base page layout and icons, language switcher with path rewrite.
Theme Config & Server Helpers
/src/lib/themeConfig.ts, /src/lib/themeServer.ts, /src/lib/dictionaries.ts, /src/lib/types.ts
Add theme registry and dynamic imports, server-side theme loading and randomizer, file-based dictionaries loader, shared UI/types.
Theme: Kayron
/src/themes/kayron/*
Add Kayron theme components (Layout, Navbar, Hero, About, Projects, Testimonials, Contact, Footer) and theme stylesheet.
Theme: Nicosup
/src/themes/nicosup/*
Add Nicosup theme components (About, Contact) and styles import.
Public Data
/public/data/en.json, /public/data/es.json, /public/data/testimonials.json
Add i18n dictionaries and testimonials dataset for SSR sections.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Page as Next.js [locale]/layout
  participant Dict as getDictionary
  participant GitHub as api.github.com
  participant FS as public/data
  participant Theme as themeServer

  User->>Page: Request /[locale]
  Page->>Dict: load locale JSON
  par concurrent SSR
    Page->>GitHub: fetch repos (optional token)
    Page->>FS: read testimonials.json
    Page->>Theme: load theme components (by cookie or random)
  end
  Page-->>User: SSR HTML with content + initial data
Loading
sequenceDiagram
  autonumber
  actor User
  participant UI as BaseLayout ContactForm
  participant API as POST /api/contact
  participant Turnstile as Cloudflare siteverify
  participant Discord as Discord Webhook

  User->>UI: Submit form (name,email,subject,message,turnstileToken)
  UI->>API: JSON payload
  API->>API: Validate + sanitize + length checks
  alt Dev bypass token
    API-->>Turnstile: Skip verification
  else Verify token
    API->>Turnstile: Verify secret + response
    Turnstile-->>API: success/failure
  end
  alt Discord webhook configured
    API->>Discord: POST embed
    Discord-->>API: 2xx/err
  end
  alt Any failure
    API-->>UI: 4xx/5xx with errors[]
  else Success
    API-->>UI: 200 {"message":"ok"}
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

yo this diff goes zoom, no cap, so clean,
old stacks packed up, Next on the scene.
themes switch drip, locales in glow,
SSR cookin’, projects in flow.
captcha on lock, webhooks in flight—
ship it, fam, this PR’s tight. 🚀🔥

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Feat: 404 pages" references a real part of the changeset (404/not-found handling) but is overly narrow relative to the PR's actual scope. The branch performs a large migration to Next.js, adds theming and i18n, introduces a contact API and many structural/layout changes that the title does not communicate. As written, the title may mislead reviewers or future readers about the primary intent of the PR.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/404-pages

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

await themeComponentLoaders[availableThemes[0].id]();
return defaultThemeModule.default;
}
const module = await loader();

Check failure

Code scanning / CodeQL

Unvalidated dynamic method call High

Invocation of method with
user-controlled
name may dispatch to unexpected target and cause an exception.

Copilot Autofix

AI 7 months ago

To safely invoke a dynamically looked-up method based on user-controlled data, we must:

  • Ensure the property is an own property of the object (not inherited).
  • Validate that the property value is a function before invoking it.

Best-practice fix in this situation:

  • Use Object.prototype.hasOwnProperty.call(themeComponentLoaders, themeId) to confirm the property exists directly on the object.
  • Use typeof loader === 'function' to confirm it's callable.
  • Only invoke if both checks pass; otherwise, fall back to the default theme loader.

Required changes:

  • Edit src/lib/themeServer.ts in function loadThemeComponentsServer, specifically lines 15-27.
  • No changes to imports or external definitions are necessary.
  • The check for "not found" (current lines 16-19) should be replaced with stricter validation (own property and typeof function).

Suggested changeset 1
src/lib/themeServer.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/lib/themeServer.ts b/src/lib/themeServer.ts
--- a/src/lib/themeServer.ts
+++ b/src/lib/themeServer.ts
@@ -13,11 +13,14 @@
   themeId: ThemeName,
 ): Promise<ThemeComponents> {
   const loader = themeComponentLoaders[themeId];
-  if (!loader) {
+  if (
+    !Object.prototype.hasOwnProperty.call(themeComponentLoaders, themeId) ||
+    typeof loader !== "function"
+  ) {
     console.error(
-      `Theme components for "${themeId}" not found. Falling back to default.`,
+      `Theme components for "${themeId}" not found or invalid. Falling back to default.`,
     );
-    // Fallback to a default theme if the requested one is not found
+    // Fallback to a default theme if the requested one is not found or invalid
     const defaultThemeModule =
       await themeComponentLoaders[availableThemes[0].id]();
     return defaultThemeModule.default;
EOF
@@ -13,11 +13,14 @@
themeId: ThemeName,
): Promise<ThemeComponents> {
const loader = themeComponentLoaders[themeId];
if (!loader) {
if (
!Object.prototype.hasOwnProperty.call(themeComponentLoaders, themeId) ||
typeof loader !== "function"
) {
console.error(
`Theme components for "${themeId}" not found. Falling back to default.`,
`Theme components for "${themeId}" not found or invalid. Falling back to default.`,
);
// Fallback to a default theme if the requested one is not found
// Fallback to a default theme if the requested one is not found or invalid
const defaultThemeModule =
await themeComponentLoaders[availableThemes[0].id]();
return defaultThemeModule.default;
Copilot is powered by AI and may make mistakes. Always verify output.
@ruvasqm ruvasqm committed this autofix suggestion 7 months ago.
…od call

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

♻️ Duplicate comments (1)
src/lib/themeServer.ts (1)

15-30: Harden dynamic loader + catch import fails

You validate the key (nice), but a failed import still 500s. Add whitelist check and try/catch to fall back cleanly. This also calms CodeQL’s “unvalidated dynamic method call”.

Apply this diff:

 export async function loadThemeComponentsServer(
   themeId: ThemeName,
 ): Promise<ThemeComponents> {
-  const loader = themeComponentLoaders[themeId];
-  if (
-    !Object.prototype.hasOwnProperty.call(themeComponentLoaders, themeId) ||
-    typeof loader !== "function"
-  ) {
+  const allowed = new Set(availableThemes.map((t) => t.id as ThemeName));
+  const loader = themeComponentLoaders[themeId];
+  if (!allowed.has(themeId) || typeof loader !== "function") {
     console.error(
       `Theme components for "${themeId}" not found or invalid. Falling back to default.`,
     );
-    // Fallback to a default theme if the requested one is not found or invalid
-    const defaultThemeModule =
-      await themeComponentLoaders[availableThemes[0].id]();
-    return defaultThemeModule.default;
+    const fallback = await themeComponentLoaders[availableThemes[0].id]();
+    return fallback.default;
   }
-  const module = await loader();
-  return module.default;
+  try {
+    const module = await loader();
+    return module.default;
+  } catch (err) {
+    console.error(`Failed to load theme "${themeId}":`, err);
+    const fallback = await themeComponentLoaders[availableThemes[0].id]();
+    return fallback.default;
+  }
 }
🧹 Nitpick comments (53)
next.config.ts (1)

7-15: SEO/UX sanity on root redirect — permanent 301 is a strong take.

301 from "/" → "/es" is okay if Spanish is the hard default, but search/analytics might prefer i18n alternates/canonicals instead of a hard 301. Consider revisiting once i18n is fully wired.

.gitignore (1)

33-35: Save .env.example from the shadow realm.

Right now .env* will also ignore your sample env. Keep the template tracked.

 # env files (can opt-in for committing if needed)
-.env*
+.env*
+!.env.example
src/components/LanguageSwitcher.tsx (5)

10-28: Type the icon props — tiny papercut.

TSX file but GlobeIcon props are implicit any. Give it a quick type.

-const GlobeIcon = ({ className = "" }) => (
+const GlobeIcon = ({ className = "" }: { className?: string }) => (

36-39: Locales hardcoded — keep it DRY with a central type/source.

If you already have a Locale union or config, import it so this doesn’t drift.

-interface LanguageSwitcherProps {
-  currentLocale: string;
-}
+type Locale = "en" | "es"; // or import from src/lib/types
+interface LanguageSwitcherProps {
+  currentLocale: Locale;
+}
-const locales = [
+const locales: { code: Locale; name: string }[] = [
   { code: "en", name: "EN" },
   { code: "es", name: "ES" },
 ];

41-45: Path rewrite via substring(3) is brittle — make it segment-aware.

Assumes 2‑char locale forever; any tweak = instant bozo behavior. Use segments.

-  const handleLocaleChange = (newLocale: string) => {
-    const newPathname = `/${newLocale}${pathname.substring(3)}`;
-    router.push(newPathname);
-    setIsOpen(false); // Close dropdown after selection
-  };
+  const handleLocaleChange = (newLocale: Locale) => {
+    const parts = pathname.split("/");
+    // Ensure leading slash => ["", "en", ...]
+    if (parts.length > 1 && parts[1].length > 0) {
+      parts[1] = newLocale;
+    } else {
+      parts[1] = newLocale;
+    }
+    const newPathname = parts.join("/") || `/${newLocale}`;
+    router.push(newPathname);
+    setIsOpen(false);
+  };

57-71: Outside-click handler: mousedown won’t fire on touch — go pointerdown.

Mobile users deserve love too, no cap.

-    document.addEventListener("mousedown", handleClickOutside);
+    document.addEventListener("pointerdown", handleClickOutside as EventListener);
     return () => {
-      document.removeEventListener("mousedown", handleClickOutside);
+      document.removeEventListener("pointerdown", handleClickOutside as EventListener);
     };

88-112: ARIA polish — reflect selection state.

Since exactly one locale is active, menuitemradio + aria-checked slaps.

-          <div className="py-1" role="none">
+          <div className="py-1" role="menu">
             {locales.map((locale) => (
               <button
                 key={locale.code}
                 onClick={() => handleLocaleChange(locale.code)}
                 className={`block w-full text-left px-4 py-2 text-sm ${
                   currentLocale === locale.code
                     ? "bg-primary text-primary-foreground"
                     : "text-text-base hover:bg-muted"
                 }`}
-                role="menuitem"
+                role="menuitemradio"
+                aria-checked={currentLocale === locale.code}
               >
                 {locale.name}
               </button>
             ))}
           </div>
src/app/[locale]/page.tsx (1)

6-10: Add metadata/alternates for i18n SEO — tiny upgrade, big vibes.

Since layout renders UI, this page can still export metadata for language alternates.

-export default async function Page() {
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+  alternates: {
+    languages: {
+      en: "/en",
+      es: "/es",
+    },
+  },
+};
+
+export default async function Page() {
   // You might fetch data specific to *this* page if it were not a pure landing page,
   // but for a landing page where BaseLayout renders everything, this can be minimal.
   return null; // Or <></>
 }
README.md (2)

19-21: Docs path is capping — point to the i18n page file, not app/page.tsx

Project uses src/app/[locale]/… with a redirect to /es. Update the edit hint accordingly.

Apply:

-You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+You can start editing the localized page by modifying `src/app/[locale]/page.tsx` (e.g., `src/app/es/page.tsx`). The page auto‑updates as you edit the file. Note: "/" redirects permanently to "/es".

32-36: Missing env setup section (Turnstile + Discord webhook) — add a quickstart

Dev onboarding vibes will be smoother with explicit env vars.

Apply:

 ## Deploy on Vercel
@@
 Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
+
+## Environment Variables
+
+Create `.env.local`:
+
+```
+TURNSTILE_SITE_KEY=...
+TURNSTILE_SECRET_KEY=...
+DISCORD_WEBHOOK_URL=...
+```
+
+Then:
+
+```bash
+npm i
+npm run dev
+```
src/themes/kayron/Layout.tsx (1)

5-7: If no hooks/state, drop 'use client' for free SSR perf

Make it a server component unless you truly need client APIs.

Apply:

-"use client";
+// server component: no client-only features needed
src/themes/kayron/Hero.tsx (2)

1-2: Duplicate "use client" — extra line is pure cope

Drop one.

Apply:

-"use client";
 "use client";

41-48: External CTA UX nit — open in new tab safely (if external)

If ctaLink points off‑site (Discord), consider target/rel and no prefetch.

Apply:

-            <Link
-              href={ctaLink}
+            <Link
+              href={ctaLink}
+              prefetch={false}
+              target={/^https?:\/\//.test(ctaLink) ? "_blank" : undefined}
+              rel={/^https?:\/\//.test(ctaLink) ? "noopener noreferrer" : undefined}
src/app/globals.css (2)

23-27: Font token not used — swap to var(--font-sans)

Match the font variables you defined.

Apply:

 body {
   background: var(--background);
   color: var(--foreground);
-  font-family: Arial, Helvetica, sans-serif;
+  font-family: var(--font-sans, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji");
 }

52-58: Optional: guard view‑transition rules with @supports

Prevents noisy CSS warnings on browsers that don’t vibe with the pseudo‑elements.

Apply:

-::view-transition-old(root) {
-  animation: 0.3s ease-out forwards slide-out-fade;
-}
-
-::view-transition-new(root) {
-  animation: 0.3s ease-out forwards slide-in-fade;
-}
+@supports (view-transition-name: none) {
+  ::view-transition-old(root) { animation: 0.3s ease-out forwards slide-out-fade; }
+  ::view-transition-new(root) { animation: 0.3s ease-out forwards slide-in-fade; }
+}
src/themes/nicosup/AboutSection.tsx (2)

25-25: Tailwind class cap — hover:-translate-y-1/8 prob not real

Looks like a no-op unless you’ve custom plugin. Swap to a valid utility.

Apply this diff:

-            className=" border border-foreground rounded-xl p-8 text-center hover:shadow-xl hover:shadow-emerald-300/50 hover:-translate-y-1/8 transition-shadow transition-transform duration-300" // Simpler hover effect
+            className="border border-foreground rounded-xl p-8 text-center hover:shadow-xl hover:shadow-emerald-300/50 hover:-translate-y-1 transform transition-shadow transition-transform duration-300" // Simpler hover effect

24-24: React keys need smoke — titles can dup

If two features share the same title, React re-use goes goofy. Use a stable id or title+index.

Apply this diff if no id exists:

-            key={feature.title}
+            key={`${feature.title}-${index}`}
src/themes/kayron/AboutSection.tsx (1)

23-23: Same key sauce here

Title-only keys can collide. Prefer a stable id or suffix with index.

-            key={feature.title}
+            key={`${feature.title}-${index}`}
src/themes/kayron/Footer.tsx (3)

20-25: Decorative logo should be silent

Mark wrapper as aria-hidden to avoid double announcing brand.

-          <div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-blue-600 rounded-xl flex items-center justify-center">
+          <div className="w-10 h-10 bg-gradient-to-br from-purple-500 to-blue-600 rounded-xl flex items-center justify-center" aria-hidden="true">

29-37: External socials: open in new tab like the navbar

Navbar opens GH/Discord in new tab; mirror that here + noopener.

-            <Link
+            <Link
               key={link.label}
               href={link.href}
-              className="text-gray-400 hover:text-purple-400 transition-colors"
+              className="text-gray-400 hover:text-purple-400 transition-colors"
+              target="_blank"
+              rel="noopener noreferrer"
               aria-label={`Síguenos en ${link.label}`}
             >

31-31: Key by label can clash

If two links share a label (e.g., “Website”), collision. Consider key={link.href}.

-              key={link.label}
+              key={link.href}
src/components/ThemeSwitcher.tsx (3)

70-76: A11y W — wire up aria-controls

Tie button to menu for assistive tech.

-      <button
+      <button
         type="button"
         className="inline-flex justify-center items-center rounded-md text-text-base hover:bg-background-card focus:outline-none focus:ring-2 focus:ring-primary focus:border-primary transition-colors"
         id="theme-menu-button"
         aria-expanded={isOpen}
+        aria-controls="theme-menu"
         aria-haspopup="true"
         onClick={handleToggle}
         onKeyDown={handleKeyDown}
       >

84-90: Give the menu an id and we gucci

Minor a11y polish.

-        <div
+        <div
+          id="theme-menu"
           className="origin-top-right absolute right-0 mt-2 w-36 rounded-md shadow-lg bg-background-card ring-1 ring-border ring-opacity-50 focus:outline-none z-50"
           role="menu"
           aria-orientation="vertical"
           aria-labelledby="theme-menu-button"
         >

83-108: Keyboard UX could hit harder

Arrow key navigation/roving tabindex would slap; consider Headless UI or radix for menu semantics. Non-blocker.

src/themes/kayron/Navbar.tsx (3)

86-90: Don’t toggle on desktop link clicks — just close

Clicking a nav link flips state; on desktop that can briefly switch to the mobile overlay. Close instead of toggle.

-                onClick={toggleMenu} // Close menu on link click
+                onClick={() => setIsMenuOpen(false)} // Close menu on link click

67-74: Hook up aria-controls to the menu container

Small a11y win; pair the button with the collapsible panel id.

-            <button
+            <button
               type="button"
               className="p-2 rounded-lg glass-card"
               aria-label="Abrir menú de navegación"
               aria-expanded={isMenuOpen}
+              aria-controls="primary-navigation"
               data-testid="button-mobile-menu"
               onClick={toggleMenu}
             >

79-80: Give the collapsible a stable id

So the button can target it.

-          <div
+          <div
+            id="primary-navigation"
             className={`md:flex items-center space-x-8 ${isMenuOpen ? "flex flex-col absolute top-full left-0 w-full bg-black/90 py-4 space-x-0 space-y-4" : "hidden"}`}
           >
src/themes/kayron/ProjectsSection.tsx (2)

33-34: Key uniqueness again

Title-only keys might collide. If you’ve got an id/slug, use it; else combine with link or index.

-              key={project.title}
+              key={`${project.title}-${project.projectLink}`}

57-63: External project links — open new tab?

If these go off-site, mirror navbar behavior with target+rel. If internal, ignore.

-              <Link
+              <Link
                 href={project.projectLink}
-                className="inline-flex items-center text-purple-400 hover:text-purple-300 font-semibold transition-colors group-hover:translate-x-2 transform duration-300"
+                className="inline-flex items-center text-purple-400 hover:text-purple-300 font-semibold transition-colors group-hover:translate-x-2 transform duration-300"
+                target="_blank"
+                rel="noopener noreferrer"
               >
src/themes/kayron/TestimonialSection.tsx (1)

19-21: Clamp rating + add a11y text (prevents RangeError, improves SR experience) — yo frfr

Untrusted data could send negative/huge ratings; Array.from with negative length blows up. Also add SR text for rating.

-  const starIcons = Array.from({ length: rating }, (_, i) => (
+  const safeRating = Math.max(0, Math.min(5, Math.floor(rating ?? 0)));
+  const starIcons = Array.from({ length: safeRating }, (_, i) => (
     <div key={i} className="w-4 h-4 bg-yellow-400 rounded"></div>
   ));
-          <div className="flex items-center mt-1">
-            <div className="flex space-x-0.5">{starIcons}</div>
-          </div>
+          <div className="flex items-center mt-1">
+            <div className="flex space-x-0.5">{starIcons}</div>
+            <span className="sr-only">Rating: {safeRating} out of 5</span>
+          </div>

Also applies to: 39-40

src/themes/nicosup/ContactForm.tsx (1)

5-7: Type-only React import needed (you reference React.ChangeEvent) — tiny but important

Prevents TS “Cannot find namespace 'React'” in some setups.

 import "./style.css";
 import type { FC } from "react";
+import type React from "react";
 import { SendIcon } from "@/components/BaseLayout"; // Ensure SendIcon is imported
src/themes/kayron/ContactForm.tsx (4)

1-3: Duplicate "use client" directive — drop one, keep vibes clean frfr

Two identical directives; one is enough.

Apply:

-"use client";
-// theme-modules/kayron/ContactForm.tsx
-"use client";
+"use client";
+// theme-modules/kayron/ContactForm.tsx

18-22: Use functional state update to avoid stale writes (small but W)

Prevents race conditions on rapid input.

-    setFormData({ ...formData, [e.target.name]: e.target.value });
+    setFormData((prev) => ({ ...prev, [e.target.name]: e.target.value }));

56-67: Dead branch: subject handling in name/email grid is mid

This grid only renders name/email; the subject type check here never hits.

-                    type={field.type === "subject" ? "text" : field.type} // Handle 'subject' type for input
+                    type={field.type}

56-67: Bonus UX: add autocomplete tokens for name/email/subject/message

Makes the form feel snappier, no cap.

Example pattern (apply similarly to other inputs):

-                    placeholder={field.placeholder}
+                    placeholder={field.placeholder}
+                    autoComplete={
+                      field.name === "email"
+                        ? "email"
+                        : field.name === "name"
+                        ? "name"
+                        : field.name === "subject"
+                        ? "subject"
+                        : "off"
+                    }

Also applies to: 80-90, 103-114

src/components/ThemeProvider.tsx (3)

44-71: Add deps to useCallback, keep it stable fr

Include startTransitionThemeLoad to satisfy hooks lint.

-  const loadAndSetTheme = useCallback(
+  const loadAndSetTheme = useCallback(
     (themeId: ThemeName) => {
       startTransitionThemeLoad(async () => {
         // ...
       });
     },
-    [], // initialThemeComponents is not needed here
+    [startTransitionThemeLoad],
   );

82-89: Early return when theme unchanged (tiny render win)

Skip setState if same theme.

-  const setTheme = (theme: ThemeName) => {
-    if (availableThemes.some((t) => t.id === theme)) {
-      setCurrentTheme(theme);
-      if (theme !== currentTheme) {
-        loadAndSetTheme(theme);
-      }
-    }
-  };
+  const setTheme = (theme: ThemeName) => {
+    if (!availableThemes.some((t) => t.id === theme)) return;
+    if (theme === currentTheme) return;
+    setCurrentTheme(theme);
+    loadAndSetTheme(theme);
+  };

75-79: Cookie hardening (optional): add Secure in prod

Safer cookie when served over HTTPS.

-    document.cookie = `app-theme=${currentTheme};path=/;max-age=31536000;SameSite=Lax`;
+    document.cookie = `app-theme=${currentTheme};path=/;max-age=31536000;SameSite=Lax;${location.protocol === "https:" ? "Secure" : ""}`;
src/themes/kayron/style.css (2)

378-385: Keyframes glow uses brightness() with no value

Needs a factor like brightness(1); empty calls get yeeted.

-    filter: brightness();
+    filter: brightness(1);

39-85: Nit: giant token block is legit, but consider grouping/comments

Long var slabs are a lot to scroll. Group by semantic (bg, text, border, charts) and keep a short index comment.

src/app/[locale]/layout.tsx (2)

165-170: cookies() isn’t async — drop the await

Tiny cleanup for SSR cookie read.

-  const themeCookie = (await cookies()).get("app-theme")?.value;
+  const themeCookie = cookies().get("app-theme")?.value;

25-36: Unused icon imports — trim for signal, not noise

SendIcon, ExternalLinkIcon, TwitterIcon, LinkedInIcon aren’t used here.

-  SendIcon,
-  ExternalLinkIcon,
-  TwitterIcon,
   GitHubIcon,
-  LinkedInIcon,
   DiscordIcon,
src/components/BaseLayout.tsx (2)

4-8: Drop unused useMemo import (tiny tidy, still a W)

Keep imports lean.

-import {
-  useCallback,
-  useMemo,
-  unstable_ViewTransition as ViewTransition,
-} from "react";
+import { useCallback, unstable_ViewTransition as ViewTransition } from "react";

46-60: SVG <text> uses invalid user-select attribute

React will pass it through but it does nothing; use style prop.

-      <text
+      <text
         x="210"
         y="104"
         fontFamily="'Segoe UI', Arial, sans-serif"
         fontSize="192px"
         fontWeight="900"
         fill="#FFFFFF"
         textAnchor="middle"
         dominantBaseline="middle"
-        user-select="none"
+        style={{ userSelect: "none" }}
         id="text302"
       >
src/lib/types.ts (9)

2-2: Unused import, yeet it frfr.

FC isn’t used.

-import type { FC, ReactNode } from "react";
+import type { ReactNode } from "react";

114-114: Use ReactNode consistently, no cap.

Avoid React.ReactNode when ReactNode is already imported.

-  children: React.ReactNode;
+  children: ReactNode;

58-64: Nulls are mid; prefer optionals.

Cleaner ergonomics with ? over | null for presentational bits.

-  icon: ReactNode | null; // Can be null now, assigned default in BaseLayout
+  icon?: ReactNode; // Default assigned in BaseLayout

-  demoLink: string | null;
+  demoLink?: string;

37-43: CTA icon shouldn’t be hard‑required.

Make ctaIcon optional so themes without an icon don’t have to pass null.

-  ctaIcon: ReactNode;
+  ctaIcon?: ReactNode;

66-71: Duped error sources — pick one truthy vibe.

You’ve got initial*Error on layout and error? on section props. That’s two channels for the same state → drift risk.

Option: keep errors only at the layout level and derive section error props there, or drop layout errors and let sections own them. Either way, one source of truth.

Also applies to: 90-93, 117-121


24-35: Lock props immutability with ReadonlyArray where it slaps.

These arrays are props, not state. Make them readonly to prevent accidental mutation.

Examples (apply similarly elsewhere):

-  navLinks: NavLink[];
+  navLinks: ReadonlyArray<NavLink>;

-  features: FeatureCard[];
+  features: ReadonlyArray<FeatureCard>;

-  projects: ProjectCard[];
+  projects: ReadonlyArray<ProjectCard>;

-  testimonials: TestimonialCardProps[];
+  testimonials: ReadonlyArray<TestimonialCardProps>;

-  socialLinks: SocialLink[];
+  socialLinks: ReadonlyArray<SocialLink>;

-  availableThemes: { id: ThemeName; name: string }[];
+  availableThemes: ReadonlyArray<{ id: ThemeName; name: string }>;

Also applies to: 51-55, 66-70, 81-88, 105-111, 145-146


116-116: Locale union — make it source‑of‑truth based, ong.

Hardcoding "en" | "es" can drift. Consider deriving from a const LOCALES = [...] as const.

export const LOCALES = ["en", "es"] as const;
export type Locale = typeof LOCALES[number];

Then use locale: Locale;.


24-35: Separate data vs slots — don't pass ReactNode from dictionary/server props.

src/lib/types.ts currently types fields as ReactNode (logoSvg/icon/ctaIcon) and app/[locale]/layout.tsx assigns JSX to FOOTER_CONTENT.logoSvg which is then passed into client theme components (themes//Navbar.tsx, themes//Footer.tsx); this risks server→client serialization / i18n breakage.

  • Action: keep dictionary-backed values as plain strings/URLs, add explicit slot props for JSX (e.g. logoSlot?: ReactNode) and update types + layout accordingly.

yo frfr this pr is 🔥


10-16: Avoid using "subject" as ContactFormField.type — use 'text'|'email'|'textarea' or add a separate variant. yo frfr this pr is 🔥
Found: src/app/[locale]/layout.tsx:260 sets type: "subject" — HTML has no input[type="subject"]; keep the type union for real HTML input kinds and express intent via name === "subject" or a dedicated variant field.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5dca70e and 7a2f204.

⛔ Files ignored due to path filters (7)
  • package-lock.json is excluded by !**/package-lock.json
  • public/file.svg is excluded by !**/*.svg
  • public/globe.svg is excluded by !**/*.svg
  • public/next.svg is excluded by !**/*.svg
  • public/vercel.svg is excluded by !**/*.svg
  • public/window.svg is excluded by !**/*.svg
  • src/app/favicon.ico is excluded by !**/*.ico
📒 Files selected for processing (107)
  • .github/dependabot.yml (0 hunks)
  • .gitignore (1 hunks)
  • README.md (1 hunks)
  • biome.json (1 hunks)
  • client/index.html (0 hunks)
  • client/src/App.tsx (0 hunks)
  • client/src/components/CommunitySection.tsx (0 hunks)
  • client/src/components/CorporateSection.tsx (0 hunks)
  • client/src/components/Footer.tsx (0 hunks)
  • client/src/components/LoginModal.tsx (0 hunks)
  • client/src/components/Navigation.tsx (0 hunks)
  • client/src/components/ui/accordion.tsx (0 hunks)
  • client/src/components/ui/alert-dialog.tsx (0 hunks)
  • client/src/components/ui/alert.tsx (0 hunks)
  • client/src/components/ui/aspect-ratio.tsx (0 hunks)
  • client/src/components/ui/avatar.tsx (0 hunks)
  • client/src/components/ui/badge.tsx (0 hunks)
  • client/src/components/ui/breadcrumb.tsx (0 hunks)
  • client/src/components/ui/button.tsx (0 hunks)
  • client/src/components/ui/calendar.tsx (0 hunks)
  • client/src/components/ui/card.tsx (0 hunks)
  • client/src/components/ui/carousel.tsx (0 hunks)
  • client/src/components/ui/chart.tsx (0 hunks)
  • client/src/components/ui/checkbox.tsx (0 hunks)
  • client/src/components/ui/collapsible.tsx (0 hunks)
  • client/src/components/ui/command.tsx (0 hunks)
  • client/src/components/ui/context-menu.tsx (0 hunks)
  • client/src/components/ui/dialog.tsx (0 hunks)
  • client/src/components/ui/drawer.tsx (0 hunks)
  • client/src/components/ui/dropdown-menu.tsx (0 hunks)
  • client/src/components/ui/form.tsx (0 hunks)
  • client/src/components/ui/hover-card.tsx (0 hunks)
  • client/src/components/ui/input-otp.tsx (0 hunks)
  • client/src/components/ui/input.tsx (0 hunks)
  • client/src/components/ui/label.tsx (0 hunks)
  • client/src/components/ui/menubar.tsx (0 hunks)
  • client/src/components/ui/navigation-menu.tsx (0 hunks)
  • client/src/components/ui/pagination.tsx (0 hunks)
  • client/src/components/ui/popover.tsx (0 hunks)
  • client/src/components/ui/progress.tsx (0 hunks)
  • client/src/components/ui/radio-group.tsx (0 hunks)
  • client/src/components/ui/resizable.tsx (0 hunks)
  • client/src/components/ui/scroll-area.tsx (0 hunks)
  • client/src/components/ui/select.tsx (0 hunks)
  • client/src/components/ui/separator.tsx (0 hunks)
  • client/src/components/ui/sheet.tsx (0 hunks)
  • client/src/components/ui/sidebar.tsx (0 hunks)
  • client/src/components/ui/skeleton.tsx (0 hunks)
  • client/src/components/ui/slider.tsx (0 hunks)
  • client/src/components/ui/switch.tsx (0 hunks)
  • client/src/components/ui/table.tsx (0 hunks)
  • client/src/components/ui/tabs.tsx (0 hunks)
  • client/src/components/ui/textarea.tsx (0 hunks)
  • client/src/components/ui/toast.tsx (0 hunks)
  • client/src/components/ui/toaster.tsx (0 hunks)
  • client/src/components/ui/toggle-group.tsx (0 hunks)
  • client/src/components/ui/toggle.tsx (0 hunks)
  • client/src/components/ui/tooltip.tsx (0 hunks)
  • client/src/hooks/use-mobile.tsx (0 hunks)
  • client/src/hooks/use-toast.ts (0 hunks)
  • client/src/hooks/useFirebaseAuth.ts (0 hunks)
  • client/src/index.css (0 hunks)
  • client/src/lib/firebase.ts (0 hunks)
  • client/src/lib/queryClient.ts (0 hunks)
  • client/src/lib/utils.ts (0 hunks)
  • client/src/main.tsx (0 hunks)
  • client/src/pages/Home.tsx (0 hunks)
  • client/src/pages/not-found.tsx (0 hunks)
  • components.json (0 hunks)
  • drizzle.config.ts (0 hunks)
  • next.config.ts (1 hunks)
  • package.json (1 hunks)
  • postcss.config.mjs (1 hunks)
  • public/data/en.json (1 hunks)
  • public/data/es.json (1 hunks)
  • public/data/testimonials.json (1 hunks)
  • server/index.ts (0 hunks)
  • server/routes.ts (0 hunks)
  • server/storage.ts (0 hunks)
  • server/vite.ts (0 hunks)
  • shared/schema.ts (0 hunks)
  • src/app/[locale]/layout.tsx (1 hunks)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/api/contact/route.ts (1 hunks)
  • src/app/globals.css (1 hunks)
  • src/app/layout.tsx (1 hunks)
  • src/app/page.tsx (1 hunks)
  • src/components/BaseLayout.tsx (1 hunks)
  • src/components/LanguageSwitcher.tsx (1 hunks)
  • src/components/ThemeProvider.tsx (1 hunks)
  • src/components/ThemeSwitcher.tsx (1 hunks)
  • src/lib/dictionaries.ts (1 hunks)
  • src/lib/themeConfig.ts (1 hunks)
  • src/lib/themeServer.ts (1 hunks)
  • src/lib/types.ts (1 hunks)
  • src/themes/kayron/AboutSection.tsx (1 hunks)
  • src/themes/kayron/ContactForm.tsx (1 hunks)
  • src/themes/kayron/Footer.tsx (1 hunks)
  • src/themes/kayron/Hero.tsx (1 hunks)
  • src/themes/kayron/Layout.tsx (1 hunks)
  • src/themes/kayron/Navbar.tsx (1 hunks)
  • src/themes/kayron/ProjectsSection.tsx (1 hunks)
  • src/themes/kayron/TestimonialSection.tsx (1 hunks)
  • src/themes/kayron/index.ts (1 hunks)
  • src/themes/kayron/style.css (1 hunks)
  • src/themes/nicosup/AboutSection.tsx (1 hunks)
  • src/themes/nicosup/ContactForm.tsx (1 hunks)
⛔ Files not processed due to max files limit (20)
  • src/themes/nicosup/Footer.tsx
  • src/themes/nicosup/Hero.tsx
  • src/themes/nicosup/Layout.tsx
  • src/themes/nicosup/Navbar.tsx
  • src/themes/nicosup/ProjectsSection.tsx
  • src/themes/nicosup/TestimonialSection.tsx
  • src/themes/nicosup/index.ts
  • src/themes/nicosup/style.css
  • src/themes/pix/AboutSection.tsx
  • src/themes/pix/ContactForm.tsx
  • src/themes/pix/Footer.tsx
  • src/themes/pix/Hero.tsx
  • src/themes/pix/Layout.tsx
  • src/themes/pix/Navbar.tsx
  • src/themes/pix/ProjectsSection.tsx
  • src/themes/pix/TestimonialSection.tsx
  • src/themes/pix/index.ts
  • src/themes/pix/style.css
  • tsconfig.json
  • vite.config.ts
💤 Files with no reviewable changes (72)
  • server/routes.ts
  • client/index.html
  • client/src/components/ui/textarea.tsx
  • client/src/components/ui/switch.tsx
  • client/src/main.tsx
  • client/src/components/CorporateSection.tsx
  • client/src/components/ui/avatar.tsx
  • client/src/components/ui/skeleton.tsx
  • client/src/components/ui/label.tsx
  • client/src/lib/utils.ts
  • .github/dependabot.yml
  • client/src/components/ui/toaster.tsx
  • client/src/pages/not-found.tsx
  • components.json
  • server/index.ts
  • client/src/components/ui/toggle-group.tsx
  • client/src/components/ui/tooltip.tsx
  • client/src/components/Navigation.tsx
  • client/src/components/ui/input.tsx
  • client/src/components/CommunitySection.tsx
  • client/src/components/ui/calendar.tsx
  • client/src/components/ui/radio-group.tsx
  • server/vite.ts
  • client/src/lib/firebase.ts
  • client/src/App.tsx
  • shared/schema.ts
  • client/src/lib/queryClient.ts
  • client/src/components/ui/aspect-ratio.tsx
  • client/src/components/ui/card.tsx
  • client/src/components/ui/badge.tsx
  • client/src/components/ui/hover-card.tsx
  • server/storage.ts
  • client/src/components/ui/toast.tsx
  • client/src/components/ui/slider.tsx
  • client/src/components/ui/accordion.tsx
  • client/src/components/ui/breadcrumb.tsx
  • client/src/components/ui/dialog.tsx
  • client/src/hooks/useFirebaseAuth.ts
  • client/src/components/ui/checkbox.tsx
  • client/src/components/Footer.tsx
  • client/src/components/ui/command.tsx
  • client/src/pages/Home.tsx
  • client/src/hooks/use-mobile.tsx
  • client/src/components/ui/input-otp.tsx
  • client/src/components/ui/alert.tsx
  • client/src/components/ui/table.tsx
  • client/src/components/ui/chart.tsx
  • client/src/components/ui/alert-dialog.tsx
  • client/src/components/ui/carousel.tsx
  • client/src/components/ui/scroll-area.tsx
  • drizzle.config.ts
  • client/src/components/ui/sheet.tsx
  • client/src/components/ui/form.tsx
  • client/src/components/ui/toggle.tsx
  • client/src/components/ui/tabs.tsx
  • client/src/components/LoginModal.tsx
  • client/src/components/ui/progress.tsx
  • client/src/components/ui/resizable.tsx
  • client/src/components/ui/separator.tsx
  • client/src/components/ui/button.tsx
  • client/src/components/ui/collapsible.tsx
  • client/src/components/ui/popover.tsx
  • client/src/components/ui/context-menu.tsx
  • client/src/components/ui/drawer.tsx
  • client/src/components/ui/select.tsx
  • client/src/components/ui/dropdown-menu.tsx
  • client/src/components/ui/menubar.tsx
  • client/src/hooks/use-toast.ts
  • client/src/components/ui/navigation-menu.tsx
  • client/src/index.css
  • client/src/components/ui/sidebar.tsx
  • client/src/components/ui/pagination.tsx
🧰 Additional context used
🧬 Code graph analysis (17)
src/themes/nicosup/AboutSection.tsx (1)
src/lib/types.ts (1)
  • AboutSectionProps (51-55)
src/themes/kayron/AboutSection.tsx (1)
src/lib/types.ts (1)
  • AboutSectionProps (51-55)
src/themes/kayron/Footer.tsx (1)
src/lib/types.ts (1)
  • FooterProps (105-111)
src/themes/kayron/Hero.tsx (1)
src/lib/types.ts (1)
  • HeroProps (37-43)
src/themes/kayron/index.ts (1)
src/lib/types.ts (1)
  • ThemeComponents (130-139)
src/themes/kayron/Navbar.tsx (2)
src/lib/types.ts (1)
  • NavbarProps (24-35)
src/components/BaseLayout.tsx (2)
  • GitHubIcon (218-223)
  • DiscordIcon (232-237)
src/lib/themeServer.ts (2)
src/lib/types.ts (2)
  • ThemeName (150-150)
  • ThemeComponents (130-139)
src/lib/themeConfig.ts (2)
  • availableThemes (5-9)
  • themeComponentLoaders (11-18)
src/components/ThemeSwitcher.tsx (2)
src/components/ThemeProvider.tsx (1)
  • useTheme (106-112)
src/lib/themeConfig.ts (1)
  • availableThemes (5-9)
src/themes/nicosup/ContactForm.tsx (2)
src/lib/types.ts (1)
  • ContactFormProps (95-103)
src/components/BaseLayout.tsx (1)
  • SendIcon (168-185)
src/components/ThemeProvider.tsx (2)
src/lib/types.ts (3)
  • ThemeContextType (142-148)
  • ThemeName (150-150)
  • ThemeComponents (130-139)
src/lib/themeConfig.ts (2)
  • themeComponentLoaders (11-18)
  • availableThemes (5-9)
src/lib/themeConfig.ts (1)
src/lib/types.ts (2)
  • ThemeComponents (130-139)
  • ThemeName (150-150)
src/components/BaseLayout.tsx (4)
src/lib/types.ts (1)
  • BaseLayoutProps (113-128)
src/components/ThemeProvider.tsx (1)
  • useTheme (106-112)
src/components/ThemeSwitcher.tsx (1)
  • ThemeSwitcher (32-111)
src/components/LanguageSwitcher.tsx (1)
  • LanguageSwitcher (30-115)
src/themes/kayron/ProjectsSection.tsx (2)
src/lib/types.ts (1)
  • ProjectsSectionProps (66-71)
src/components/BaseLayout.tsx (1)
  • ExternalLinkIcon (187-209)
src/themes/kayron/ContactForm.tsx (1)
src/lib/types.ts (1)
  • ContactFormProps (95-103)
src/lib/types.ts (1)
src/lib/dictionaries.ts (1)
  • Dictionary (25-25)
src/app/[locale]/layout.tsx (6)
src/lib/dictionaries.ts (1)
  • getDictionary (17-23)
src/lib/types.ts (11)
  • ProjectCard (57-64)
  • repo (73-79)
  • TestimonialCardProps (81-88)
  • ThemeName (150-150)
  • NavLink (5-8)
  • HeroProps (37-43)
  • AboutSectionProps (51-55)
  • ProjectsSectionProps (66-71)
  • TestimonialSectionProps (90-93)
  • ContactFormProps (95-103)
  • FooterProps (105-111)
src/lib/themeConfig.ts (1)
  • availableThemes (5-9)
src/lib/themeServer.ts (2)
  • getRandomThemeId (6-9)
  • loadThemeComponentsServer (12-30)
src/components/BaseLayout.tsx (8)
  • MessageCircleIcon (63-83)
  • CircleCheckBigIcon (105-126)
  • UsersIcon (128-147)
  • BookOpenIcon (149-166)
  • CodeIcon (15-62)
  • GitHubIcon (218-223)
  • DiscordIcon (232-237)
  • BaseLayout (240-339)
src/components/ThemeProvider.tsx (1)
  • ThemeProvider (33-104)
src/themes/kayron/TestimonialSection.tsx (2)
src/lib/types.ts (2)
  • TestimonialCardProps (81-88)
  • TestimonialSectionProps (90-93)
src/components/BaseLayout.tsx (1)
  • MessageCircleIcon (63-83)
🪛 GitHub Check: CodeQL
src/lib/themeServer.ts

[failure] 28-28: Unvalidated dynamic method call
Invocation of method with user-controlled name may dispatch to unexpected target and cause an exception.

🔇 Additional comments (16)
src/app/page.tsx (1)

1-3: Stub page is chill — W placeholder.

Keeping root minimal while redirecting "/" → "/es" is fine. No blockers here.

postcss.config.mjs (1)

1-5: Tailwind v4 PostCSS plug — looks valid, ship it.

Config is lean and matches TW v4 vibes. All good.

biome.json (2)

21-25: Biome domains — nice, on-brand for Next/React.

Enabling "domains": { "next": "recommended", "react": "recommended" } is a W. No notes.


9-11: Include negations are honored by Biome here — no change needed.
biome v2.1.2 ran; biome check scanned 54 files and did not include node_modules, .next, dist, or build, so the negated globs in biome.json are respected — yo frfr this pr is 🔥

package.json (1)

12-18: Sanity-check Next 15.5.0 + Node — yo frfr this pr is 🔥

  • Node on environment: v24.3.0 — satisfies Next 15.5.0 engines (node: '^18.18.0 || ^19.8.0 || >= 20.0.0').
  • package.json sets react/react-dom = "experimental" while Next peerDeps expect ^18.2.0 or ^19.x — verify the lockfile resolves "experimental" to a compatible React or pin explicit versions.
  • Ensure lockfile (package-lock.json / pnpm-lock.yaml / yarn.lock) is updated and CI installs/tests on Node v24.x pass.

Location: package.json lines 12-18 (also applies to 29-31).

next.config.ts (1)

4-6: Confirmed — experimental.viewTransition (singular) is correct

Docs and community threads confirm the key is experimental.viewTransition; your config already uses that — yo frfr this pr is 🔥

public/data/es.json (1)

1-82: Confirmed — placeholders wired to rendering code

dictionary.brand.copyright is .replace("{year}", new Date().getFullYear().toString()) and .replace("{brandName}", brandName) in src/app/[locale]/layout.tsx (lines 281–282). yo frfr this pr is 🔥

src/themes/kayron/index.ts (1)

11-20: Approve: Theme wiring looks clean — ship it
All mapped components exist under src/themes/kayron (Layout, Navbar, Hero, AboutSection, ProjectsSection, TestimonialSection, ContactForm, Footer).
yo frfr this pr is 🔥

src/app/globals.css (1)

1-14: yo frfr this pr is 🔥 — Tailwind v4 detected; confirm toolchain & config

  • File: src/app/globals.css (lines 1–14) uses @import "tailwindcss" and @theme inline (v4 syntax).
  • package.json shows tailwindcss: "^4".
  • fd returned no tailwind.config.{js,cjs,ts} matches.
  • Action: ensure CI/build targets Tailwind v4 or update the CSS to v3-compatible syntax / add a Tailwind config.
src/lib/dictionaries.ts (1)

1-4: Edge/runtime check — node:fs used; confirm no Edge routes or switch to JSON import

src/lib/dictionaries.ts imports { promises as fs } from "node:fs" (lines 1–4). Repo-wide search found no occurrences of export const runtime = "edge". If any route runs on the Edge runtime this will 500 — ensure affected routes use the Node runtime or load the dictionaries via static JSON import instead. yo frfr this pr is 🔥

src/themes/nicosup/AboutSection.tsx (1)

12-20: UI vibes are clean af

Layout/semantics look solid; headings + copy hierarchy land well. Ship it.

src/themes/kayron/AboutSection.tsx (1)

24-31: Animation delay via index — chill but be aware

Using index for delay is fine for cosmetics; if list order changes, timing shuffles. All good for now.

src/lib/themeServer.ts (1)

6-9: RNG theme picker is low-stakes, vibes good

No issues; server-only util is fine.

src/themes/kayron/ProjectsSection.tsx (1)

16-24: Header + empty/error states hit clean

Logic branches are crisp; copy lands. Ship it.

src/components/BaseLayout.tsx (1)

254-292: Assumes non-null themeComponents — aligned with provider, just flagging

Given ThemeProvider guarantees non-null, destructuring is valid. If you later allow null in context, add a guard here.

src/components/ThemeProvider.tsx (1)

16-22: Consolidate ThemeContextType nullability (themeComponents)

Local interface marks themeComponents as non-null while src/lib/types allows null — unify to a single source of truth to avoid downstream TS drift. I couldn't locate src/lib/types/other ThemeContextType declarations in the sandbox; verify which declaration is canonical and either move ThemeContextType to src/lib/types with non-null themeComponents and update imports, or relax the local type to allow null.
yo frfr this pr is 🔥

Comment on lines +43 to +46
"testimonials": {
"loading": "Loading testimonials...",
"error": "Error loading testimonials."
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add testimonials heading/subheading to enable i18n (un-hardcode UI) — chef’s kiss

Lets the Kayron section pull from the dictionary.

   "testimonials": {
+    "heading": "Testimonials",
+    "subheading": "What our community is saying.",
     "loading": "Loading testimonials...",
     "error": "Error loading testimonials."
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"testimonials": {
"loading": "Loading testimonials...",
"error": "Error loading testimonials."
},
"testimonials": {
"heading": "Testimonials",
"subheading": "What our community is saying.",
"loading": "Loading testimonials...",
"error": "Error loading testimonials."
},
🤖 Prompt for AI Agents
In public/data/en.json around lines 43 to 46, the testimonials section currently
only includes loading and error messages; add i18n keys for the testimonials
heading and subheading so the UI can pull those strings instead of hardcoding
them. Add entries such as "heading" and "subheading" (or "title" and "subtitle"
depending on existing conventions) with appropriate English copy, and ensure any
components reading the dictionary are updated to use these new keys.

Comment on lines +1 to +23
[
{
"quote": "Escuadrón 404 transformó mi carrera como desarrollador. La comunidad es increíble, siempre hay alguien dispuesto a ayudar y los proyectos colaborativos me han permitido crecer exponencialmente.",
"authorName": "María González",
"authorRole": "Senior Full Stack Developer",
"authorAvatar": null,
"rating": 5
},
{
"quote": "Desde que me uní a Escuadrón 404, he encontrado un ambiente de aprendizaje inigualable. Las mentorías son de gran valor y la camaradería es fantástica.",
"authorName": "Carlos Pérez",
"authorRole": "Junior Software Engineer",
"authorAvatar": null,
"rating": 4
},
{
"quote": "Los eventos tech organizados por Escuadrón 404 son siempre relevantes y muy bien organizados. Es una excelente manera de mantenerse actualizado y conectar con otros profesionales.",
"authorName": "Ana López",
"authorRole": "Data Scientist",
"authorAvatar": null,
"rating": 5
}
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add stable ids for React keys — keep lists crispy, no diff thrash

IDs also help future edits/analytics.

 [
   {
+    "id": "maria-gonzalez",
     "quote": "Escuadrón 404 transformó mi carrera como desarrollador. La comunidad es increíble, siempre hay alguien dispuesto a ayudar y los proyectos colaborativos me han permitido crecer exponencialmente.",
     "authorName": "María González",
     "authorRole": "Senior Full Stack Developer",
     "authorAvatar": null,
     "rating": 5
   },
   {
+    "id": "carlos-perez",
     "quote": "Desde que me uní a Escuadrón 404, he encontrado un ambiente de aprendizaje inigualable. Las mentorías son de gran valor y la camaradería es fantástica.",
     "authorName": "Carlos Pérez",
     "authorRole": "Junior Software Engineer",
     "authorAvatar": null,
     "rating": 4
   },
   {
+    "id": "ana-lopez",
     "quote": "Los eventos tech organizados por Escuadrón 404 son siempre relevantes y muy bien organizados. Es una excelente manera de mantenerse actualizado y conectar con otros profesionales.",
     "authorName": "Ana López",
     "authorRole": "Data Scientist",
     "authorAvatar": null,
     "rating": 5
   }
 ]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[
{
"quote": "Escuadrón 404 transformó mi carrera como desarrollador. La comunidad es increíble, siempre hay alguien dispuesto a ayudar y los proyectos colaborativos me han permitido crecer exponencialmente.",
"authorName": "María González",
"authorRole": "Senior Full Stack Developer",
"authorAvatar": null,
"rating": 5
},
{
"quote": "Desde que me uní a Escuadrón 404, he encontrado un ambiente de aprendizaje inigualable. Las mentorías son de gran valor y la camaradería es fantástica.",
"authorName": "Carlos Pérez",
"authorRole": "Junior Software Engineer",
"authorAvatar": null,
"rating": 4
},
{
"quote": "Los eventos tech organizados por Escuadrón 404 son siempre relevantes y muy bien organizados. Es una excelente manera de mantenerse actualizado y conectar con otros profesionales.",
"authorName": "Ana López",
"authorRole": "Data Scientist",
"authorAvatar": null,
"rating": 5
}
]
[
{
"id": "maria-gonzalez",
"quote": "Escuadrón 404 transformó mi carrera como desarrollador. La comunidad es increíble, siempre hay alguien dispuesto a ayudar y los proyectos colaborativos me han permitido crecer exponencialmente.",
"authorName": "María González",
"authorRole": "Senior Full Stack Developer",
"authorAvatar": null,
"rating": 5
},
{
"id": "carlos-perez",
"quote": "Desde que me uní a Escuadrón 404, he encontrado un ambiente de aprendizaje inigualable. Las mentorías son de gran valor y la camaradería es fantástica.",
"authorName": "Carlos Pérez",
"authorRole": "Junior Software Engineer",
"authorAvatar": null,
"rating": 4
},
{
"id": "ana-lopez",
"quote": "Los eventos tech organizados por Escuadrón 404 son siempre relevantes y muy bien organizados. Es una excelente manera de mantenerse actualizado y conectar con otros profesionales.",
"authorName": "Ana López",
"authorRole": "Data Scientist",
"authorAvatar": null,
"rating": 5
}
]
🤖 Prompt for AI Agents
public/data/testimonials.json lines 1-23: The JSON array of testimonial objects
lacks stable id fields for React list keys; add an "id" property to each
testimonial (use stable values—UUID strings, deterministic slugs, or numeric
ids) so React can key list items consistently and avoid re-render/diff thrash;
update each object to include a unique "id" field (e.g., "id":
"maría-gonzalez-1" or a UUID) and ensure any code that renders these
testimonials uses that "id" as the key.

Comment on lines +44 to +54
export async function generateMetadata(props: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const params = await props.params;
const { locale } = params;
const dictionary = await getDictionary(locale as "en" | "es");
return {
title: `${dictionary.brand.name} - ${dictionary.brand.tagline}`,
description: dictionary.hero.subtitle,
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Next params aren’t Promises — fix types and usage

Typing params as Promise is cap; use plain object.

-export async function generateMetadata(props: {
-  params: Promise<{ locale: string }>;
-}): Promise<Metadata> {
-  const params = await props.params;
-  const { locale } = params;
+export async function generateMetadata({
+  params,
+}: {
+  params: { locale: string };
+}): Promise<Metadata> {
+  const { locale } = params;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function generateMetadata(props: {
params: Promise<{ locale: string }>;
}): Promise<Metadata> {
const params = await props.params;
const { locale } = params;
const dictionary = await getDictionary(locale as "en" | "es");
return {
title: `${dictionary.brand.name} - ${dictionary.brand.tagline}`,
description: dictionary.hero.subtitle,
};
}
export async function generateMetadata({
params,
}: {
params: { locale: string };
}): Promise<Metadata> {
const { locale } = params;
const dictionary = await getDictionary(locale as "en" | "es");
return {
title: `${dictionary.brand.name} - ${dictionary.brand.tagline}`,
description: dictionary.hero.subtitle,
};
}
🤖 Prompt for AI Agents
In src/app/[locale]/layout.tsx around lines 44 to 54, props.params is
incorrectly typed as a Promise and awaited; change the function signature to
accept props: { params: { locale: string } }, remove the await when accessing
params (use const { locale } = props.params), and keep the dictionary fetch as
await getDictionary(locale as "en" | "es"); update any related types accordingly
so params is treated as a plain object rather than a Promise.

Comment on lines +70 to +72
{testimonials.map((testimonial, index) => (
<TestimonialCard key={index} {...testimonial} />
))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid index as key — use stable key from data, plz — vibing but keep it solid

Index keys cause janky reorders. Use authorName (or add id).

-          {testimonials.map((testimonial, index) => (
-            <TestimonialCard key={index} {...testimonial} />
+          {testimonials.map((t, index) => (
+            <TestimonialCard key={`${t.authorName}-${index}`} {...t} />
           ))}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/themes/kayron/TestimonialSection.tsx around lines 70 to 72, the map
currently uses the array index as the React key which leads to unstable
reorders; change the key to a stable unique identifier from the testimonial
object (prefer testimonial.id, otherwise testimonial.authorName if guaranteed
unique) and update the data source to include an id field if needed; ensure the
key prop on <TestimonialCard> uses that stable property so React can track items
correctly.

Comment on lines +17 to +21
const turnstileSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "poop"; // Use a safer default or error
const [formData, setFormData] = useState<Record<string, string>>({});
const [turnstileToken, setTurnstileToken] = useState<string | null>(
isDev ? "development_bypass_token" : null,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Don’t ship “poop” default; wrong var checked — vibes off + logic bug

The default is for site key but you compare it against the token later.

-  const turnstileSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "poop"; // Use a safer default or error
+  const turnstileSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ?? "";
+  const siteKeyMissing = !isDev && !turnstileSiteKey;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const turnstileSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY || "poop"; // Use a safer default or error
const [formData, setFormData] = useState<Record<string, string>>({});
const [turnstileToken, setTurnstileToken] = useState<string | null>(
isDev ? "development_bypass_token" : null,
);
const turnstileSiteKey = process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY ?? "";
const siteKeyMissing = !isDev && !turnstileSiteKey;
const [formData, setFormData] = useState<Record<string, string>>({});
const [turnstileToken, setTurnstileToken] = useState<string | null>(
isDev ? "development_bypass_token" : null,
);
🤖 Prompt for AI Agents
In src/themes/nicosup/ContactForm.tsx around lines 17 to 21, the Turnstile site
key default is an inappropriate placeholder ("poop") and later code compares the
wrong variable (token vs site key); replace the silly default with a safe
fallback (empty string or undefined) or throw/configure to fail fast, and update
any checks to compare the correct variable (use turnstileSiteKey when validating
presence of the site key, and only use turnstileToken for token-specific logic);
ensure development bypass token remains assigned only in dev and that runtime
checks use the proper variables so the logic is correct and vibes are fixed.

Comment on lines +30 to +43
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!turnstileToken || (turnstileToken === "poop" && !isDev)) {
alert("CAPTCHA site key is missing or not configured for production.");
return;
}
if (
!turnstileToken ||
(turnstileToken === "development_bypass_token" && !isDev)
) {
alert("Please complete the CAPTCHA.");
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

CAPTCHA gating: check site key presence and token separately

Compare against siteKey, not token, and keep prod/dev behavior clean.

   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault();
-    if (!turnstileToken || (turnstileToken === "poop" && !isDev)) {
-      alert("CAPTCHA site key is missing or not configured for production.");
-      return;
-    }
-    if (
-      !turnstileToken ||
-      (turnstileToken === "development_bypass_token" && !isDev)
-    ) {
-      alert("Please complete the CAPTCHA.");
-      return;
-    }
+    if (siteKeyMissing) {
+      alert("CAPTCHA site key is not configured.");
+      return;
+    }
+    if (!isDev && !turnstileToken) {
+      alert("Please complete the CAPTCHA.");
+      return;
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/themes/nicosup/ContactForm.tsx around lines 30 to 43, the current CAPTCHA
checks incorrectly inspect the token for site-key presence and mix
production/dev behavior; change the first check to verify the siteKey exists and
when in production (isDev false) alert and return if missing, and change the
second check to verify the turnstileToken itself (allowing a development bypass
token only when isDev is true) so production rejects the bypass token and
prompts the user to complete CAPTCHA.

Comment on lines +55 to +60
// Basic form validation: check if all required fields are filled
const isFormValid = fields.every(
(field) => !field.required || (formData[field.name]?.trim() ?? "") !== "",
);
const canSubmit = isFormValid && (turnstileToken !== null || isDev);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Submission enablement should also respect siteKey presence

Disable submit when site key missing in prod.

-  const canSubmit = isFormValid && (turnstileToken !== null || isDev);
+  const canSubmit = isFormValid && !siteKeyMissing && (turnstileToken !== null || isDev);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Basic form validation: check if all required fields are filled
const isFormValid = fields.every(
(field) => !field.required || (formData[field.name]?.trim() ?? "") !== "",
);
const canSubmit = isFormValid && (turnstileToken !== null || isDev);
// Basic form validation: check if all required fields are filled
const isFormValid = fields.every(
(field) => !field.required || (formData[field.name]?.trim() ?? "") !== "",
);
const canSubmit = isFormValid && !siteKeyMissing && (turnstileToken !== null || isDev);
🤖 Prompt for AI Agents
In src/themes/nicosup/ContactForm.tsx around lines 55 to 60, the canSubmit check
only considers form validity and turnstile token but doesn't ensure a siteKey
exists in production; update canSubmit to also require a non-empty siteKey when
not in dev (e.g., include a condition like isDev || (siteKey && siteKey.trim()
!== "")), so submissions are disabled if siteKey is missing in prod.

Comment on lines +129 to +141
<div className="mt-4">
{isDev ? (
<p className="text-muted-foreground text-sm">
CAPTCHA bypassed in development mode.
</p>
) : (
<Turnstile
siteKey={turnstileSiteKey}
onSuccess={(token: string) => setTurnstileToken(token)}
options={{ theme: "light" }} // Light theme for Pix
/>
)}
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Render fallback when site key missing (prod) — fail loud, not weird

Show a clear message instead of a broken widget.

           <div className="mt-4">
             {isDev ? (
               <p className="text-muted-foreground text-sm">
                 CAPTCHA bypassed in development mode.
               </p>
             ) : (
-              <Turnstile
-                siteKey={turnstileSiteKey}
-                onSuccess={(token: string) => setTurnstileToken(token)}
-                options={{ theme: "light" }} // Light theme for Pix
-              />
+              <>
+                {siteKeyMissing ? (
+                  <p className="text-red-500 text-sm">
+                    CAPTCHA misconfigured. Please contact the site admin.
+                  </p>
+                ) : (
+                  <Turnstile
+                    siteKey={turnstileSiteKey}
+                    onSuccess={(token: string) => setTurnstileToken(token)}
+                    options={{ theme: "light" }}
+                  />
+                )}
+              </>
             )}
           </div>

Committable suggestion skipped: line range outside the PR's diff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants