(SP: 3) [Frontend] Complete About Page redesign: add topics, sponsors, and updated stats#158
Conversation
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughAbout 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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
40sduration, while the vertical marquee (line 224) uses a CSS variable--durationwith 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 (since100vwincludes scrollbar width on some browsers). Consider testing on Windows/Linux where scrollbars are typically always visible.A safer alternative is using
w-screenwithoverflow-x-hiddenon a parent, or using CSScalc(100vw - var(--scrollbar-width)).frontend/components/about/TopicsSection.tsx (1)
47-47: Add proper TypeScript type fortopicparameter.Using
anytype loses type safety. Define or import aTopictype 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.
displaySponsorsis assigned directly fromsponsorswith no transformation. Consider usingsponsorsdirectly.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.revalidateoption on the fetch call is redundant since the entire function is already wrapped inunstable_cachewithrevalidate: 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 usingPromise.allto 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
anytype 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
MobileStatItemat 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 globalinjects 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
anyloses type safety. The props should match the structure fromTESTIMONIALSinfrontend/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 avoidany.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))
| <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'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> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat package.json | head -100Repository: DevLoversTeam/devlovers.net
Length of output: 115
🏁 Script executed:
fd -e json -e yml -e yaml | grep -i "i18n\|translation\|locale\|intl" | head -20Repository: DevLoversTeam/devlovers.net
Length of output: 53
🏁 Script executed:
rg "useTranslation|i18n\.|i18next\|FormattedMessage\|gettext" --type tsx --type ts --max-count 20Repository: DevLoversTeam/devlovers.net
Length of output: 98
🏁 Script executed:
find . -type d -name "locales" -o -name "translations" -o -name "i18n" 2>/dev/nullRepository: DevLoversTeam/devlovers.net
Length of output: 86
🏁 Script executed:
rg "from.*i18n|import.*i18next|import.*react-intl" --type tsx --type tsRepository: DevLoversTeam/devlovers.net
Length of output: 98
🏁 Script executed:
ls -la frontend/i18n/ 2>/dev/null | head -50Repository: DevLoversTeam/devlovers.net
Length of output: 361
🏁 Script executed:
find frontend -maxdepth 1 -name "package.json" 2>/dev/nullRepository: 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 20Repository: DevLoversTeam/devlovers.net
Length of output: 3017
🏁 Script executed:
head -50 frontend/components/about/PricingSection.tsxRepository: DevLoversTeam/devlovers.net
Length of output: 3134
🏁 Script executed:
rg "Invest in your brain|Junior Engineer|Open Source Hero" frontend/ --max-count 5Repository: 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.
| 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 [] | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files frontend/lib/about/github-sponsors.tsRepository: DevLoversTeam/devlovers.net
Length of output: 108
🏁 Script executed:
cat -n frontend/lib/about/github-sponsors.ts | head -80Repository: DevLoversTeam/devlovers.net
Length of output: 3044
🏁 Script executed:
cat -n frontend/lib/about/github-sponsors.ts | tail -20Repository: 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."
… add TS types, and fix configuration
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
Database Changes (if applicable)
How Has This Been Tested?
Screenshots (if applicable)
Checklist
Before submitting
Reviewers
Summary by CodeRabbit
New Features
UI/UX Improvements
Chores
✏️ Tip: You can customize this high-level summary in your review settings.