(SP: 2) [Frontend] Redesign Home Hero & Add Features Section#319
(SP: 2) [Frontend] Redesign Home Hero & Add Features Section#319ViktorSvertoka merged 7 commits intodevelopfrom
Conversation
- Rename LegacyHeroSection → WelcomeHeroSection - Rename HeroSection → FeaturesHeroSection - Add welcomeDescription translation key to eliminate duplication - Translate all hardcoded text (headings, badges, CTAs) - Improve Ukrainian/Polish translations for better readability - Remove unused legacy components and images
Resolved conflicts in translation files by preserving both welcomeDescription and ogImageAlt keys
✅ Deploy Preview for develop-devlovers ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughReplaces the legacy homepage hero with two sections (WelcomeHeroSection then FeaturesHeroSection), removes legacy hero/code-card components, adds interactive visuals (FlipCardQA, FloatingCode, InteractiveConstellation), rewrites InteractiveCTAButton, adds 3D CSS utilities, updates i18n content, and bumps LinkedIn fallback/display value. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Page as "Page"
participant Welcome as "WelcomeHeroSection"
participant Constell as "InteractiveConstellation"
participant CTA as "InteractiveCTAButton"
participant Features as "FeaturesHeroSection"
participant Flip as "FlipCardQA"
participant Floating as "FloatingCode"
User->>Page: Request homepage
Page->>Welcome: Render WelcomeHeroSection
Welcome->>Constell: Mount particle canvas
Constell->>Constell: Animate, handle mouse
Welcome->>CTA: Render interactive CTA
User->>CTA: Hover / move
CTA->>CTA: Track mouse, animate/parallax
Page->>Features: Render FeaturesHeroSection
Features->>Flip: Mount FlipCardQA
User->>Flip: Interact (click/keys)
Flip->>Flip: Tilt/flip, auto-advance
Features->>Floating: Mount FloatingCode
Floating->>Floating: Typewriter & motion animations
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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: 8
🤖 Fix all issues with AI agents
In `@frontend/components/home/FeaturesHeroSection.tsx`:
- Around line 25-30: The heading concatenation in FeaturesHeroSection.tsx causes
"Ace Your NextTechnical Interview" on sm+ screens because the <br
className="sm:hidden" /> is hidden and the translation
t('featuresHeading.aceYourNext') lacks a trailing space; fix by inserting an
explicit space between the text node and the <span> (e.g. add {" "} or
after {t('featuresHeading.aceYourNext')}) inside the h1, or alternatively add a
trailing space to the EN translation key t('featuresHeading.aceYourNext') in
en.json if you prefer a locale-specific fix.
In `@frontend/components/home/FlipCardQA.tsx`:
- Around line 16-72: The questions array is hardcoded in English (Question
objects: id 1–5, fields question/answer/tip/category) and several UI labels
("Click to reveal"/"Tap to reveal", "Answer", "Pro Tip", "Click again to see
question") are not localized; replace hardcoded strings by wiring next-intl's
useTranslations hook in FlipCardQA.tsx, move each question/answer/tip/category
text to translation keys (e.g.,
flipCard.questions.[id].question/answer/tip/category) and use t('...') when
building the questions array and rendering the UI labels so all displayed
strings come from the t(...) calls rather than literals.
- Around line 119-150: The effect schedules two setTimeouts when progress
reaches 100% (inside useEffect) but only clears the interval `timer`; capture
the timeout IDs (e.g., `flipTimeoutId` and `indexTimeoutId`) when calling
setTimeout and store them in local variables, then clear them with
clearTimeout(...) in the effect cleanup alongside clearInterval(timer) to
prevent state updates after unmount; ensure you still call setIsFlipped,
setCurrentIndex, and setProgress from those timeouts but cancel both timeouts on
cleanup or before scheduling new ones.
- Around line 218-232: The rotateY value is being driven in two
places—style.rotateY (rotateYSpring) and animate.rotateY—which lets animate
override the mouse-tilt spring; update the flip logic so rotateYSpring includes
the flip offset instead of using animate: compute rotateYSpring as rotateYSpring
+ (isFlipped ? 180 : 0) (or set the spring target to 180/0 based on isFlipped)
and remove rotateY from the motion.div animate prop so the tilt (rotateYSpring
and rotateXSpring) and flip (isFlipped) are combined in one spring-driven
source; adjust usage around motion.div, rotateYSpring, rotateXSpring, isFlipped
and handleFlip accordingly.
- Around line 152-170: The global keydown handler in the useEffect is hijacking
Space/Enter/Arrow keys site-wide; remove the window-level listener and instead
attach a scoped onKeyDown to the card element (the focusable motion.div that
already uses tabIndex={0}), using a ref (e.g., cardRef) to verify focus
(document.activeElement === cardRef.current) or simply rely on the event target
being the card before calling e.preventDefault() and invoking setIsFlipped or
setCurrentIndex (which use questions); move the logic from handleKeyDown into
that onKeyDown handler and delete the useEffect-added
window.addEventListener/removeEventListener.
In `@frontend/components/home/FloatingCode.tsx`:
- Around line 77-107: The effect starts timers inside startTyping (typeInterval
and nested setTimeouts) but only cleans up the initial timeout; fix by tracking
all timer IDs in refs (e.g., store the initial timeout, typeInterval, and any
nested timeouts in a mutable ref like timersRef) and clear them all in the
useEffect cleanup using clearInterval/clearTimeout; update startTyping to push
each returned ID into timersRef.current and ensure you clear timersRef.current
and reset relevant state (isTyping/displayedCode) on unmount or dependency
change to prevent state updates after unmount.
In `@frontend/components/home/InteractiveConstellation.tsx`:
- Around line 186-194: Guard against division by zero in the particle-mouse and
particle-center force calculations by ensuring you never divide by dist or
distCenter when they are 0: in the block using mouseRef.current, replace the
direct normalization (dx / dist, dy / dist) with a safe normalization that first
checks dist (e.g., skip applying the force if dist === 0 or clamp dist to a
small epsilon) before updating p.vx and p.vy with (interactionRadius -
dist)/interactionRadius * magneticForce; apply the same guard/clamping to the
code that uses distCenter (the center-force code) so you never compute (dxCenter
/ distCenter) or (dyCenter / distCenter) when distCenter is zero.
In `@frontend/components/home/WelcomeHeroSection.tsx`:
- Around line 48-67: The scroll indicator uses hardcoded white colors which will
be invisible on light backgrounds; update the Tailwind classes on the outer
motion.div, the rounded container (border-...), the animated dot (bg-...), and
the ChevronDown (text-...) to theme-aware classes (e.g. use conditional dark:
and light: variants or neutral color tokens like text-white/50
dark:text-white/50 light:text-gray-600 / border-white/30 dark:border-white/30
light:border-gray-300 / bg-white/70 dark:bg-white/70 light:bg-gray-600) so the
element has sufficient contrast in both light and dark themes and retains the
same visual hierarchy and animations declared in the existing motion.div and
ChevronDown usage.
🧹 Nitpick comments (11)
frontend/components/home/WelcomeHeroBackground.tsx (1)
1-1: UnnecessaryReactimport.With the modern JSX transform (enabled in this project),
import React from 'react'is not needed unless you referenceReactexplicitly — which this file doesn't.🧹 Remove unused import
-import React from 'react'; -frontend/components/home/FloatingCode.tsx (1)
143-148:Math.random()intransitionrecalculates on every render.
duration: 5 + Math.random() * 3inside theanimate/transitionprops produces a new random value each render, potentially causing animation restarts or flicker. Memoize or hoist the random duration per snippet.♻️ Suggested approach — add a stable random duration to each snippet
Extend the
CodeSnippetinterface or compute it once:const snippets: CodeSnippet[] = [ { id: 'react-hook', code: 'useEffect(() => {\n fetchData();\n}, []);', language: 'react', position: { x: '12%', y: '25%' }, rotate: -6, delay: 0, color: '#61DAFB', + floatDuration: 6.2, // pre-computed random value }, // ... same for other snippets ];Then use
snippet.floatDurationinstead of5 + Math.random() * 3in the transition prop.frontend/components/home/InteractiveConstellation.tsx (3)
30-125: Moveiconsmap outside the component.This object of pure drawing functions is recreated on every render. Since the functions capture no component state, hoist it to module scope. This also avoids a missing-dependency lint warning —
iconsis used inside theuseEffect(line 264) but not listed in its dependency array.♻️ Move to module scope
+const icons: Record<IconType, (ctx: CanvasRenderingContext2D, size: number) => void> = { + react: (ctx, size) => { /* ... */ }, + // ... all other icon draw functions +}; + export function InteractiveConstellation() { const canvasRef = useRef<HTMLCanvasElement>(null); // ... - const icons: Record<IconType, ...> = { ... };
179-179: Unused variableparticleColor.
particleColoris assigned on line 179 but never referenced.currentParticleColor(line 234) is used instead. Remove it to avoid confusion.🧹 Remove dead code
- const particleColor = `rgba(${r}, ${g}, ${b}, 0.8)`; - particles.forEach((p, i) => {
253-266: CanvasshadowBluron every particle is expensive.
shadowBlurforces the browser to apply a Gaussian blur per draw call. With ~50 particles per frame, this can degrade performance on lower-end devices. Consider batching icons without per-particle shadow, or using a single post-processing glow pass.frontend/app/globals.css (1)
280-292: Consider using Tailwind CSS v4 built-in 3D utilities instead of custom classes.Tailwind v4 ships with
backface-hidden,backface-visible, andtransform-3d(forpreserve-3d) utilities natively. For perspective, use arbitrary values likeperspective-[1000px]. These custom utility classes may be redundant.frontend/app/[locale]/page.tsx (1)
60-61: Move import to the top of the file with the other imports.The
WelcomeHeroSectionimport is placed betweengenerateMetadataandHome, separated from the other imports at the top. Consolidate it alongside theFeaturesHeroSectionimport on line 3 for consistency and readability.♻️ Proposed fix
import { getTranslations } from 'next-intl/server'; import FeaturesHeroSection from '@/components/home/FeaturesHeroSection'; +import WelcomeHeroSection from '@/components/home/WelcomeHeroSection'; export async function generateMetadata({And remove lines 60-61.
frontend/components/home/FeaturesHeroSection.tsx (2)
36-60: Consider extracting repeated badge markup into a data-driven loop.The three badges share identical DOM structure and differ only in color palette, icon, and translation key. A small array +
.map()would eliminate ~40 lines of duplication.♻️ Sketch
const badges = [ { icon: MessageCircleQuestion, key: 'smartQA' as const, color: 'blue' }, { icon: BrainCircuit, key: 'adaptiveQuizzes' as const, color: 'purple' }, { icon: TrendingUp, key: 'performance' as const, color: 'emerald' }, ] as const; // Then in JSX: {badges.map(({ icon: Icon, key, color }) => ( <div key={key} className={`group relative overflow-hidden rounded-full border border-${color}-200/60 ...`}> {/* Note: Tailwind needs full class names for purging — use a color-class map or safelist */} ... </div> ))}Note: Since Tailwind cannot purge dynamic class concatenation, you'd need a mapping object for the color class strings (e.g.,
colorClasses[color].border, etc.) to keep it purge-safe.
65-87: CTA links look good; consider using lucide icons instead of inline SVGs.The
lucide-reactlibrary is already imported. You could replace the inline SVG arrow (lines 71-73) and lightning bolt (lines 83-84) with<ArrowRight>and<Zap>from lucide-react for consistency with the badge icons above.frontend/components/home/InteractiveCTAButton.tsx (2)
18-18:isFirstRenderis dead state — set but never read.
isFirstRenderis initialized totrue, toggled tofalsein the mount effect, but its value is never consumed anywhere in the component. Remove it to avoid confusion.♻️ Proposed fix
- const [isFirstRender, setIsFirstRender] = useState(true); ... - useEffect(() => { - setIsFirstRender(false); - }, []);Also applies to: 32-34
76-83: ConsiderrequestAnimationFrameinstead ofsetIntervalfor the gradient rotation.The 16ms
setIntervalapproximates 60fps but can drift and isn't synchronized with the browser's paint cycle.requestAnimationFrameis the standard approach for visual animations and will automatically pause when the tab is backgrounded.♻️ Sketch using rAF
useEffect(() => { if (isHovered) { - const interval = setInterval(() => { - rotate.set((rotate.get() + 2) % 360); - }, 16); - return () => clearInterval(interval); + let frameId: number; + const step = () => { + rotate.set((rotate.get() + 2) % 360); + frameId = requestAnimationFrame(step); + }; + frameId = requestAnimationFrame(step); + return () => cancelAnimationFrame(frameId); } }, [isHovered, rotate]);
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@frontend/components/home/InteractiveConstellation.tsx`:
- Line 179: Remove the dead local constant by deleting the unused declaration
const particleColor = `rgba(${r}, ${g}, ${b}, 0.8)` (it is never read); the
component already uses currentParticleColor for per-particle alpha, so remove
the particleColor declaration to avoid unused variable warnings and keep
currentParticleColor as the sole color value.
In `@frontend/messages/en.json`:
- Around line 63-66: The "aceYourNext" value currently contains a trailing space
which is fragile; remove the trailing space from the "aceYourNext" string and
either add the separating space when rendering (concatenate "aceYourNext" + " "
+ "technicalInterview") or consolidate into a single key (e.g.,
"featuresHeading": { "aceYourNextTechnicalInterview": "Ace Your Next Technical
Interview" }) so that rendering does not rely on trailing whitespace.
In `@frontend/messages/uk.json`:
- Around line 63-66: The "aceYourNext" value in featuresHeading contains a
trailing space which makes localization fragile; remove the trailing space from
"aceYourNext" so it reads "Пройди наступну" and ensure any UI that concatenates
featuresHeading.aceYourNext and featuresHeading.technicalInterview inserts
spacing explicitly (or use a single combined string) to mirror the fix applied
to en.json; update the locale entry for featuresHeading (aceYourNext and/or
combine with technicalInterview) accordingly.
🧹 Nitpick comments (5)
frontend/components/home/FloatingCode.tsx (2)
146-161:Math.random()in the render path causes unstable animation parameters.
duration: 5 + Math.random() * 3on line 157 is evaluated on every render. SincedisplayedCodestate updates ~every 50-80 ms during the typing phase, this produces a new random duration each time. While Framer Motion generally won't restart an in-progressanimate, feeding it a constantly changingtransitionobject is wasteful (new object allocation each render) and risks subtle animation glitches if the component re-renders for other reasons mid-cycle.Compute the random duration once (e.g., via
useMemoor include it in theCodeSnippetdata) so the value is stable across renders.♻️ Option A – add to snippet data (simplest)
Add a
floatDurationfield to the snippet objects:interface CodeSnippet { id: string; code: string; language: string; position: { x: string; y: string }; rotate: number; delay: number; color: string; + floatDuration: number; }Precompute it in each snippet entry (e.g.,
floatDuration: 6.2) and reference it in the transition:transition={{ - duration: 5 + Math.random() * 3, + duration: snippet.floatDuration, repeat: Infinity,♻️ Option B – useMemo inside CodeBlock
function CodeBlock({ snippet }: { snippet: CodeSnippet }) { const [displayedCode, setDisplayedCode] = useState(''); const [isTyping, setIsTyping] = useState(false); + const floatDuration = useMemo(() => 5 + Math.random() * 3, []);Then reference
floatDurationin the transition prop.
163-186:displayedCode.split('\n')is computed twice per render.Line 165 splits the string for iteration, then line 176 splits it again inside the
.map()callback (on every iteration) just to check if the current line is the last. This is an unnecessary repeated allocation.♻️ Proposed fix – split once
<pre className="font-mono text-[11px] leading-relaxed text-gray-600 dark:text-gray-400"> <code> - {displayedCode.split('\n').map((line, i) => ( - <div key={i} className="flex min-h-[1.5em]"> + {(() => { + const lines = displayedCode.split('\n'); + return lines.map((line, i) => ( + <div key={i} className="flex min-h-[1.5em]"> <span className="mr-3 w-3 select-none text-right opacity-30 text-xs">{i + 1}</span> <span style={{ color: i === 0 ? snippet.color : 'inherit', textShadow: i === 0 ? `0 0 10px ${snippet.color}40` : 'none', fontWeight: i === 0 ? 600 : 400, }} > {line} - {i === displayedCode.split('\n').length - 1 && ( + {i === lines.length - 1 && ( <motion.span animate={{ opacity: [1, 0, 1] }} transition={{ duration: 0.8, repeat: Infinity }} className="ml-0.5 inline-block h-3 w-1.5 align-middle bg-current opacity-70" /> )} </span> </div> - ))} + )); + })()} </code> </pre>Alternatively, extract
const lines = useMemo(() => displayedCode.split('\n'), [displayedCode]);at the top of the component for cleaner JSX.frontend/components/home/InteractiveConstellation.tsx (2)
30-125: Moveiconsoutside the component or into theuseEffectto avoid recreation on every render.The
iconsrecord is a pure mapping of drawing functions with no dependency on component state or props. Defining it in the component body means it's recreated each render and captured as a stale closure by theuseEffect(which only liststhemeas a dependency). Move it to module scope to fix the stale-closure lint warning and eliminate the per-render allocation.♻️ Suggested approach
+const icons: Record<IconType, (ctx: CanvasRenderingContext2D, size: number) => void> = { + react: (ctx, size) => { /* ... */ }, + // ... all icon entries ... +}; + export function InteractiveConstellation() { const canvasRef = useRef<HTMLCanvasElement>(null); // ... - const icons: Record<IconType, ...> = { ... };
127-329: Theme change re-initializes all particles, causing a visual reset.Because
themeis in theuseEffectdependency array (line 329), every theme toggle destroys and recreates all particles from random positions. Consider reading the theme insidedraw()(you already do thedocument.documentElement.classListcheck on line 174) and removingthemefrom deps, so the animation persists across theme switches.♻️ Suggested approach
- const { theme } = useTheme(); + const { resolvedTheme } = useTheme(); + const themeRef = useRef(resolvedTheme); + useEffect(() => { themeRef.current = resolvedTheme; }, [resolvedTheme]); // ... useEffect(() => { // inside draw(): - const isDark = theme === 'dark' || document.documentElement.classList.contains('dark'); + const isDark = themeRef.current === 'dark'; // ... - }, [theme]); + }, []); // stable — never re-initializesfrontend/components/home/FlipCardQA.tsx (1)
76-76: Consider narrowing theany[]type inuseTransformcallback.The
latestparameter is typed asany[], which bypasses type checking. Framer Motion'suseTransformwith multiple inputs has limited built-in typing, but you can cast more precisely:- const rotateY = useTransform([tiltY, flipRotation], (latest: any[]) => latest[0] + latest[1]); + const rotateY = useTransform([tiltY, flipRotation], (latest: number[]) => latest[0] + latest[1]);
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@frontend/components/home/FlipCardQA.tsx`:
- Around line 25-31: The Next.js color in the categoryColors map (const
categoryColors) is set to '#FFFFFF', which becomes nearly invisible when used to
generate the badge background (`${categoryColor}15`) and border
(`${categoryColor}30`); update the 'Next.js' entry in categoryColors to a darker
value (e.g., '#000000' or a neutral gray) so the generated translucent
background and border are visible on light-mode cards, and run the UI to confirm
contrast for both light and dark themes.
- Around line 341-365: Replace the hardcoded English aria-label in FlipCardQA
(the button rendering inside the questions.map) with a localized string from
useTranslations: add a key like homepage.flipCard.ui.goToQuestion with
placeholder {index} to your locale files (en.json, pl.json, uk.json), then call
the translation function inside the component to compose the aria-label (pass
index + 1) instead of using the template literal `Go to question ${index + 1}`;
keep the rest of the props (key, onClick -> handleNavigate, aria-current, etc.)
unchanged.
In `@frontend/messages/en.json`:
- Line 61: The key ctaVariants.8 currently contains a debug/test string ("Hello,
new commit"); replace it with a meaningful CTA consistent with other variants
(e.g., a short motivational phrase) or remove the key entirely if unused, and
ensure any corresponding translation entries or references that expect
ctaVariants length are updated to avoid missing-text errors; look for the
ctaVariants object in en.json and change the value for "8" accordingly.
🧹 Nitpick comments (5)
frontend/components/home/InteractiveConstellation.tsx (4)
30-125: Moveiconsoutside the component — it's a pure constant recreated every render.This record has no dependency on props, state, or theme, yet it's defined inside the component body and captured by the
useEffectclosure. This causes two issues: (1) it's needlessly recreated on every render, and (2) ESLint'sreact-hooks/exhaustive-depsrule would flag it as a missing dependency of the effect — but adding it would trigger infinite re-runs since the object identity changes each render.Hoist it to module scope to fix both.
♻️ Proposed fix
+'use client'; + +import { useTheme } from 'next-themes'; +import React, { useEffect, useRef } from 'react'; + +// ... interfaces ... + +const icons: Record<IconType, (ctx: CanvasRenderingContext2D, size: number) => void> = { + react: (ctx, size) => { /* ... same ... */ }, + // ... all other icons ... +}; + export function InteractiveConstellation() { const canvasRef = useRef<HTMLCanvasElement>(null); // ... - const icons: Record<IconType, ...> = { ... }; - useEffect(() => { // icons is now a stable module-level reference
144-148: Canvas will appear blurry on HiDPI / Retina displays.The canvas dimensions are set to CSS pixels without accounting for
window.devicePixelRatio. On 2x+ screens, this produces visibly blurred output.♻️ Proposed fix
const resize = () => { - canvas.width = container.clientWidth; - canvas.height = container.clientHeight; + const dpr = window.devicePixelRatio || 1; + canvas.width = container.clientWidth * dpr; + canvas.height = container.clientHeight * dpr; + canvas.style.width = `${container.clientWidth}px`; + canvas.style.height = `${container.clientHeight}px`; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); initParticles(); };With this change, all drawing coordinates remain in CSS-pixel space while the backing buffer is high-resolution. Note that
canvas.width/canvas.heightreferences later in the code (bounds checks, center calc, etc.) would need to use the CSS dimensions (container.clientWidth/clientHeight) instead, or you can store them separately.
210-226: Repulsion loop is O(n²) with redundant pair checks — consider halving iterations.The repulsion loop iterates all
jfrom0..nfor eachi, meaning every pair(i, j)is visited twice. The connection loop at line 270 already optimizes this withj = i + 1. You could apply the same pattern here and apply equal-opposite forces to both particles in a single pass, halving the work per frame.Not urgent at n ≤ 50, but a straightforward improvement if the particle count grows.
127-328:useEffectre-runs on everythemechange, resetting all particles.When
themechanges, the cleanup runs andresize()→initParticles()reinitializes particles at random positions, causing a visible "jump." Consider decoupling the theme read from the effect lifecycle — e.g., store theme-derived colors in a ref so the animation loop picks up changes without restarting.♻️ Sketch
+ const themeRef = useRef(theme); + useEffect(() => { themeRef.current = theme; }, [theme]); useEffect(() => { // ... const draw = () => { - const isDark = theme === 'dark' || ...; + const isDark = themeRef.current === 'dark' || ...; // ... }; // ... - }, [theme]); + }, []); // runs once; theme changes flow through themeReffrontend/components/home/FeaturesHeroSection.tsx (1)
36-60: Consider extracting badge markup into a small helper.The three feature badges share identical structure, differing only in color scheme, icon, and label. A small
FeatureBadgecomponent or a mapped array could reduce the ~24 lines of near-duplicate markup, improving maintainability.♻️ Example refactor
const badges = [ { icon: MessageCircleQuestion, label: 'featureBadges.smartQA', color: 'blue' }, { icon: BrainCircuit, label: 'featureBadges.adaptiveQuizzes', color: 'purple' }, { icon: TrendingUp, label: 'featureBadges.performance', color: 'emerald' }, ] as const; // Then map over badges in JSX: {badges.map(({ icon: Icon, label, color }) => ( <div key={label} className={`group relative overflow-hidden rounded-full border border-${color}-200/60 ...`}> ... <Icon className="h-4 w-4" /> <span className="whitespace-nowrap">{t(label)}</span> </div> ))}Note: Tailwind doesn't support dynamic class names via template literals out of the box — you'd need to use a mapping object or
clsxfor the color variants. This is just illustrative.
| const categoryColors: Record<string, string> = { | ||
| React: '#61DAFB', | ||
| JavaScript: '#F7DF1E', | ||
| 'Node.js': '#339933', | ||
| 'Next.js': '#FFFFFF', | ||
| TypeScript: '#3178C6', | ||
| }; |
There was a problem hiding this comment.
Next.js category color #FFFFFF may be invisible on light backgrounds.
The white color is used for the category badge background (${categoryColor}15) and border (${categoryColor}30). On light-mode cards, these will be nearly invisible since they produce near-white-on-white styling.
Consider using the Next.js brand dark color (#000000) or a neutral gray instead.
🤖 Prompt for AI Agents
In `@frontend/components/home/FlipCardQA.tsx` around lines 25 - 31, The Next.js
color in the categoryColors map (const categoryColors) is set to '#FFFFFF',
which becomes nearly invisible when used to generate the badge background
(`${categoryColor}15`) and border (`${categoryColor}30`); update the 'Next.js'
entry in categoryColors to a darker value (e.g., '#000000' or a neutral gray) so
the generated translucent background and border are visible on light-mode cards,
and run the UI to confirm contrast for both light and dark themes.
| <div className="mt-6 flex justify-center gap-2"> | ||
| {questions.map((_, index) => ( | ||
| <button | ||
| key={index} | ||
| onClick={() => handleNavigate(index)} | ||
| className="relative focus:outline-none focus:ring-2 focus:ring-[var(--accent-primary)] focus:ring-offset-2" | ||
| aria-label={`Go to question ${index + 1}`} | ||
| aria-current={index === currentIndex ? 'true' : 'false'} | ||
| > | ||
| {index === currentIndex ? ( | ||
| <div className="relative h-2 w-8"> | ||
| <div className="absolute inset-0 rounded-full bg-gray-300 dark:bg-gray-700" /> | ||
| <motion.div | ||
| className="absolute inset-0 rounded-full bg-gradient-to-r from-[var(--accent-primary)] to-[var(--accent-hover)]" | ||
| initial={{ width: '0%' }} | ||
| animate={{ width: `${progress}%` }} | ||
| transition={{ duration: 0.05, ease: 'linear' }} | ||
| /> | ||
| </div> | ||
| ) : ( | ||
| <div className="h-2 w-2 rounded-full bg-gray-300 transition-colors hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600" /> | ||
| )} | ||
| </button> | ||
| ))} | ||
| </div> |
There was a problem hiding this comment.
Hardcoded English aria-label on navigation dots breaks i18n.
Line 347: aria-label={`Go to question ${index + 1}`} is hardcoded English while the rest of the component uses useTranslations. This is inconsistent with the PR's goal of full i18n support.
Add a translation key (e.g., flipCard.ui.goToQuestion) and use it here.
🌐 Suggested fix
In en.json (and pl.json, uk.json), add under homepage.flipCard.ui:
"goToQuestion": "Go to question {index}"Then in the component:
- aria-label={`Go to question ${index + 1}`}
+ aria-label={t('ui.goToQuestion', { index: index + 1 })}📝 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.
| <div className="mt-6 flex justify-center gap-2"> | |
| {questions.map((_, index) => ( | |
| <button | |
| key={index} | |
| onClick={() => handleNavigate(index)} | |
| className="relative focus:outline-none focus:ring-2 focus:ring-[var(--accent-primary)] focus:ring-offset-2" | |
| aria-label={`Go to question ${index + 1}`} | |
| aria-current={index === currentIndex ? 'true' : 'false'} | |
| > | |
| {index === currentIndex ? ( | |
| <div className="relative h-2 w-8"> | |
| <div className="absolute inset-0 rounded-full bg-gray-300 dark:bg-gray-700" /> | |
| <motion.div | |
| className="absolute inset-0 rounded-full bg-gradient-to-r from-[var(--accent-primary)] to-[var(--accent-hover)]" | |
| initial={{ width: '0%' }} | |
| animate={{ width: `${progress}%` }} | |
| transition={{ duration: 0.05, ease: 'linear' }} | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="h-2 w-2 rounded-full bg-gray-300 transition-colors hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600" /> | |
| )} | |
| </button> | |
| ))} | |
| </div> | |
| <div className="mt-6 flex justify-center gap-2"> | |
| {questions.map((_, index) => ( | |
| <button | |
| key={index} | |
| onClick={() => handleNavigate(index)} | |
| className="relative focus:outline-none focus:ring-2 focus:ring-[var(--accent-primary)] focus:ring-offset-2" | |
| aria-label={t('ui.goToQuestion', { index: index + 1 })} | |
| aria-current={index === currentIndex ? 'true' : 'false'} | |
| > | |
| {index === currentIndex ? ( | |
| <div className="relative h-2 w-8"> | |
| <div className="absolute inset-0 rounded-full bg-gray-300 dark:bg-gray-700" /> | |
| <motion.div | |
| className="absolute inset-0 rounded-full bg-gradient-to-r from-[var(--accent-primary)] to-[var(--accent-hover)]" | |
| initial={{ width: '0%' }} | |
| animate={{ width: `${progress}%` }} | |
| transition={{ duration: 0.05, ease: 'linear' }} | |
| /> | |
| </div> | |
| ) : ( | |
| <div className="h-2 w-2 rounded-full bg-gray-300 transition-colors hover:bg-gray-400 dark:bg-gray-700 dark:hover:bg-gray-600" /> | |
| )} | |
| </button> | |
| ))} | |
| </div> |
🤖 Prompt for AI Agents
In `@frontend/components/home/FlipCardQA.tsx` around lines 341 - 365, Replace the
hardcoded English aria-label in FlipCardQA (the button rendering inside the
questions.map) with a localized string from useTranslations: add a key like
homepage.flipCard.ui.goToQuestion with placeholder {index} to your locale files
(en.json, pl.json, uk.json), then call the translation function inside the
component to compose the aria-label (pass index + 1) instead of using the
template literal `Go to question ${index + 1}`; keep the rest of the props (key,
onClick -> handleNavigate, aria-current, etc.) unchanged.
| @@ -58,6 +59,61 @@ | |||
| "6": "Brave move, by the way", | |||
| "7": "Okay, now it's happening", | |||
| "8": "Hello, new commit" | |||
There was a problem hiding this comment.
ctaVariants.8 looks like a debug placeholder.
"Hello, new commit" reads like a test string rather than a real CTA variant. If this is shipped to production, users will see it. Replace it with a meaningful motivational phrase consistent with the other variants, or remove it.
🤖 Prompt for AI Agents
In `@frontend/messages/en.json` at line 61, The key ctaVariants.8 currently
contains a debug/test string ("Hello, new commit"); replace it with a meaningful
CTA consistent with other variants (e.g., a short motivational phrase) or remove
the key entirely if unused, and ensure any corresponding translation entries or
references that expect ctaVariants length are updated to avoid missing-text
errors; look for the ctaVariants object in en.json and change the value for "8"
accordingly.
* feat(mobile): improve dashboard, leaderboard & AI helper UX for touch devices - Add touch drag support for AI helper modal and explained terms reorder - Position explain button below selected word on mobile - Show delete/restore buttons always visible on mobile (no hover) - Add user avatar to dashboard profile card (same as leaderboard) - Fix leaderboard page layout - Fix Tailwind v4 canonical class warnings * Added touchcancel listener * (SP: 2) [Frontend] Quiz results dashboard, review cache fix, UX improvements (#317) * (SP: 3) [Backend] add internal janitor (jobs 1-4), claim/lease + runbook (G0-G6) (#318) * (SP: 2) [Frontend] Redesign Home Hero & Add Features Section (#319) * refactor(home): rename hero sections and add complete i18n support - Rename LegacyHeroSection → WelcomeHeroSection - Rename HeroSection → FeaturesHeroSection - Add welcomeDescription translation key to eliminate duplication - Translate all hardcoded text (headings, badges, CTAs) - Improve Ukrainian/Polish translations for better readability - Remove unused legacy components and images * feat(about): update LinkedIn follower count to reflect current stat (1.5k+) * refactor(home): implement i18n for FlipCardQA & fix memory leaks * fix(home): resolve rotateY conflict & scope keyboard events in FlipCardQA * fix(home): resolve all issues * chore(home): cleanup comments, remove dead code & fix trailing spaces * (SP: 2) [Frontend] Quiz UX improvements: violations counter, breadcrumbs, status badges (#320) * feat(quiz): add guest warning before start and bot protection Guest warning: show login/signup/continue buttons for unauthenticated users on quiz rules screen before starting. Bot protection: multi-attempt verification via Redis - each question can only be verified once per user per attempt. Keys use dynamic TTL matching quiz time limit and are cleared on retake. Additional fixes: - Footer flash on quiz navigation (added loading.tsx, eliminated redirect) - Renamed QaLoader to Loader for reuse across pages - React compiler purity errors (crypto.getRandomValues in handlers) - Start button disabled after retake (isStarting not reset) * refactor(quiz): PR review feedback - Extract shared resolveRequestIdentifier() helper to eliminate duplicated auth/IP resolution logic in route.ts and actions/quiz.ts - Return null instead of 'unknown' when identifier unresolvable, skip verification tracking for unidentifiable users - Cap Redis TTL with MAX_TTL (3600s) to prevent client-supplied timeLimitSeconds from persisting keys indefinitely - Add locale prefix to returnTo paths in guest warning links - Replace nested Button inside Link with styled Link to fix invalid HTML (interactive element nesting) * fix(quiz): fall through to IP when auth cookie is expired/invalid * feat(quiz): add quiz results dashboard and review page - Add quiz history section to dashboard with last attempt per quiz - Add review page showing incorrect questions with explanations - Add collapsible cards with expand/collapse all toggle - Add "Review Mistakes" button on quiz result screen - Add category icons to quiz page and review page headers - Add BookOpen icon to explanation block in QuizQuestion - Update guest message to mention error review benefit - Add i18n translations (en/uk/pl) for all new features * fix(quiz): scroll to next button on answer reveal, scope review cache by userId * fix(quiz): restore type imports and userId cache key after merge conflict * fix: restore type imports, sync @swc/helpers, fix indentation after merge * feat(quiz): add violations counter UI, fix disqualification threshold - Add ViolationsCounter component with color escalation (green/yellow/red) - Sticky top bar keeps counter visible on scroll (mobile/tablet) - Add i18n counter keys for en/uk/pl with ICU plural forms - Fix threshold bug: violations warning now triggers at 4+ (was 3+) to match actual integrity score calculation (100 - violations * 10 < 70) * fix(quiz): fix points mismatch between leaderboard and dashboard Dashboard showed raw pointsEarned from last quiz_attempt, while leaderboard summed improvement deltas from point_transactions. Additionally, orphaned transactions from re-seeded quizzes inflated leaderboard totals (12 rows, 83 ghost points cleaned up in DB). - Dashboard query now joins point_transactions to show actual awarded points per quiz instead of raw attempt score - Leaderboard query filters out orphaned transactions where the source attempt no longer exists in quiz_attempts * OBfix(quiz): fix points mismatch, consistent status badges, mobile UX Dashboard showed raw pointsEarned from last attempt while leaderboard summed improvement deltas from point_transactions. Orphaned transactions from re-seeded quizzes inflated leaderboard totals (cleaned up in DB). - Dashboard query joins point_transactions for actual awarded points - Leaderboard query filters orphaned transactions (source_id not in quiz_attempts) - Quiz cards use 3-level badges (Mastered/Review/Study) matching dashboard - Mobile quiz results show dash for zero points, added chevron indicator * fix(quiz): add breadcrumbs to review page, fix recommendation tautology * Header UX polish, quiz highlight fix, Blog button styling, shop i18n product descriptions (#322) * Header UX: reorder languages, swap controls, fix quiz highlight, style Blog button * shop i18n product descriptions * (SP: 1) [Frontend] Q&A: Next.js tab states + faster loader start (#324) * fix(qa): align Next.js tab states and speed up loader startup * feat(home,qa): improve home snap flow and add configurable Q&A page size * fix(i18n,qa,seed): address review issues for locale handling and pagination state * (SP: 1) [Frontend] Align quiz result messages with status badges, fix locale switch on result page (#325) * feat(quiz): add guest warning before start and bot protection Guest warning: show login/signup/continue buttons for unauthenticated users on quiz rules screen before starting. Bot protection: multi-attempt verification via Redis - each question can only be verified once per user per attempt. Keys use dynamic TTL matching quiz time limit and are cleared on retake. Additional fixes: - Footer flash on quiz navigation (added loading.tsx, eliminated redirect) - Renamed QaLoader to Loader for reuse across pages - React compiler purity errors (crypto.getRandomValues in handlers) - Start button disabled after retake (isStarting not reset) * refactor(quiz): PR review feedback - Extract shared resolveRequestIdentifier() helper to eliminate duplicated auth/IP resolution logic in route.ts and actions/quiz.ts - Return null instead of 'unknown' when identifier unresolvable, skip verification tracking for unidentifiable users - Cap Redis TTL with MAX_TTL (3600s) to prevent client-supplied timeLimitSeconds from persisting keys indefinitely - Add locale prefix to returnTo paths in guest warning links - Replace nested Button inside Link with styled Link to fix invalid HTML (interactive element nesting) * fix(quiz): fall through to IP when auth cookie is expired/invalid * feat(quiz): add quiz results dashboard and review page - Add quiz history section to dashboard with last attempt per quiz - Add review page showing incorrect questions with explanations - Add collapsible cards with expand/collapse all toggle - Add "Review Mistakes" button on quiz result screen - Add category icons to quiz page and review page headers - Add BookOpen icon to explanation block in QuizQuestion - Update guest message to mention error review benefit - Add i18n translations (en/uk/pl) for all new features * fix(quiz): scroll to next button on answer reveal, scope review cache by userId * fix(quiz): restore type imports and userId cache key after merge conflict * fix: restore type imports, sync @swc/helpers, fix indentation after merge * feat(quiz): add violations counter UI, fix disqualification threshold - Add ViolationsCounter component with color escalation (green/yellow/red) - Sticky top bar keeps counter visible on scroll (mobile/tablet) - Add i18n counter keys for en/uk/pl with ICU plural forms - Fix threshold bug: violations warning now triggers at 4+ (was 3+) to match actual integrity score calculation (100 - violations * 10 < 70) * fix(quiz): fix points mismatch between leaderboard and dashboard Dashboard showed raw pointsEarned from last quiz_attempt, while leaderboard summed improvement deltas from point_transactions. Additionally, orphaned transactions from re-seeded quizzes inflated leaderboard totals (12 rows, 83 ghost points cleaned up in DB). - Dashboard query now joins point_transactions to show actual awarded points per quiz instead of raw attempt score - Leaderboard query filters out orphaned transactions where the source attempt no longer exists in quiz_attempts * OBfix(quiz): fix points mismatch, consistent status badges, mobile UX Dashboard showed raw pointsEarned from last attempt while leaderboard summed improvement deltas from point_transactions. Orphaned transactions from re-seeded quizzes inflated leaderboard totals (cleaned up in DB). - Dashboard query joins point_transactions for actual awarded points - Leaderboard query filters orphaned transactions (source_id not in quiz_attempts) - Quiz cards use 3-level badges (Mastered/Review/Study) matching dashboard - Mobile quiz results show dash for zero points, added chevron indicator * fix(quiz): add breadcrumbs to review page, fix recommendation tautology * fix(quiz): align result messages with status badges, persist result on locale switch * chore(release): v1.0.0 --------- Co-authored-by: tetiana zorii <tanyusha.zoriy@gmail.com> Co-authored-by: Lesia Soloviova <106915140+LesiaUKR@users.noreply.github.com> Co-authored-by: liudmylasovetovs <127711697+liudmylasovetovs@users.noreply.github.com> Co-authored-by: Yevhenii Datsenko <134847096+yevheniidatsenko@users.noreply.github.com> Co-authored-by: Tetiana Zorii <131365289+TiZorii@users.noreply.github.com>
* feat(mobile): improve dashboard, leaderboard & AI helper UX for touch devices - Add touch drag support for AI helper modal and explained terms reorder - Position explain button below selected word on mobile - Show delete/restore buttons always visible on mobile (no hover) - Add user avatar to dashboard profile card (same as leaderboard) - Fix leaderboard page layout - Fix Tailwind v4 canonical class warnings * Added touchcancel listener * (SP: 2) [Frontend] Quiz results dashboard, review cache fix, UX improvements (#317) * (SP: 3) [Backend] add internal janitor (jobs 1-4), claim/lease + runbook (G0-G6) (#318) * (SP: 2) [Frontend] Redesign Home Hero & Add Features Section (#319) * refactor(home): rename hero sections and add complete i18n support - Rename LegacyHeroSection → WelcomeHeroSection - Rename HeroSection → FeaturesHeroSection - Add welcomeDescription translation key to eliminate duplication - Translate all hardcoded text (headings, badges, CTAs) - Improve Ukrainian/Polish translations for better readability - Remove unused legacy components and images * feat(about): update LinkedIn follower count to reflect current stat (1.5k+) * refactor(home): implement i18n for FlipCardQA & fix memory leaks * fix(home): resolve rotateY conflict & scope keyboard events in FlipCardQA * fix(home): resolve all issues * chore(home): cleanup comments, remove dead code & fix trailing spaces * (SP: 2) [Frontend] Quiz UX improvements: violations counter, breadcrumbs, status badges (#320) * feat(quiz): add guest warning before start and bot protection Guest warning: show login/signup/continue buttons for unauthenticated users on quiz rules screen before starting. Bot protection: multi-attempt verification via Redis - each question can only be verified once per user per attempt. Keys use dynamic TTL matching quiz time limit and are cleared on retake. Additional fixes: - Footer flash on quiz navigation (added loading.tsx, eliminated redirect) - Renamed QaLoader to Loader for reuse across pages - React compiler purity errors (crypto.getRandomValues in handlers) - Start button disabled after retake (isStarting not reset) * refactor(quiz): PR review feedback - Extract shared resolveRequestIdentifier() helper to eliminate duplicated auth/IP resolution logic in route.ts and actions/quiz.ts - Return null instead of 'unknown' when identifier unresolvable, skip verification tracking for unidentifiable users - Cap Redis TTL with MAX_TTL (3600s) to prevent client-supplied timeLimitSeconds from persisting keys indefinitely - Add locale prefix to returnTo paths in guest warning links - Replace nested Button inside Link with styled Link to fix invalid HTML (interactive element nesting) * fix(quiz): fall through to IP when auth cookie is expired/invalid * feat(quiz): add quiz results dashboard and review page - Add quiz history section to dashboard with last attempt per quiz - Add review page showing incorrect questions with explanations - Add collapsible cards with expand/collapse all toggle - Add "Review Mistakes" button on quiz result screen - Add category icons to quiz page and review page headers - Add BookOpen icon to explanation block in QuizQuestion - Update guest message to mention error review benefit - Add i18n translations (en/uk/pl) for all new features * fix(quiz): scroll to next button on answer reveal, scope review cache by userId * fix(quiz): restore type imports and userId cache key after merge conflict * fix: restore type imports, sync @swc/helpers, fix indentation after merge * feat(quiz): add violations counter UI, fix disqualification threshold - Add ViolationsCounter component with color escalation (green/yellow/red) - Sticky top bar keeps counter visible on scroll (mobile/tablet) - Add i18n counter keys for en/uk/pl with ICU plural forms - Fix threshold bug: violations warning now triggers at 4+ (was 3+) to match actual integrity score calculation (100 - violations * 10 < 70) * fix(quiz): fix points mismatch between leaderboard and dashboard Dashboard showed raw pointsEarned from last quiz_attempt, while leaderboard summed improvement deltas from point_transactions. Additionally, orphaned transactions from re-seeded quizzes inflated leaderboard totals (12 rows, 83 ghost points cleaned up in DB). - Dashboard query now joins point_transactions to show actual awarded points per quiz instead of raw attempt score - Leaderboard query filters out orphaned transactions where the source attempt no longer exists in quiz_attempts * OBfix(quiz): fix points mismatch, consistent status badges, mobile UX Dashboard showed raw pointsEarned from last attempt while leaderboard summed improvement deltas from point_transactions. Orphaned transactions from re-seeded quizzes inflated leaderboard totals (cleaned up in DB). - Dashboard query joins point_transactions for actual awarded points - Leaderboard query filters orphaned transactions (source_id not in quiz_attempts) - Quiz cards use 3-level badges (Mastered/Review/Study) matching dashboard - Mobile quiz results show dash for zero points, added chevron indicator * fix(quiz): add breadcrumbs to review page, fix recommendation tautology * Header UX polish, quiz highlight fix, Blog button styling, shop i18n product descriptions (#322) * Header UX: reorder languages, swap controls, fix quiz highlight, style Blog button * shop i18n product descriptions * (SP: 1) [Frontend] Q&A: Next.js tab states + faster loader start (#324) * fix(qa): align Next.js tab states and speed up loader startup * feat(home,qa): improve home snap flow and add configurable Q&A page size * fix(i18n,qa,seed): address review issues for locale handling and pagination state * (SP: 1) [Frontend] Align quiz result messages with status badges, fix locale switch on result page (#325) * feat(quiz): add guest warning before start and bot protection Guest warning: show login/signup/continue buttons for unauthenticated users on quiz rules screen before starting. Bot protection: multi-attempt verification via Redis - each question can only be verified once per user per attempt. Keys use dynamic TTL matching quiz time limit and are cleared on retake. Additional fixes: - Footer flash on quiz navigation (added loading.tsx, eliminated redirect) - Renamed QaLoader to Loader for reuse across pages - React compiler purity errors (crypto.getRandomValues in handlers) - Start button disabled after retake (isStarting not reset) * refactor(quiz): PR review feedback - Extract shared resolveRequestIdentifier() helper to eliminate duplicated auth/IP resolution logic in route.ts and actions/quiz.ts - Return null instead of 'unknown' when identifier unresolvable, skip verification tracking for unidentifiable users - Cap Redis TTL with MAX_TTL (3600s) to prevent client-supplied timeLimitSeconds from persisting keys indefinitely - Add locale prefix to returnTo paths in guest warning links - Replace nested Button inside Link with styled Link to fix invalid HTML (interactive element nesting) * fix(quiz): fall through to IP when auth cookie is expired/invalid * feat(quiz): add quiz results dashboard and review page - Add quiz history section to dashboard with last attempt per quiz - Add review page showing incorrect questions with explanations - Add collapsible cards with expand/collapse all toggle - Add "Review Mistakes" button on quiz result screen - Add category icons to quiz page and review page headers - Add BookOpen icon to explanation block in QuizQuestion - Update guest message to mention error review benefit - Add i18n translations (en/uk/pl) for all new features * fix(quiz): scroll to next button on answer reveal, scope review cache by userId * fix(quiz): restore type imports and userId cache key after merge conflict * fix: restore type imports, sync @swc/helpers, fix indentation after merge * feat(quiz): add violations counter UI, fix disqualification threshold - Add ViolationsCounter component with color escalation (green/yellow/red) - Sticky top bar keeps counter visible on scroll (mobile/tablet) - Add i18n counter keys for en/uk/pl with ICU plural forms - Fix threshold bug: violations warning now triggers at 4+ (was 3+) to match actual integrity score calculation (100 - violations * 10 < 70) * fix(quiz): fix points mismatch between leaderboard and dashboard Dashboard showed raw pointsEarned from last quiz_attempt, while leaderboard summed improvement deltas from point_transactions. Additionally, orphaned transactions from re-seeded quizzes inflated leaderboard totals (12 rows, 83 ghost points cleaned up in DB). - Dashboard query now joins point_transactions to show actual awarded points per quiz instead of raw attempt score - Leaderboard query filters out orphaned transactions where the source attempt no longer exists in quiz_attempts * OBfix(quiz): fix points mismatch, consistent status badges, mobile UX Dashboard showed raw pointsEarned from last attempt while leaderboard summed improvement deltas from point_transactions. Orphaned transactions from re-seeded quizzes inflated leaderboard totals (cleaned up in DB). - Dashboard query joins point_transactions for actual awarded points - Leaderboard query filters orphaned transactions (source_id not in quiz_attempts) - Quiz cards use 3-level badges (Mastered/Review/Study) matching dashboard - Mobile quiz results show dash for zero points, added chevron indicator * fix(quiz): add breadcrumbs to review page, fix recommendation tautology * fix(quiz): align result messages with status badges, persist result on locale switch * chore(release): v1.0.0 * feat(jpg): add images for shop --------- Co-authored-by: tetiana zorii <tanyusha.zoriy@gmail.com> Co-authored-by: Lesia Soloviova <106915140+LesiaUKR@users.noreply.github.com> Co-authored-by: liudmylasovetovs <127711697+liudmylasovetovs@users.noreply.github.com> Co-authored-by: Yevhenii Datsenko <134847096+yevheniidatsenko@users.noreply.github.com> Co-authored-by: Tetiana Zorii <131365289+TiZorii@users.noreply.github.com>
* feat(mobile): improve dashboard, leaderboard & AI helper UX for touch devices - Add touch drag support for AI helper modal and explained terms reorder - Position explain button below selected word on mobile - Show delete/restore buttons always visible on mobile (no hover) - Add user avatar to dashboard profile card (same as leaderboard) - Fix leaderboard page layout - Fix Tailwind v4 canonical class warnings * Added touchcancel listener * (SP: 2) [Frontend] Quiz results dashboard, review cache fix, UX improvements (#317) * (SP: 3) [Backend] add internal janitor (jobs 1-4), claim/lease + runbook (G0-G6) (#318) * (SP: 2) [Frontend] Redesign Home Hero & Add Features Section (#319) * refactor(home): rename hero sections and add complete i18n support - Rename LegacyHeroSection → WelcomeHeroSection - Rename HeroSection → FeaturesHeroSection - Add welcomeDescription translation key to eliminate duplication - Translate all hardcoded text (headings, badges, CTAs) - Improve Ukrainian/Polish translations for better readability - Remove unused legacy components and images * feat(about): update LinkedIn follower count to reflect current stat (1.5k+) * refactor(home): implement i18n for FlipCardQA & fix memory leaks * fix(home): resolve rotateY conflict & scope keyboard events in FlipCardQA * fix(home): resolve all issues * chore(home): cleanup comments, remove dead code & fix trailing spaces * (SP: 2) [Frontend] Quiz UX improvements: violations counter, breadcrumbs, status badges (#320) * feat(quiz): add guest warning before start and bot protection Guest warning: show login/signup/continue buttons for unauthenticated users on quiz rules screen before starting. Bot protection: multi-attempt verification via Redis - each question can only be verified once per user per attempt. Keys use dynamic TTL matching quiz time limit and are cleared on retake. Additional fixes: - Footer flash on quiz navigation (added loading.tsx, eliminated redirect) - Renamed QaLoader to Loader for reuse across pages - React compiler purity errors (crypto.getRandomValues in handlers) - Start button disabled after retake (isStarting not reset) * refactor(quiz): PR review feedback - Extract shared resolveRequestIdentifier() helper to eliminate duplicated auth/IP resolution logic in route.ts and actions/quiz.ts - Return null instead of 'unknown' when identifier unresolvable, skip verification tracking for unidentifiable users - Cap Redis TTL with MAX_TTL (3600s) to prevent client-supplied timeLimitSeconds from persisting keys indefinitely - Add locale prefix to returnTo paths in guest warning links - Replace nested Button inside Link with styled Link to fix invalid HTML (interactive element nesting) * fix(quiz): fall through to IP when auth cookie is expired/invalid * feat(quiz): add quiz results dashboard and review page - Add quiz history section to dashboard with last attempt per quiz - Add review page showing incorrect questions with explanations - Add collapsible cards with expand/collapse all toggle - Add "Review Mistakes" button on quiz result screen - Add category icons to quiz page and review page headers - Add BookOpen icon to explanation block in QuizQuestion - Update guest message to mention error review benefit - Add i18n translations (en/uk/pl) for all new features * fix(quiz): scroll to next button on answer reveal, scope review cache by userId * fix(quiz): restore type imports and userId cache key after merge conflict * fix: restore type imports, sync @swc/helpers, fix indentation after merge * feat(quiz): add violations counter UI, fix disqualification threshold - Add ViolationsCounter component with color escalation (green/yellow/red) - Sticky top bar keeps counter visible on scroll (mobile/tablet) - Add i18n counter keys for en/uk/pl with ICU plural forms - Fix threshold bug: violations warning now triggers at 4+ (was 3+) to match actual integrity score calculation (100 - violations * 10 < 70) * fix(quiz): fix points mismatch between leaderboard and dashboard Dashboard showed raw pointsEarned from last quiz_attempt, while leaderboard summed improvement deltas from point_transactions. Additionally, orphaned transactions from re-seeded quizzes inflated leaderboard totals (12 rows, 83 ghost points cleaned up in DB). - Dashboard query now joins point_transactions to show actual awarded points per quiz instead of raw attempt score - Leaderboard query filters out orphaned transactions where the source attempt no longer exists in quiz_attempts * OBfix(quiz): fix points mismatch, consistent status badges, mobile UX Dashboard showed raw pointsEarned from last attempt while leaderboard summed improvement deltas from point_transactions. Orphaned transactions from re-seeded quizzes inflated leaderboard totals (cleaned up in DB). - Dashboard query joins point_transactions for actual awarded points - Leaderboard query filters orphaned transactions (source_id not in quiz_attempts) - Quiz cards use 3-level badges (Mastered/Review/Study) matching dashboard - Mobile quiz results show dash for zero points, added chevron indicator * fix(quiz): add breadcrumbs to review page, fix recommendation tautology * Header UX polish, quiz highlight fix, Blog button styling, shop i18n product descriptions (#322) * Header UX: reorder languages, swap controls, fix quiz highlight, style Blog button * shop i18n product descriptions * (SP: 1) [Frontend] Q&A: Next.js tab states + faster loader start (#324) * fix(qa): align Next.js tab states and speed up loader startup * feat(home,qa): improve home snap flow and add configurable Q&A page size * fix(i18n,qa,seed): address review issues for locale handling and pagination state * (SP: 1) [Frontend] Align quiz result messages with status badges, fix locale switch on result page (#325) * feat(quiz): add guest warning before start and bot protection Guest warning: show login/signup/continue buttons for unauthenticated users on quiz rules screen before starting. Bot protection: multi-attempt verification via Redis - each question can only be verified once per user per attempt. Keys use dynamic TTL matching quiz time limit and are cleared on retake. Additional fixes: - Footer flash on quiz navigation (added loading.tsx, eliminated redirect) - Renamed QaLoader to Loader for reuse across pages - React compiler purity errors (crypto.getRandomValues in handlers) - Start button disabled after retake (isStarting not reset) * refactor(quiz): PR review feedback - Extract shared resolveRequestIdentifier() helper to eliminate duplicated auth/IP resolution logic in route.ts and actions/quiz.ts - Return null instead of 'unknown' when identifier unresolvable, skip verification tracking for unidentifiable users - Cap Redis TTL with MAX_TTL (3600s) to prevent client-supplied timeLimitSeconds from persisting keys indefinitely - Add locale prefix to returnTo paths in guest warning links - Replace nested Button inside Link with styled Link to fix invalid HTML (interactive element nesting) * fix(quiz): fall through to IP when auth cookie is expired/invalid * feat(quiz): add quiz results dashboard and review page - Add quiz history section to dashboard with last attempt per quiz - Add review page showing incorrect questions with explanations - Add collapsible cards with expand/collapse all toggle - Add "Review Mistakes" button on quiz result screen - Add category icons to quiz page and review page headers - Add BookOpen icon to explanation block in QuizQuestion - Update guest message to mention error review benefit - Add i18n translations (en/uk/pl) for all new features * fix(quiz): scroll to next button on answer reveal, scope review cache by userId * fix(quiz): restore type imports and userId cache key after merge conflict * fix: restore type imports, sync @swc/helpers, fix indentation after merge * feat(quiz): add violations counter UI, fix disqualification threshold - Add ViolationsCounter component with color escalation (green/yellow/red) - Sticky top bar keeps counter visible on scroll (mobile/tablet) - Add i18n counter keys for en/uk/pl with ICU plural forms - Fix threshold bug: violations warning now triggers at 4+ (was 3+) to match actual integrity score calculation (100 - violations * 10 < 70) * fix(quiz): fix points mismatch between leaderboard and dashboard Dashboard showed raw pointsEarned from last quiz_attempt, while leaderboard summed improvement deltas from point_transactions. Additionally, orphaned transactions from re-seeded quizzes inflated leaderboard totals (12 rows, 83 ghost points cleaned up in DB). - Dashboard query now joins point_transactions to show actual awarded points per quiz instead of raw attempt score - Leaderboard query filters out orphaned transactions where the source attempt no longer exists in quiz_attempts * OBfix(quiz): fix points mismatch, consistent status badges, mobile UX Dashboard showed raw pointsEarned from last attempt while leaderboard summed improvement deltas from point_transactions. Orphaned transactions from re-seeded quizzes inflated leaderboard totals (cleaned up in DB). - Dashboard query joins point_transactions for actual awarded points - Leaderboard query filters orphaned transactions (source_id not in quiz_attempts) - Quiz cards use 3-level badges (Mastered/Review/Study) matching dashboard - Mobile quiz results show dash for zero points, added chevron indicator * fix(quiz): add breadcrumbs to review page, fix recommendation tautology * fix(quiz): align result messages with status badges, persist result on locale switch * chore(release): v1.0.0 * feat(jpg): add images for shop * (SP: 3) [Shop][Monobank] Janitor map + internal janitor endpoint stub + status UX + security/obs + J test gate (#328) * (SP: 3) [Backend] add internal janitor (jobs 1-4), claim/lease + runbook (G0-G6) * (SP: 3) [Backend] add provider selector, fix payments gating, i18n checkout errors * Add shop category images to public * (SP: 3) [Shop][Monobank] I1 structured logging: codes + logging safety checks * (SP: 3) [Shop][Monobank] Fail-closed non-browser origin posture for webhook + janitor (ORIGIN_BLOCKED) * (SP: 3) [Shop][Monobank] [Shop][Monobank] J gate: add orders status ownership test and pass all pre-prod invariants * (SP: 3) [Shop][Monobank] review fixes (tests, logging, success UI) * (SP: 1) [Shop][Monobank] Tighten webhook log-code typing; harden DB tests; minor security/log/UI cleanups * (SP: 1) [Shop][Monobank] harden Monobank webhook (origin/PII-safe logs) and remove duplicate sha256 hashing * (SP:2) [Frontend] Fix duplicated Q&A items after content updates (#330) * fix(qa): prevent duplicate questions and improve cache invalidation * fix(qa): keep pagination totals consistent after deduplication * (SP: 1) [Frontend] Integrate online users counter popup and fix header (#331) * feat(home): add online users counter + fix header breakpoint * deleted scrollY in OnlineCounterPopup * fixed fetch in OnlineCounterPopup * Bug/fix qa (#332) * fix(qa): prevent duplicate questions and improve cache invalidation * fix(qa): keep pagination totals consistent after deduplication * fix(qa): paginate by unique questions and bump cache namespace * chore(release): v1.0.1 --------- Co-authored-by: tetiana zorii <tanyusha.zoriy@gmail.com> Co-authored-by: Lesia Soloviova <106915140+LesiaUKR@users.noreply.github.com> Co-authored-by: liudmylasovetovs <127711697+liudmylasovetovs@users.noreply.github.com> Co-authored-by: Yevhenii Datsenko <134847096+yevheniidatsenko@users.noreply.github.com> Co-authored-by: Tetiana Zorii <131365289+TiZorii@users.noreply.github.com> Co-authored-by: Yuliia Nazymko <122815071+YNazymko12@users.noreply.github.com>
Description
Redesigned the main landing page to include a new "Features" section and improved the "Welcome" hero area with better visuals and full localization.
Changes
Database Changes (if applicable)
How Has This Been Tested?
Checklist
Before submitting
Reviewers
Summary by CodeRabbit
New Features
Updates
Removed