Skip to content

(SP: 3) [Frontend] Complete About Page redesign: add topics, sponsors, and updated stats#158

Merged
ViktorSvertoka merged 3 commits into
developfrom
yd/feat/about/about-us-redesign
Jan 19, 2026
Merged

(SP: 3) [Frontend] Complete About Page redesign: add topics, sponsors, and updated stats#158
ViktorSvertoka merged 3 commits into
developfrom
yd/feat/about/about-us-redesign

Conversation

@yevheniidatsenko
Copy link
Copy Markdown
Collaborator

@yevheniidatsenko yevheniidatsenko commented Jan 18, 2026

Description

This PR introduces a complete redesign of the About Us page. The goal was to modernize the UI, improve the information hierarchy, and integrate new interactive modules (sponsors, stats, and topics) to better showcase the project's value.


Related Issue

Issue: #---

Changes

  • Complete Page Redesign: Implemented a fresh, modern aesthetic for the About Us page.
  • Interactive Features Section: Added a specialized section highlighting core platform features (Q&A, Quizzes, Leaderboard) with a custom 3D browser-like visual.
  • Sponsors & Supporters: Integrated a "Supporters Registry" section to acknowledge project partners and sponsors.
  • Project Topics: Added a structured breakdown of key technical areas and project goals.
  • GitHub Statistics: Implemented a visual section for project activity and repository stats.

Database Changes (if applicable)

  • Schema migration required
  • Seed data updated
  • Breaking changes to existing queries
  • Transaction-safe migration
  • Migration tested locally on Neon

How Has This Been Tested?

  • Tested locally
  • Verified in development environment
  • Checked responsive layout (Desktop and Tablet)
  • Tested accessibility (keyboard / screen reader)

Screenshots (if applicable)


Checklist

Before submitting

  • Code has been self-reviewed
  • No TypeScript or console errors
  • Code follows project conventions
  • Scope is limited to this feature/fix
  • No unrelated refactors included
  • English used in code, commits, and docs
  • New dependencies discussed with team
  • Database migration tested locally (if applicable)
  • GitHub Projects card moved to In Review

Reviewers

Summary by CodeRabbit

  • New Features

    • Interactive mini-game added to the About page.
    • Topics (ecosystem) section introduced.
    • Testimonials section with looping marquee and GitHub Discussions CTA.
    • Sponsors wall added to showcase supporters.
  • UI/UX Improvements

    • Redesigned About layout with improved dark/light responsiveness.
    • Features section converted to interactive tabs with animated transitions.
    • New horizontal/vertical scroll animations and hover-pause behavior.
    • Hero now displays live platform statistics when available.
  • Chores

    • Updated development dependency pins.

✏️ Tip: You can customize this high-level summary in your review settings.

@netlify
Copy link
Copy Markdown

netlify Bot commented Jan 18, 2026

Deploy Preview for develop-devlovers ready!

Name Link
🔨 Latest commit da72391
🔍 Latest deploy log https://app.netlify.com/projects/develop-devlovers/deploys/696d60f01eaa1300082a5943
😎 Deploy Preview https://deploy-preview-158--develop-devlovers.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 18, 2026

📝 Walkthrough

Walkthrough

About page converted to an async server component that fetches platform stats and GitHub sponsors in parallel; multiple about-related UI components were added or refactored (TopicsSection, InteractiveGame, SponsorsWall, HeroSection, FeaturesSection, PricingSection, CommunitySection), StatsSection removed, and new data/util libs introduced for stats and sponsors.

Changes

Cohort / File(s) Change Summary
About Page & Data Fetching
frontend/app/[locale]/about/page.tsx, frontend/lib/about/stats.ts, frontend/lib/about/github-sponsors.ts
Page made async; parallel Promise.all fetch for platform stats and sponsors; new getPlatformStats (cached) and getSponsors GraphQL helper added with typed Sponsor/PlatformStats interfaces.
Data Constants
frontend/data/about.ts
Added exported TESTIMONIALS and TOPICS arrays and corresponding exported types (Testimonial, Topic).
Hero / Interactive Centerpiece
frontend/components/about/HeroSection.tsx, frontend/components/about/InteractiveGame.tsx
HeroSection now accepts optional stats prop and adds mouse-tracking glow and GlassWidget/MobileStatItem; new InteractiveGame component implements a client-side mini-game with localStorage high score and animation loop.
Features UX Overhaul
frontend/components/about/FeaturesSection.tsx
Replaced card grid with tab-driven interactive features UI, framer-motion animations, and per-tab visual components.
Community / Testimonials
frontend/components/about/CommunitySection.tsx, frontend/components/about/TestimonialCard (internal)
Switched to data-driven testimonials (TESTIMONIALS), looped rows replacing carousel, redesigned TestimonialCard props (now uses icon/color).
Pricing & Sponsors
frontend/components/about/PricingSection.tsx, frontend/components/about/SponsorsWall.tsx
Pricing simplified to static cards, accepts sponsors?: Sponsor[]; new SponsorsWall component renders sponsor avatars, hover info, and CTA.
New Topics Section
frontend/components/about/TopicsSection.tsx
Added TopicsSection that maps TOPICS into TopicCard tiles with motion entry animations.
Removed Stats Component
frontend/components/about/StatsSection.tsx
Deleted StatsSection file; its responsibilities moved to getPlatformStats and HeroSection via props.
Styling & Config
frontend/app/globals.css, frontend/next.config.ts
Added horizontal/vertical scroll animations and pause-on-hover CSS; added remote image domains for github.com and cdn.jsdelivr.net.
Misc / Packaging
frontend/package.json, frontend/app/[locale]/contacts/page.tsx
Dev dependency version bumps for type packages; minor newline/trailing formatting change.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant AboutPage as AboutPage (async)
    participant StatsLib as getPlatformStats()
    participant SponsorsLib as getSponsors()
    participant Hero as HeroSection
    participant Pricing as PricingSection
    Browser->>AboutPage: Request /about
    par Parallel fetch
        AboutPage->>StatsLib: fetch platform stats
        AboutPage->>SponsorsLib: fetch sponsors
    end
    StatsLib-->>AboutPage: PlatformStats
    SponsorsLib-->>AboutPage: Sponsor[]
    AboutPage->>Hero: pass stats prop
    AboutPage->>Pricing: pass sponsors prop
    AboutPage->>Browser: render composed page (Topics, Features, Community, etc.)
    Browser->>Hero: user interactions (mouse, clicks)
    Hero->>Hero: update motion values / InteractiveGame start
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • AM1007
  • ViktorSvertoka
  • LesiaUKR

Poem

🐇
I hopped through lines of code today,
Stats fetched in parallel, sponsors at play,
Topics sprout and a tiny game leaps high,
Avatars shimmer, tabs flip by—
A rabbit claps as the About page sighs.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 clearly summarizes the main objective of the PR: a complete About page redesign with three key additions (topics, sponsors, and updated stats), which directly aligns with the major file changes (new components, data structures, and the redesigned AboutPage).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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.

Copy link
Copy Markdown
Contributor

@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: 4

🤖 Fix all issues with AI agents
In `@frontend/components/about/PricingSection.tsx`:
- Around line 30-143: PricingSection currently hard-codes all UI strings;
replace them with next-intl translations by importing and calling
useTranslations('pricing') inside the PricingSection component and swapping each
literal (e.g., headings "Invest in your brain, not our subscriptions.",
paragraph copy, plan titles "Junior Engineer", "Open Source Hero", feature list
items like "Unlimited Questions", buttons "Start Learning", "Support the
Project", badge "High Impact", footer note, and any other visible text) for
t('key') lookups using descriptive keys (e.g., title, subtitle,
plan.junior.title, plan.openSource.title, feature.unlimitedQuestions,
cta.startLearning, cta.supportProject, badge.highImpact, footer.note) so the
component reads translations from useTranslations('pricing') instead of
hard-coded strings.

In `@frontend/components/about/TopicsSection.tsx`:
- Around line 70-75: The Next Image usage in the TopicsSection component (Image
with src={topic.icon} / alt={topic.name}) loads icons from cdn.jsdelivr.net but
that host is not included in the Next.js remotePatterns; update next.config.ts
to add a remote pattern allowing protocol "https", hostname "cdn.jsdelivr.net"
and a wildcard pathname (e.g., /**) so Next's Image optimization accepts
topic.icon URLs from jsDelivr, then restart the dev server to apply the change.

In `@frontend/data/about.ts`:
- Around line 51-60: The TOPICS array has inconsistent indentation—specifically
the object starting with id: "git" is indented with extra leading spaces; update
the TOPICS array so each object literal (e.g., the entry with id "git", name
"Git & Version Control", href "/q&a") uses consistent 4-space indentation for
its opening brace and all properties, aligning it with the other entries in
TOPICS.

In `@frontend/lib/about/github-sponsors.ts`:
- Around line 44-60: The fetch block that calls
fetch("https://api.github.com/graphql") currently assumes a 200 response and
that res.json() succeeds; change it to first check res.ok and, if false, attempt
to read the response text/JSON to extract and log the error message (including
res.status) and return [] for non-OK responses, and wrap the res.json() call in
a try/catch to handle malformed JSON; update the error handling around the
variables res and json (and the surrounding function that constructs
query/token) to ensure authentication failures (401) and parse errors produce an
explicit log and early return rather than falling through to "Found 0 sponsors."
🧹 Nitpick comments (14)
frontend/app/globals.css (3)

209-211: Consider using a CSS variable for duration consistency.

The horizontal scroll animation uses a hardcoded 40s duration, while the vertical marquee (line 224) uses a CSS variable --duration with a fallback. Using a CSS variable for both would improve flexibility and consistency.

♻️ Suggested change
 .animate-scroll {
-  animation: scroll 40s linear infinite;
+  animation: scroll var(--scroll-duration, 40s) linear infinite;
 }

213-215: Inconsistent class naming for pause-on-hover behavior.

Two different naming conventions are used: .pause-on-hover (line 213) and .hover\:pause (line 228). Consider unifying these for consistency and to reduce confusion when applying the classes in components.

Also applies to: 228-230


217-230: Non-English comments reduce maintainability for diverse teams.

The comments on lines 220 and 227 are in Ukrainian. For better collaboration across an international team, consider translating them to English.

♻️ Suggested translations
 `@keyframes` marquee-vertical {
   0% { transform: translateY(0); }
-  100% { transform: translateY(-50%); } /* Рухаємось на 50%, бо контент дубльовано */
+  100% { transform: translateY(-50%); } /* Move 50% because content is duplicated */
 }

 .animate-marquee-vertical {
   animation: marquee-vertical var(--duration, 40s) linear infinite;
 }

-/* Зупинка при наведенні мишкою, щоб роздивитися */
+/* Pause on hover to allow inspection */
 .hover\:pause:hover .animate-marquee-vertical {
   animation-play-state: paused;
 }
frontend/app/[locale]/about/page.tsx (1)

17-19: Verify horizontal scroll behavior.

The w-[100vw] with negative margin centering technique (left-[50%] -ml-[50vw]) can cause horizontal scrollbar issues if the page has a vertical scrollbar (since 100vw includes scrollbar width on some browsers). Consider testing on Windows/Linux where scrollbars are typically always visible.

A safer alternative is using w-screen with overflow-x-hidden on a parent, or using CSS calc(100vw - var(--scrollbar-width)).

frontend/components/about/TopicsSection.tsx (1)

47-47: Add proper TypeScript type for topic parameter.

Using any type loses type safety. Define or import a Topic type from the data file.

♻️ Suggested improvement

In frontend/data/about.ts, export a type:

export type Topic = typeof TOPICS[number]

Then in TopicsSection.tsx:

+import { TOPICS, type Topic } from "@/data/about"
-import { TOPICS } from "@/data/about"

-function TopicCard({ topic, index }: { topic: any, index: number }) {
+function TopicCard({ topic, index }: { topic: Topic, index: number }) {
frontend/components/about/SponsorsWall.tsx (1)

14-17: Remove redundant variable assignment.

displaySponsors is assigned directly from sponsors with no transformation. Consider using sponsors directly.

Suggested simplification
 export function SponsorsWall({ sponsors = [] }: SponsorsWallProps) {
-  
-  const displaySponsors = sponsors
 
   return (
     <div className="w-full mt-16 flex flex-col items-center">
       ...
         <div className="flex -space-x-3 md:-space-x-4 pl-2">
-            {displaySponsors.map((sponsor) => (
+            {sponsors.map((sponsor) => (
                 <SponsorItem key={sponsor.login} sponsor={sponsor} />
             ))}
 
-             {displaySponsors.length === 0 && (
+             {sponsors.length === 0 && (
frontend/lib/about/stats.ts (2)

27-31: Redundant revalidation configuration.

The next.revalidate option on the fetch call is redundant since the entire function is already wrapped in unstable_cache with revalidate: 3600. The outer cache controls when this function re-executes.

Also, consider using English for code comments to maintain consistency across the codebase.

Suggested fix
-        // Додаємо тип any для опцій fetch, щоб TS не лаявся на next.js розширення
-        const res = await fetch('https://api.github.com/repos/DevLoversTeam/devlovers.net', { 
-            headers, 
-            next: { revalidate: 3600 } 
-        } as RequestInit & { next?: { revalidate?: number } })
+        // Fetch GitHub repo data for star count
+        const res = await fetch('https://api.github.com/repos/DevLoversTeam/devlovers.net', { 
+            headers,
+            cache: 'no-store' // Parent unstable_cache handles caching
+        })

42-49: Consider parallelizing database queries.

The two count() queries are independent and could be executed in parallel using Promise.all to reduce latency.

Suggested optimization
     try {
-      const [u] = await db.select({ value: count() }).from(users)
-      if (u) totalUsers = u.value
-      const [q] = await db.select({ value: count() }).from(quizAttempts)
-      if (q) solvedTests = q.value
+      const [[u], [q]] = await Promise.all([
+        db.select({ value: count() }).from(users),
+        db.select({ value: count() }).from(quizAttempts)
+      ])
+      if (u) totalUsers = u.value
+      if (q) solvedTests = q.value
     } catch (e) { 
         console.error("DB Fetch Error:", e) 
     }
frontend/components/about/HeroSection.tsx (1)

128-143: Add proper TypeScript types for component props.

Using any type loses type safety. Consider defining explicit prop types for better maintainability and IDE support.

Suggested type definition
+interface StatWidgetProps {
+    icon: React.ComponentType<{ size?: number }>
+    color: string
+    bg: string
+    label: string
+    value: string
+}
+
-function GlassWidget({ icon: Icon, color, bg, label, value }: any) {
+function GlassWidget({ icon: Icon, color, bg, label, value }: StatWidgetProps) {

Apply the same type to MobileStatItem at line 145.

frontend/components/about/InteractiveGame.tsx (3)

75-79: Avoid using ternary expressions for side effects.

The eslint-disable comment hints at a code smell. Using a ternary for side effects reduces readability. Prefer an explicit conditional.

Suggested fix
             if (e.code === "Space" || e.code === "ArrowUp") {
                 e.preventDefault()
-                // eslint-disable-next-line `@typescript-eslint/no-unused-expressions`
-                gameOver ? handleRetry() : jump()
+                if (gameOver) {
+                    handleRetry()
+                } else {
+                    jump()
+                }
             }

110-116: High-frequency state updates may impact performance.

The score increments on every animation frame (~60 times/second), triggering React re-renders each time. Consider batching updates or using a ref for the score during gameplay and only syncing to state periodically.

Suggested optimization approach
+    const scoreRef = useRef(0)
+
     // In the game loop:
-                setScore(s => {
-                    const newScore = s + 1
-                    if (newScore % 300 === 0) {
-                        setGameSpeed(prev => Math.max(0.7, prev * 0.95))
-                    }
-                    return newScore
-                })
+                scoreRef.current += 1
+                if (scoreRef.current % 300 === 0) {
+                    setGameSpeed(prev => Math.max(0.7, prev * 0.95))
+                }
+                // Sync to state less frequently (e.g., every 10 frames)
+                if (scoreRef.current % 10 === 0) {
+                    setScore(scoreRef.current)
+                }

268-273: Global style injection may cause conflicts.

Using style jsx global injects styles globally. Consider scoping the animation name or moving it to a CSS module/global stylesheet to prevent potential naming collisions.

frontend/components/about/CommunitySection.tsx (1)

90-98: Add proper TypeScript types for TestimonialCard props.

Using any loses type safety. The props should match the structure from TESTIMONIALS in frontend/data/about.ts.

Suggested type definition
+interface TestimonialCardProps {
+    name: string
+    role: string
+    avatar: string
+    content: string
+    platform: string
+    icon: React.ComponentType<{ size?: number }>
+    color: string
+}
+
 function TestimonialCard({
   name,
   role,
   avatar,
   content,
   platform,
   icon: Icon,
   color
-}: any) {
+}: TestimonialCardProps) {
frontend/lib/about/github-sponsors.ts (1)

62-80: Tighten typing for sponsor nodes to avoid any.

This is an easy win for maintainability and safer refactors (e.g., tier shape changes). Consider a local type + type guard instead of any.

♻️ Proposed refactor
-    const rawNodes = json.data?.organization?.sponsorshipsAsMaintainer?.nodes || []
+    type SponsorshipNode = {
+      tier?: { monthlyPriceInDollars?: number | null } | null
+      sponsorEntity?: { login: string; name?: string | null; avatarUrl: string } | null
+    }
+
+    const rawNodes: SponsorshipNode[] =
+      json.data?.organization?.sponsorshipsAsMaintainer?.nodes ?? []
 
     console.log(`✅ GitHub: Found ${rawNodes.length} sponsors for Organization`)
 
-    const sponsors: Sponsor[] = rawNodes.map((node: any) => {
+    const sponsors = rawNodes.map((node) => {
       const price = node.tier?.monthlyPriceInDollars || 0
       const { name, color } = getTierDetails(price)
 
       if (!node.sponsorEntity) return null
@@
         tierName: name,
         tierColor: color,
       }
-    }).filter(Boolean) as Sponsor[]
+    }).filter((s): s is Sponsor => Boolean(s))

Comment on lines +30 to +143
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-gray-900 dark:text-white mb-6">
Invest in your brain, <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-gray-500 to-gray-700 dark:from-neutral-400 dark:to-neutral-600">not our subscriptions.</span>
</h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto">
{t("subtitle")}
<p className="text-gray-700 dark:text-neutral-400 max-w-2xl mx-auto text-lg font-light">
We believe knowledge should be accessible. So we don&apos;t sell courses. But servers heat up and coffee runs out. The choice is yours.
</p>
</motion.div>

<motion.div
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
className="relative mx-auto max-w-3xl"
>
<div className="absolute -top-4 left-1/2 -translate-x-1/2 z-20">
<span className="flex items-center gap-1 rounded-full bg-gradient-to-r from-[#2C7FFF] to-blue-600 px-4 py-1 text-xs font-bold text-white shadow-lg shadow-blue-500/20 uppercase tracking-wider">
<Sparkles className="h-6 w-3" /> {t("badge")}
</span>
</div>

<div className="overflow-hidden rounded-3xl border border-border bg-card shadow-2xl transition-all duration-300 hover:shadow-[#2C7FFF]/10 hover:border-[#2C7FFF]/30">
<div className="px-8 py-12 md:px-16 text-center">

<div className="mb-10 min-h-[160px] flex flex-col justify-center">
<p className="text-sm font-medium text-muted-foreground mb-4 uppercase tracking-widest">{t("monthlyPrice")}</p>
</div>

<PriceEvolution />
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto mb-24">

<motion.div
whileHover={{ y: -5 }}
className="flex flex-col p-8 rounded-3xl border border-gray-200 dark:border-neutral-800 bg-white dark:bg-neutral-900/50 backdrop-blur-sm shadow-sm"
>
<div className="mb-6">
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2">Junior Engineer</h3>
<p className="text-sm text-gray-600 dark:text-neutral-400">For those who want an offer, not expenses.</p>
</div>
<div className="mb-8">
<span className="text-5xl font-black text-gray-900 dark:text-white">$0</span>
<span className="text-gray-500 dark:text-neutral-500 font-mono text-sm ml-2">/ forever</span>
</div>

<ul className="space-y-4 mb-8 flex-1">
{[
"Unlimited Questions",
"Full Quiz Access",
"No Credit Card Required",
"0% Guilt Trip",
].map((item) => (
<li key={item} className="flex items-center gap-3 text-sm text-gray-700 dark:text-neutral-300">
<div className="p-1 rounded-full bg-green-500/10 text-green-500">
<Check size={12} />
</div>
{item}
</li>
))}
<li className="flex items-center gap-3 text-sm text-gray-400 dark:text-neutral-500 line-through decoration-gray-300 dark:decoration-neutral-700">
<div className="p-1 rounded-full bg-gray-100 dark:bg-neutral-800 text-gray-400 dark:text-neutral-600">
<X size={12} />
</div>
Personal Yacht
</li>
</ul>

<div className="mt-6">
<p className="text-[#2C7FFF] font-medium bg-[#2C7FFF]/10 inline-block px-4 py-1 rounded-full text-sm">
{t("free")}
</p>
</div>
</div>
<Link href="/" className="w-full py-4 rounded-xl border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 hover:bg-gray-100 dark:hover:bg-white/10 text-gray-900 dark:text-white font-bold text-center transition-all uppercase tracking-widest text-xs">
Start Learning
</Link>
</motion.div>

<div className="h-px w-full bg-gradient-to-r from-transparent via-border to-transparent mb-10" />
<motion.div
whileHover={{ y: -5 }}
className="relative flex flex-col p-8 rounded-3xl overflow-hidden backdrop-blur-sm
border border-[#1e5eff]/30 dark:border-[#ff2d55]/30
bg-gradient-to-b from-[#1e5eff]/5 to-white dark:from-[#ff2d55]/10 dark:to-neutral-900/50"
>
<div className="absolute top-0 right-0 px-3 py-1 rounded-bl-xl uppercase tracking-widest text-[10px] font-bold text-white
bg-[#1e5eff] dark:bg-[#ff2d55]"
>
High Impact
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-12 text-left max-w-lg mx-auto mb-12">
{featureKeys.map((featureKey) => (
<div key={featureKey} className="flex items-center gap-3">
<div className="flex-shrink-0 h-5 w-5 rounded-full bg-[#2C7FFF]/10 flex items-center justify-center">
<Heart className="h-3 w-3 text-[#2C7FFF] fill-[#2C7FFF]" />
</div>
<span className="text-muted-foreground text-sm font-medium">{tFeatures(featureKey)}</span>
<div className="mb-6">
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2 flex items-center gap-2">
Open Source Hero
<Heart size={18} className="fill-[#1e5eff] text-[#1e5eff] dark:fill-[#ff2d55] dark:text-[#ff2d55]" />
</h3>
<p className="text-sm text-neutral-500 dark:text-neutral-400">For those who already landed an offer thanks to us.</p>
</div>
<div className="mb-8">
<span className="text-5xl font-black text-[#1e5eff] dark:text-[#ff2d55]">$$$</span>
<span className="text-neutral-500 font-mono text-sm ml-2">/ karma points</span>
</div>

<ul className="space-y-4 mb-8 flex-1">
{[
"Keep Servers Alive",
"Buy Coffee for Mentors",
"Profile Badge (Big Flex)",
"Warm Fuzzy Feeling",
].map((item) => (
<li key={item} className="flex items-center gap-3 text-sm text-gray-900 dark:text-white font-medium">
<div className="p-1 rounded-full bg-[#1e5eff]/20 text-[#1e5eff] dark:bg-[#ff2d55]/20 dark:text-[#ff2d55]">
<Sparkles size={12} />
</div>
))}
</div>

<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<button className="w-full sm:w-auto px-8 py-3 rounded-full bg-[#2C7FFF] hover:bg-blue-600 text-white font-bold transition-all shadow-lg shadow-[#2C7FFF]/25">
{t("cta")}
</button>
{item}
</li>
))}
<li className="flex items-center gap-3 text-sm text-gray-600 dark:text-neutral-400 italic">
<div className="p-1 rounded-full bg-gray-200 dark:bg-neutral-800 text-gray-500 dark:text-neutral-500">
<Server size={12} />
</div>
We actually pay for Drizzle
</li>
</ul>

<Link
href="https://github.com/sponsors/DevLoversTeam"
target="_blank"
rel="noopener noreferrer"
className="group w-full py-4 rounded-xl text-white font-bold flex items-center justify-center gap-2 transition-all uppercase tracking-widest text-xs
bg-[#1e5eff] hover:bg-[#1e5eff]/90 shadow-[0_0_20px_rgba(30,94,255,0.3)] hover:shadow-[0_0_30px_rgba(30,94,255,0.5)]
dark:bg-[#ff2d55] dark:hover:bg-[#ff2d55]/90 dark:shadow-[0_0_20px_rgba(255,45,85,0.3)] dark:hover:shadow-[0_0_30px_rgba(255,45,85,0.5)]"
>
Support the Project <ArrowRight size={14} className="group-hover:translate-x-1 transition-transform"/>
</Link>
</motion.div>

<a
href="https://buymeacoffee.com/viktor.svertoka"
target="_blank"
rel="noreferrer"
className="w-full sm:w-auto px-8 py-3 rounded-full border border-border hover:bg-muted text-foreground font-medium transition-all flex items-center justify-center gap-2 group"
>
<Coffee className="h-4 w-4 text-muted-foreground group-hover:text-[#2C7FFF] transition-colors" />
<span>{t("coffee")}</span>
</a>
</div>
</div>

<p className="text-center text-gray-500 dark:text-neutral-600 text-xs mb-12 font-mono">
*No developers were harmed in the making of this pricing table. Only caffeine levels were impacted.
</p>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat package.json | head -100

Repository: DevLoversTeam/devlovers.net

Length of output: 115


🏁 Script executed:

fd -e json -e yml -e yaml | grep -i "i18n\|translation\|locale\|intl" | head -20

Repository: DevLoversTeam/devlovers.net

Length of output: 53


🏁 Script executed:

rg "useTranslation|i18n\.|i18next\|FormattedMessage\|gettext" --type tsx --type ts --max-count 20

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

find . -type d -name "locales" -o -name "translations" -o -name "i18n" 2>/dev/null

Repository: DevLoversTeam/devlovers.net

Length of output: 86


🏁 Script executed:

rg "from.*i18n|import.*i18next|import.*react-intl" --type tsx --type ts

Repository: DevLoversTeam/devlovers.net

Length of output: 98


🏁 Script executed:

ls -la frontend/i18n/ 2>/dev/null | head -50

Repository: DevLoversTeam/devlovers.net

Length of output: 361


🏁 Script executed:

find frontend -maxdepth 1 -name "package.json" 2>/dev/null

Repository: DevLoversTeam/devlovers.net

Length of output: 92


🏁 Script executed:

cat frontend/package.json 2>/dev/null | grep -A5 -B5 "i18n\|intl\|translation"

Repository: DevLoversTeam/devlovers.net

Length of output: 382


🏁 Script executed:

rg "useTranslation|i18n\.|i18next" frontend/ --max-count 20

Repository: DevLoversTeam/devlovers.net

Length of output: 3017


🏁 Script executed:

head -50 frontend/components/about/PricingSection.tsx

Repository: DevLoversTeam/devlovers.net

Length of output: 3134


🏁 Script executed:

rg "Invest in your brain|Junior Engineer|Open Source Hero" frontend/ --max-count 5

Repository: DevLoversTeam/devlovers.net

Length of output: 426


Use translation keys for hard-coded UI strings.

This component hard-codes all UI text directly, which will break non-English locales. The project uses next-intl throughout (as seen in BlogGrid, HeroSection, QuizQuestion, Footer, etc.)—PricingSection should follow the same pattern with useTranslations('pricing') for strings like "Invest in your brain," "Junior Engineer," "Open Source Hero," and others.

🤖 Prompt for AI Agents
In `@frontend/components/about/PricingSection.tsx` around lines 30 - 143,
PricingSection currently hard-codes all UI strings; replace them with next-intl
translations by importing and calling useTranslations('pricing') inside the
PricingSection component and swapping each literal (e.g., headings "Invest in
your brain, not our subscriptions.", paragraph copy, plan titles "Junior
Engineer", "Open Source Hero", feature list items like "Unlimited Questions",
buttons "Start Learning", "Support the Project", badge "High Impact", footer
note, and any other visible text) for t('key') lookups using descriptive keys
(e.g., title, subtitle, plan.junior.title, plan.openSource.title,
feature.unlimitedQuestions, cta.startLearning, cta.supportProject,
badge.highImpact, footer.note) so the component reads translations from
useTranslations('pricing') instead of hard-coded strings.

Comment thread frontend/components/about/TopicsSection.tsx
Comment thread frontend/data/about.ts
Comment on lines +44 to +60
try {
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query }),
cache: "no-store" // Завжди свіжі дані
})

const json = await res.json()

if (json.errors) {
console.error("❌ GitHub API Error:", json.errors[0].message)
return []
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files frontend/lib/about/github-sponsors.ts

Repository: DevLoversTeam/devlovers.net

Length of output: 108


🏁 Script executed:

cat -n frontend/lib/about/github-sponsors.ts | head -80

Repository: DevLoversTeam/devlovers.net

Length of output: 3044


🏁 Script executed:

cat -n frontend/lib/about/github-sponsors.ts | tail -20

Repository: DevLoversTeam/devlovers.net

Length of output: 668


Handle non-OK GitHub responses explicitly to avoid silent empty sponsors.

If the token is invalid/expired, GitHub returns 401 (non-200) with a message body (not errors), which currently falls through and logs "Found 0 sponsors." This silently masks authentication failures and makes debugging production issues difficult. Additionally, res.json() can throw on malformed responses. Check res.ok before processing the response and wrap JSON parsing in error handling.

🐛 Proposed fix
-    const res = await fetch("https://api.github.com/graphql", {
+    const res = await fetch("https://api.github.com/graphql", {
       method: "POST",
       headers: {
         Authorization: `Bearer ${token}`,
         "Content-Type": "application/json",
       },
       body: JSON.stringify({ query }),
       cache: "no-store" // Завжди свіжі дані
     })
 
-    const json = await res.json()
+    let json: any
+    try {
+      json = await res.json()
+    } catch (e) {
+      console.error("❌ GitHub API Error: Invalid JSON response", e)
+      return []
+    }
+
+    if (!res.ok) {
+      console.error("❌ GitHub API Error:", res.status, res.statusText, json?.message)
+      return []
+    }
 
-    if (json.errors) {
+    if (json.errors?.length) {
       console.error("❌ GitHub API Error:", json.errors[0].message)
       return []
     }
🤖 Prompt for AI Agents
In `@frontend/lib/about/github-sponsors.ts` around lines 44 - 60, The fetch block
that calls fetch("https://api.github.com/graphql") currently assumes a 200
response and that res.json() succeeds; change it to first check res.ok and, if
false, attempt to read the response text/JSON to extract and log the error
message (including res.status) and return [] for non-OK responses, and wrap the
res.json() call in a try/catch to handle malformed JSON; update the error
handling around the variables res and json (and the surrounding function that
constructs query/token) to ensure authentication failures (401) and parse errors
produce an explicit log and early return rather than falling through to "Found 0
sponsors."

@ViktorSvertoka ViktorSvertoka merged commit be4bdf0 into develop Jan 19, 2026
8 checks passed
@ViktorSvertoka ViktorSvertoka deleted the yd/feat/about/about-us-redesign branch January 19, 2026 05:30
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.

2 participants