From daeda48187794c8db11c7c5a059fc40f5a8a11d2 Mon Sep 17 00:00:00 2001 From: Viktor Svertoka Date: Tue, 20 Jan 2026 15:41:37 +0200 Subject: [PATCH 1/5] feat(qa): refresh tabs, pagination, and background --- frontend/app/[locale]/q&a/page.tsx | 31 +++++-- frontend/components/about/HeroSection.tsx | 47 ++--------- frontend/components/q&a/Pagination.tsx | 65 ++++++++++++--- frontend/components/q&a/QaSection.tsx | 74 +++++++---------- frontend/components/q&a/QaTabButton.tsx | 55 ++++++++++++ frontend/components/q&a/useQaTabs.ts | 71 +++++----------- .../shared/DynamicGridBackground.tsx | 48 +++++++++++ frontend/data/qaTabs.ts | 83 +++++++++++++++++++ frontend/messages/en.json | 2 + frontend/messages/pl.json | 2 + frontend/messages/uk.json | 2 + 11 files changed, 326 insertions(+), 154 deletions(-) create mode 100644 frontend/components/q&a/QaTabButton.tsx create mode 100644 frontend/components/shared/DynamicGridBackground.tsx create mode 100644 frontend/data/qaTabs.ts diff --git a/frontend/app/[locale]/q&a/page.tsx b/frontend/app/[locale]/q&a/page.tsx index f0bf623a..935361cb 100644 --- a/frontend/app/[locale]/q&a/page.tsx +++ b/frontend/app/[locale]/q&a/page.tsx @@ -1,6 +1,7 @@ import { Suspense } from 'react'; import { getTranslations } from 'next-intl/server'; import QaSection from '@/components/q&a/QaSection'; +import { DynamicGridBackground } from '@/components/shared/DynamicGridBackground'; export async function generateMetadata({ params, @@ -16,12 +17,30 @@ export async function generateMetadata({ }; } -export default function QAPage() { +export default async function QAPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: 'qa' }); + return ( -
- ...}> - - -
+ +
+
+

+ {t('pretitle')} +

+

{t('title')}

+

+ {t('subtitle')} +

+
+ ...}> + + +
+
); } diff --git a/frontend/components/about/HeroSection.tsx b/frontend/components/about/HeroSection.tsx index bfeecf65..8f855d4c 100644 --- a/frontend/components/about/HeroSection.tsx +++ b/frontend/components/about/HeroSection.tsx @@ -1,23 +1,12 @@ "use client" -import { useRef } from "react" -import { motion, useMotionTemplate, useMotionValue } from "framer-motion" +import { motion } from "framer-motion" import { CheckCircle, Users, Star, Linkedin, ArrowDown } from "lucide-react" import { InteractiveGame } from "./InteractiveGame" import type { PlatformStats } from "@/lib/about/stats" +import { DynamicGridBackground } from "@/components/shared/DynamicGridBackground" export function HeroSection({ stats }: { stats?: PlatformStats }) { - const containerRef = useRef(null) - - const mouseX = useMotionValue(0) - const mouseY = useMotionValue(0) - - function handleMouseMove({ currentTarget, clientX, clientY }: React.MouseEvent) { - const { left, top } = currentTarget.getBoundingClientRect() - mouseX.set(clientX - left) - mouseY.set(clientY - top) - } - const data = stats || { questionsSolved: "850+", githubStars: "120+", @@ -26,33 +15,7 @@ export function HeroSection({ stats }: { stats?: PlatformStats }) { } return ( -
- -
-
-
- - -
- - -
-
-
- +
@@ -121,7 +84,7 @@ export function HeroSection({ stats }: { stats?: PlatformStats }) {
-
+ ) } @@ -154,4 +117,4 @@ function MobileStatItem({ icon: Icon, color, bg, label, value }: any) { ) -} \ No newline at end of file +} diff --git a/frontend/components/q&a/Pagination.tsx b/frontend/components/q&a/Pagination.tsx index 42e1b1b7..5e275680 100644 --- a/frontend/components/q&a/Pagination.tsx +++ b/frontend/components/q&a/Pagination.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect, useState } from 'react'; import { useTranslations } from 'next-intl'; import { cn } from '@/lib/utils'; @@ -7,20 +8,42 @@ interface PaginationProps { currentPage: number; totalPages: number; onPageChange: (page: number) => void; + accentColor: string; +} + +function hexToRgba(hex: string, alpha: number): string { + const normalized = hex.replace('#', ''); + if (normalized.length !== 6) return `rgba(0, 0, 0, ${alpha})`; + const r = parseInt(normalized.slice(0, 2), 16); + const g = parseInt(normalized.slice(2, 4), 16); + const b = parseInt(normalized.slice(4, 6), 16); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; } export function Pagination({ currentPage, totalPages, onPageChange, + accentColor, }: PaginationProps) { const t = useTranslations('qa.pagination'); + const accentSoft = hexToRgba(accentColor, 0.16); + const accentGlow = hexToRgba(accentColor, 0.22); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const media = window.matchMedia('(max-width: 640px)'); + const update = () => setIsMobile(media.matches); + update(); + media.addEventListener('change', update); + return () => media.removeEventListener('change', update); + }, []); if (totalPages <= 1) return null; const getPageNumbers = (): (number | 'ellipsis')[] => { const pages: (number | 'ellipsis')[] = []; - const maxVisible = 5; + const maxVisible = isMobile ? 3 : 5; if (totalPages <= maxVisible + 2) { for (let i = 1; i <= totalPages; i++) { @@ -59,25 +82,32 @@ export function Pagination({ return ( ); -} \ No newline at end of file +} diff --git a/frontend/components/q&a/QaSection.tsx b/frontend/components/q&a/QaSection.tsx index 4291f56d..0ca2ac1a 100644 --- a/frontend/components/q&a/QaSection.tsx +++ b/frontend/components/q&a/QaSection.tsx @@ -1,84 +1,69 @@ 'use client'; import { useTranslations } from 'next-intl'; -import { Search, X } from 'lucide-react'; - import AccordionList from '@/components/q&a/AccordionList'; import { Pagination } from '@/components/q&a/Pagination'; -import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +import { Tabs, TabsList, TabsContent } from '@/components/ui/tabs'; import { categoryData } from '@/data/category'; import { useQaTabs } from '@/components/q&a/useQaTabs'; +import { QaTabButton } from '@/components/q&a/QaTabButton'; +import { qaTabStyles } from '@/data/qaTabs'; +import { cn } from '@/lib/utils'; export default function TabsSection() { const t = useTranslations('qa'); const { active, currentPage, - debouncedSearch, handleCategoryChange, handlePageChange, isLoading, items, localeKey, - searchQuery, - setSearchQuery, totalPages, - clearSearch, } = useQaTabs(); return (
-
- - - setSearchQuery(e.target.value)} - placeholder={t('searchPlaceholder')} - className="w-full pl-10 pr-10 py-3 border rounded-lg" - /> - - {searchQuery && ( - - )} -
- - + {categoryData.map(category => ( - - {category.translations[localeKey] ?? + label={ + category.translations[localeKey] ?? category.translations.en ?? - category.slug} - + category.slug + } + style={qaTabStyles[category.slug]} + isActive={active === category.slug} + /> ))} {categoryData.map(category => ( - {isLoading ? ( + {isLoading && (
- ) : items.length ? ( - - ) : ( -

- {debouncedSearch - ? t('noResults', { query: debouncedSearch }) - : t('noQuestions')} -

)} +
+ {items.length ? ( + + ) : ( +

+ {t('noQuestions')} +

+ )} +
))} @@ -88,6 +73,7 @@ export default function TabsSection() { currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} + accentColor={qaTabStyles[active].accent} /> )}
diff --git a/frontend/components/q&a/QaTabButton.tsx b/frontend/components/q&a/QaTabButton.tsx new file mode 100644 index 00000000..b183afd1 --- /dev/null +++ b/frontend/components/q&a/QaTabButton.tsx @@ -0,0 +1,55 @@ +'use client'; + +import Image from 'next/image'; + +import { TabsTrigger } from '@/components/ui/tabs'; +import { cn } from '@/lib/utils'; +import type { CategorySlug } from '@/components/q&a/types'; +import type { QaTabStyle } from '@/data/qaTabs'; + +type QaTabButtonProps = { + value: CategorySlug; + label: string; + style: QaTabStyle; + isActive: boolean; +}; + +export function QaTabButton({ + value, + label, + style, + isActive, +}: QaTabButtonProps) { + return ( + + + {label} + + {label} + + + ); +} diff --git a/frontend/components/q&a/useQaTabs.ts b/frontend/components/q&a/useQaTabs.ts index cd585deb..7779a64b 100644 --- a/frontend/components/q&a/useQaTabs.ts +++ b/frontend/components/q&a/useQaTabs.ts @@ -1,6 +1,6 @@ 'use client'; -import { useState, useEffect, useCallback, useRef } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { useSearchParams, useParams } from 'next/navigation'; import { useRouter } from '@/i18n/routing'; import { categoryData } from '@/data/category'; @@ -15,8 +15,6 @@ import { const CATEGORY_SLUGS = categoryData.map(category => category.slug); const DEFAULT_CATEGORY = CATEGORY_SLUGS[0] || 'html'; -const DEBOUNCE_MS = 400; - function resolveLocale(value: string): Locale { return qaConstants.supportedLocales.includes(value as Locale) ? (value as Locale) @@ -41,28 +39,20 @@ export function useQaTabs() { const safePageFromUrl = Number.isFinite(pageFromUrl) && pageFromUrl > 0 ? pageFromUrl : 1; const categoryFromUrl = searchParams.get('category') || DEFAULT_CATEGORY; - const searchFromUrl = searchParams.get('search') || ''; - const [active, setActive] = useState( isCategorySlug(categoryFromUrl) ? categoryFromUrl : DEFAULT_CATEGORY ); const [currentPage, setCurrentPage] = useState(safePageFromUrl); - const [searchQuery, setSearchQuery] = useState(searchFromUrl); - const [debouncedSearch, setDebouncedSearch] = useState(searchFromUrl); const [items, setItems] = useState([]); const [totalPages, setTotalPages] = useState(0); const [isLoading, setIsLoading] = useState(true); - const debounceRef = useRef | null>(null); - const mountedRef = useRef(false); - const updateUrl = useCallback( - (category: CategorySlug, page: number, search: string) => { + (category: CategorySlug, page: number) => { const params = new URLSearchParams(); if (category !== DEFAULT_CATEGORY) params.set('category', category); if (page > 1) params.set('page', String(page)); - if (search) params.set('search', search); const queryString = params.toString(); @@ -74,37 +64,26 @@ export function useQaTabs() { ); useEffect(() => { - if (!mountedRef.current) { - mountedRef.current = true; + setCurrentPage(safePageFromUrl); + }, [safePageFromUrl]); + + useEffect(() => { + if (!isCategorySlug(categoryFromUrl)) { return; } - - if (debounceRef.current) clearTimeout(debounceRef.current); - - debounceRef.current = setTimeout(() => { - setDebouncedSearch(searchQuery); - setCurrentPage(1); - updateUrl(active, 1, searchQuery); - }, DEBOUNCE_MS); - - return () => { - if (debounceRef.current) clearTimeout(debounceRef.current); - }; - }, [searchQuery, active, updateUrl]); + setActive(categoryFromUrl); + }, [categoryFromUrl]); useEffect(() => { + let isActive = true; const controller = new AbortController(); async function load() { setIsLoading(true); try { - const searchParam = debouncedSearch - ? `&search=${encodeURIComponent(debouncedSearch)}` - : ''; - const res = await fetch( - `/api/questions/${active}?page=${currentPage}&limit=10&locale=${localeKey}${searchParam}`, + `/api/questions/${active}?page=${currentPage}&limit=10&locale=${localeKey}`, { signal: controller.signal } ); @@ -124,19 +103,26 @@ export function useQaTabs() { ); setTotalPages(data.totalPages); } catch (error) { + if (!isActive || controller.signal.aborted) { + return; + } console.error('Failed to load questions:', error); setItems([]); setTotalPages(0); } finally { + if (!isActive) { + return; + } setIsLoading(false); } } load(); return () => { + isActive = false; controller.abort(); }; - }, [active, currentPage, debouncedSearch, localeKey]); + }, [active, currentPage, localeKey]); const handleCategoryChange = useCallback( (category: string) => { @@ -145,9 +131,7 @@ export function useQaTabs() { } setActive(category); setCurrentPage(1); - setSearchQuery(''); - setDebouncedSearch(''); - updateUrl(category, 1, ''); + updateUrl(category, 1); }, [updateUrl] ); @@ -155,31 +139,20 @@ export function useQaTabs() { const handlePageChange = useCallback( (page: number) => { setCurrentPage(page); - updateUrl(active, page, debouncedSearch); + updateUrl(active, page); window.scrollTo({ top: 0, behavior: 'smooth' }); }, - [active, debouncedSearch, updateUrl] + [active, updateUrl] ); - const clearSearch = useCallback(() => { - setSearchQuery(''); - setDebouncedSearch(''); - setCurrentPage(1); - updateUrl(active, 1, ''); - }, [active, updateUrl]); - return { active, currentPage, - debouncedSearch, handleCategoryChange, handlePageChange, isLoading, items, localeKey, - searchQuery, - setSearchQuery, totalPages, - clearSearch, }; } diff --git a/frontend/components/shared/DynamicGridBackground.tsx b/frontend/components/shared/DynamicGridBackground.tsx new file mode 100644 index 00000000..1b45edd9 --- /dev/null +++ b/frontend/components/shared/DynamicGridBackground.tsx @@ -0,0 +1,48 @@ +'use client'; + +import type { ReactNode, MouseEvent } from 'react'; +import { motion, useMotionTemplate, useMotionValue } from 'framer-motion'; + +import { cn } from '@/lib/utils'; + +type DynamicGridBackgroundProps = { + className?: string; + children?: ReactNode; +}; + +export function DynamicGridBackground({ + className, + children, +}: DynamicGridBackgroundProps) { + const mouseX = useMotionValue(0); + const mouseY = useMotionValue(0); + + function handleMouseMove(event: MouseEvent) { + const { left, top } = event.currentTarget.getBoundingClientRect(); + mouseX.set(event.clientX - left); + mouseY.set(event.clientY - top); + } + + return ( +
+
+
+
+ + +
+ + + {children} +
+ ); +} diff --git a/frontend/data/qaTabs.ts b/frontend/data/qaTabs.ts new file mode 100644 index 00000000..b17c95ce --- /dev/null +++ b/frontend/data/qaTabs.ts @@ -0,0 +1,83 @@ +import type { CategorySlug } from '@/components/q&a/types'; + +export type QaTabStyle = { + icon: string; + color: string; + glow: string; + accent: string; + iconClassName?: string; +}; + +export const qaTabStyles = { + git: { + icon: '/icons/git.svg', + color: + 'group-hover:border-[#F05032]/50 group-hover:bg-[#F05032]/10 data-[state=active]:border-[#F05032]/50 data-[state=active]:bg-[#F05032]/10', + glow: 'bg-[#F05032]', + accent: '#F05032', + }, + html: { + icon: '/icons/html5.svg', + color: + 'group-hover:border-[#E34F26]/50 group-hover:bg-[#E34F26]/10 data-[state=active]:border-[#E34F26]/50 data-[state=active]:bg-[#E34F26]/10', + glow: 'bg-[#E34F26]', + accent: '#E34F26', + }, + css: { + icon: '/icons/css3.svg', + color: + 'group-hover:border-[#7C4DFF]/50 group-hover:bg-[#7C4DFF]/10 data-[state=active]:border-[#7C4DFF]/50 data-[state=active]:bg-[#7C4DFF]/10', + glow: 'bg-[#7C4DFF]', + accent: '#7C4DFF', + }, + javascript: { + icon: '/icons/javascript.svg', + color: + 'group-hover:border-[#F7DF1E]/50 group-hover:bg-[#F7DF1E]/10 data-[state=active]:border-[#F7DF1E]/50 data-[state=active]:bg-[#F7DF1E]/10', + glow: 'bg-[#F7DF1E]', + accent: '#F7DF1E', + }, + typescript: { + icon: '/icons/typescript.svg', + color: + 'group-hover:border-[#3178C6]/50 group-hover:bg-[#3178C6]/10 data-[state=active]:border-[#3178C6]/50 data-[state=active]:bg-[#3178C6]/10', + glow: 'bg-[#3178C6]', + accent: '#3178C6', + }, + react: { + icon: '/icons/react.svg', + color: + 'group-hover:border-[#61DAFB]/50 group-hover:bg-[#61DAFB]/10 data-[state=active]:border-[#61DAFB]/50 data-[state=active]:bg-[#61DAFB]/10', + glow: 'bg-[#61DAFB]', + accent: '#61DAFB', + }, + next: { + icon: '/icons/nextjs.svg', + color: + 'group-hover:border-black/50 dark:group-hover:border-white/50 group-hover:bg-black/5 dark:group-hover:bg-white/10 data-[state=active]:border-black/50 dark:data-[state=active]:border-white/50 data-[state=active]:bg-black/5 dark:data-[state=active]:bg-white/10', + glow: 'bg-black dark:bg-white', + iconClassName: 'dark:invert', + accent: '#111111', + }, + vue: { + icon: '/icons/vuejs.svg', + color: + 'group-hover:border-[#4FC08D]/50 group-hover:bg-[#4FC08D]/10 data-[state=active]:border-[#4FC08D]/50 data-[state=active]:bg-[#4FC08D]/10', + glow: 'bg-[#4FC08D]', + accent: '#4FC08D', + }, + angular: { + icon: '/icons/angular.svg', + color: + 'group-hover:border-[#DD0031]/50 group-hover:bg-[#DD0031]/10 data-[state=active]:border-[#DD0031]/50 data-[state=active]:bg-[#DD0031]/10', + glow: 'bg-[#DD0031]', + accent: '#DD0031', + }, + node: { + icon: '/icons/nodejs.svg', + color: + 'group-hover:border-[#339933]/50 group-hover:bg-[#339933]/10 data-[state=active]:border-[#339933]/50 data-[state=active]:bg-[#339933]/10', + glow: 'bg-[#339933]', + accent: '#339933', + }, +} as const satisfies Record; diff --git a/frontend/messages/en.json b/frontend/messages/en.json index fd1ae080..c2413a03 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -31,6 +31,8 @@ }, "qa": { "title": "Questions and Answers", + "pretitle": "Interview", + "subtitle": "Explore curated interview questions by category", "searchPlaceholder": "Search questions...", "clearSearch": "Clear search", "noResults": "Nothing found for \"{query}\"", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index ada7588c..e4b92278 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -31,6 +31,8 @@ }, "qa": { "title": "Pytania i Odpowiedzi", + "pretitle": "Rozmowa", + "subtitle": "Przeglądaj wybrane pytania według kategorii", "searchPlaceholder": "Szukaj pytań...", "clearSearch": "Wyczyść wyszukiwanie", "noResults": "Nic nie znaleziono dla \"{query}\"", diff --git a/frontend/messages/uk.json b/frontend/messages/uk.json index e844c9bb..59ee735c 100644 --- a/frontend/messages/uk.json +++ b/frontend/messages/uk.json @@ -31,6 +31,8 @@ }, "qa": { "title": "Питання та Відповіді", + "pretitle": "Інтерв'ю", + "subtitle": "Переглядайте добірку питань за категоріями", "searchPlaceholder": "Пошук...", "clearSearch": "Очистити пошук", "noResults": "Нічого не знайдено за запитом \"{query}\"", From 951b5675acea4b8b19c9191a9a75b9243e8689fb Mon Sep 17 00:00:00 2001 From: Viktor Svertoka Date: Tue, 20 Jan 2026 16:11:59 +0200 Subject: [PATCH 2/5] fix(qa): type category slug when indexing tab styles --- frontend/components/q&a/QaSection.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/components/q&a/QaSection.tsx b/frontend/components/q&a/QaSection.tsx index 0ca2ac1a..8b329e01 100644 --- a/frontend/components/q&a/QaSection.tsx +++ b/frontend/components/q&a/QaSection.tsx @@ -9,6 +9,7 @@ import { useQaTabs } from '@/components/q&a/useQaTabs'; import { QaTabButton } from '@/components/q&a/QaTabButton'; import { qaTabStyles } from '@/data/qaTabs'; import { cn } from '@/lib/utils'; +import type { CategorySlug } from '@/components/q&a/types'; export default function TabsSection() { const t = useTranslations('qa'); @@ -27,19 +28,22 @@ export default function TabsSection() {
- {categoryData.map(category => ( + {categoryData.map(category => { + const slug = category.slug as CategorySlug; + return ( - ))} + ); + })} {categoryData.map(category => ( From d6f2b54a853a3475ddd86de35a52bb02ea8fa2e3 Mon Sep 17 00:00:00 2001 From: Viktor Svertoka Date: Tue, 20 Jan 2026 16:31:51 +0200 Subject: [PATCH 3/5] fix(qa): type category slug when indexing tab styles --- frontend/components/q&a/QaSection.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/components/q&a/QaSection.tsx b/frontend/components/q&a/QaSection.tsx index 8b329e01..edbfaf04 100644 --- a/frontend/components/q&a/QaSection.tsx +++ b/frontend/components/q&a/QaSection.tsx @@ -29,18 +29,19 @@ export default function TabsSection() { {categoryData.map(category => { - const slug = category.slug as CategorySlug; + const slug = category.slug as keyof typeof qaTabStyles; + const value = slug as CategorySlug; return ( ); })} From 2154aaa22660b5547494703ac8ac1ab180f64070 Mon Sep 17 00:00:00 2001 From: Viktor Svertoka Date: Tue, 20 Jan 2026 16:37:36 +0200 Subject: [PATCH 4/5] fix(qa): type category slug when indexing tab styles --- frontend/components/q&a/QaSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/q&a/QaSection.tsx b/frontend/components/q&a/QaSection.tsx index edbfaf04..6ff38632 100644 --- a/frontend/components/q&a/QaSection.tsx +++ b/frontend/components/q&a/QaSection.tsx @@ -78,7 +78,7 @@ export default function TabsSection() { currentPage={currentPage} totalPages={totalPages} onPageChange={handlePageChange} - accentColor={qaTabStyles[active].accent} + accentColor={qaTabStyles[active as keyof typeof qaTabStyles].accent} /> )}
From 8814fc35e3ff129c319f423a4c19bcabe5d08675 Mon Sep 17 00:00:00 2001 From: Viktor Svertoka Date: Tue, 20 Jan 2026 16:47:49 +0200 Subject: [PATCH 5/5] fix(ui): address lint warnings in Q&A tabs and dynamic background --- frontend/components/q&a/useQaTabs.ts | 5 ++--- frontend/components/shared/DynamicGridBackground.tsx | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/components/q&a/useQaTabs.ts b/frontend/components/q&a/useQaTabs.ts index 7779a64b..d2497f3c 100644 --- a/frontend/components/q&a/useQaTabs.ts +++ b/frontend/components/q&a/useQaTabs.ts @@ -110,10 +110,9 @@ export function useQaTabs() { setItems([]); setTotalPages(0); } finally { - if (!isActive) { - return; + if (isActive) { + setIsLoading(false); } - setIsLoading(false); } } diff --git a/frontend/components/shared/DynamicGridBackground.tsx b/frontend/components/shared/DynamicGridBackground.tsx index 1b45edd9..554117f9 100644 --- a/frontend/components/shared/DynamicGridBackground.tsx +++ b/frontend/components/shared/DynamicGridBackground.tsx @@ -16,6 +16,7 @@ export function DynamicGridBackground({ }: DynamicGridBackgroundProps) { const mouseX = useMotionValue(0); const mouseY = useMotionValue(0); + const maskImage = useMotionTemplate`radial-gradient(300px circle at ${mouseX}px ${mouseY}px, black, transparent)`; function handleMouseMove(event: MouseEvent) { const { left, top } = event.currentTarget.getBoundingClientRect(); @@ -35,8 +36,8 @@ export function DynamicGridBackground({