-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/marketing website #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Add ESLint, TypeScript, PostCSS, and Next.js configs - Add workspace configuration for pnpm - Add .gitignore and README
- Include Next.js, React, Tailwind CSS, GSAP, Framer Motion - Add development and build scripts
- Add logos, icons, and SVG assets
- Add root layout with providers (theme, GSAP) - Add global CSS with Tailwind directives - Add main landing page
- Add dashboard page for authenticated users - Add documentation page - Add pricing page with pricing section - Add privacy and terms pages
- Add hero section with radar animation - Add features section - Add pricing section with Pro (/mo) and Team (/mo) plans - Add FAQ, social proof, CTA, and how-it-works sections
- Add header with navigation - Add footer with links - Add container component
- Add base components: button, card, badge, avatar, tabs - Add form components: accordion, separator, switch, tooltip - Add navigation: dropdown-menu, navigation-menu - Add theme provider for dark/light mode
- Add GSAP hook for animations - Add mobile detection hook - Add constants for pricing, features, FAQs, testimonials - Add utility functions and GSAP config - Add extension version file
- Add radar animation for hero section
|
Warning Rate limit exceeded@senutpal has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 13 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR introduces a complete Next.js web application including configuration files, a global design system with CSS variables and animations, page components (home, docs, pricing, privacy, terms, dashboard), a comprehensive UI component library built on Radix UI, landing page sections with animations, and utility hooks and constants for site-wide configuration. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
- Add dependencies for apps/web (Next.js, React, GSAP, Radix UI, etc.) - Update workspace lockfile for monorepo
- Sync lockfile with apps/extension/package.json - Update @types/minimatch, @types/node, esbuild versions
- Fix workspace name conflict with apps/extension - Follow monorepo naming conventions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 49
🤖 Fix all issues with AI agents
In @apps/web/package.json:
- Around line 5-10: Update the "lint" script in package.json so it explicitly
passes a target instead of just "eslint"; change the "lint" script entry (the
"lint" key) to run eslint against the repo (for example "eslint ." or "eslint
src") to ensure consistent behavior and clarity about what files are being
linted.
In @apps/web/pnpm-workspace.yaml:
- Around line 1-5: Remove the nested pnpm-workspace.yaml file in the apps/web
directory and consolidate its settings into the repository root
pnpm-workspace.yaml; specifically, delete the local file containing "packages: -
. " and the "ignoredBuiltDependencies: - sharp - unrs-resolver" block, and if
those ignoredBuiltDependencies are required, add them to the root
pnpm-workspace.yaml under ignoredBuiltDependencies so the workspace config only
lives at the repository root.
In @apps/web/README.md:
- Line 1: Add a top-level heading as the first line of the README to satisfy
markdown-lint rules; prepend a project-specific H1 (e.g., "# Web App" or "#
Next.js App") before the existing text ("This is a Next.js project...") so the
file begins with a single leading top-level heading.
In @apps/web/src/app/dashboard/page.tsx:
- Around line 52-58: The Link element rendering SITE_CONFIG.links.marketplace
(the "Install DevRadar" link) opens an external domain but lacks target and rel
attributes; update the Link component that wraps "Install DevRadar" (and the
adjacent ExternalLink icon) to include target="_blank" and rel="noopener
noreferrer" so the link opens in a new tab and prevents window.opener security
issues.
In @apps/web/src/app/docs/page.tsx:
- Around line 36-53: The docSections array references documentation routes that
don't exist, causing 404s; either create the missing sub-page components for
each referenced href (e.g., implement page components for
'/docs/getting-started', '/docs/configuration', '/docs/privacy', '/docs/teams'
under the docs route) or update the docSections entries to only include existing
routes; locate the docSections constant in apps/web/src/app/docs/page.tsx and
either add new page.tsx files for each href (implementing the expected export
default React component for those subpages) or remove/replace the hrefs/items in
docSections to match currently implemented routes.
- Around line 143-160: The card divs in the docSections mapping (rendered in
apps/web/src/app/docs/page.tsx) are styled with cursor-pointer and hover effects
but do not navigate; wrap each card's outer container in the appropriate Next.js
Link (or <a> if using plain router) using section.href so clicking the card
navigates to section.href, preserve existing className (e.g., "card-brutal p-8
card-hover group cursor-pointer") on the Link or inner element, move
key={section.title} to the Link, and ensure the inner semantics remain (h3, ul,
li) and accessibility is preserved (add role or aria-label if needed).
In @apps/web/src/app/globals.css:
- Around line 71-87: You have two separate :root blocks defining CSS custom
properties which should be consolidated for maintainability; find both :root
declarations and merge their variable definitions into a single :root block
(keep all unique variables, remove duplicates or reconcile conflicting values by
choosing the intended one), ensure variables like --radar-bg, --radar-ring,
--radar-card-bg, etc. are present once with the correct final values, then
delete the redundant :root block so all global CSS custom properties live in one
place.
- Around line 366-376: Remove the duplicate .text-glow CSS rule: keep a single
.text-glow definition with the existing text-shadow declarations and delete the
repeated block so only one .text-glow class remains in globals.css.
- Around line 89-123: There are two consecutive .dark rule blocks that conflict
and make the first block dead; consolidate them into a single .dark block so
both intended values are preserved or intentionally chosen (e.g., merge
differing declarations for --radar-bg, --radar-bg-inner, --radar-card-border,
--radar-card-bg, --radar-ring-dim, --radar-scanline, etc.), remove the duplicate
block, and ensure the final .dark contains the correct, non-contradictory
variable values you want to use for the radar theme.
In @apps/web/src/app/layout.tsx:
- Around line 66-72: Extract the hardcoded Twitter handle from the
twitter.creator field and reference a new SITE_CONFIG.twitterHandle instead of
the literal '@devradar'; add a twitterHandle property to SITE_CONFIG (populated
from SITE_CONFIG.links.twitter or the canonical handle) and update the twitter
object in layout.tsx to use SITE_CONFIG.twitterHandle so the handle is
centralized and consistent with SITE_CONFIG.links.twitter.
- Around line 97-102: The noise overlay div with className "fixed inset-0
pointer-events-none z-50 opacity-[0.015] dark:opacity-[0.015]" is currently
using z-50 and can visually sit above the header; change its z-index to a lower
value (for example replace z-50 with z-10 or z-0) so the decorative overlay
renders behind interactive elements while keeping pointer-events-none and the
existing style unchanged.
In @apps/web/src/app/pricing/page.tsx:
- Around line 68-89: Replace hardcoded hex color utility classes in the pricing
section with the project’s Tailwind theme tokens: swap bg-[#050505] on the outer
div to bg-background, text-[#FAFAFA] on the h1 to text-foreground,
text-[#737373] on the paragraph to text-muted-foreground, and text-[#FFB800] on
the "Pricing" span to text-primary (leave non-color utilities like
bg-grid-brutal intact). Update the className strings in the JSX elements (the
outer div with "pt-24 pb-16 bg-[#050505] min-h-screen", the h1 with
"text-[#FAFAFA]", the p with "text-[#737373]", and the span with
"text-[#FFB800]") to use these semantic theme classes so colors follow the app
theme and support future theme or mode changes.
- Around line 165-196: The fragment returned in the comparisonFeatures.map
callback is missing a key, causing React warnings; wrap the mapped rows in a
keyed fragment by replacing the shorthand fragment <>...</> with an explicit
React.Fragment (or Fragment) and add a unique key (for example use
category.category or another unique id) so the outermost element of the map has
a key; update the mapping that produces the rows for comparisonFeatures and
ensure any required import for Fragment is added if you choose Fragment instead
of React.Fragment.
In @apps/web/src/app/privacy/page.tsx:
- Around line 65-66: The hard-coded lastUpdated in PrivacyPage is duplicated;
create a single exported constant (e.g., LAST_UPDATED) in a shared module and
replace the inline const lastUpdated in PrivacyPage with an import of that
shared constant so both PrivacyPage and the Terms page reference the same
centralized value.
- Around line 43-59: The current parsing in the items.map callback uses
item.split(' - ') (see item.split(' - ')) which is fragile; change the data
shape to use objects like { label, description } for the arrays used by this
component and update the items.map rendering to destructure each object (e.g.,
const { label, description } = item) instead of splitting strings; keep the
conditional styling using isNever and preserve the existing JSX output (the span
showing label and optional description) so descriptions can safely contain " - "
without breaking.
In @apps/web/src/app/terms/page.tsx:
- Around line 13-14: The TermsPage currently uses a hardcoded lastUpdated value
('lastUpdated' in TermsPage) which requires manual updates; centralize this by
adding a LEGAL_CONFIG (or augment SITE_CONFIG) in your shared constants file
(e.g., constants.ts) exporting lastUpdated, then import that constant into
TermsPage and replace the local lastUpdated with LEGAL_CONFIG.lastUpdated (also
update the Privacy page to use the same constant) so legal dates stay
synchronized across pages.
In @apps/web/src/components/animations/radar-animation.tsx:
- Line 210: The Image in RadarAnimation is used in the hero (above-the-fold) but
lacks the Next/Image preload hint; update the <Image> usage in the
RadarAnimation component to add the priority prop (e.g., <Image ... priority />)
so the logo is preloaded for improved LCP and ensure any existing
width/height/alt props remain unchanged.
- Around line 68-79: The inline transform uses Tailwind-like responsive prefixes
which don't work in style, so replace the inline responsive transforms with CSS
classes that apply different translateY values via media queries and keep only a
CSS variable for rotation; e.g., add a .radar-tick CSS rule that sets transform:
translate(-50%, -50%) rotate(var(--deg)) translateY(-148px) and overrides
translateY in @media (min-width: ...) rules for md and lg, then in the mapped
element (the div inside {[0, 90, 180, 270].map(...)} ) remove the responsive
translate from style and instead set style={{ '--deg': `${deg}deg` }} (or
equivalent CSS variable syntax) and apply the new className (e.g., "radar-tick")
so rotation still uses the per-item deg while responsive translateY is driven by
CSS.
In @apps/web/src/components/landing/faq-section.tsx:
- Around line 34-55: The toggle <button> that calls onToggle currently has no
explicit type and will default to type="submit" in a form; update the button
element (the one with onClick={onToggle} and className="w-full flex items-start
gap-6 py-6 text-left group") to include type="button" to prevent unintended form
submissions when used inside a form.
- Around line 100-108: The current mapping uses the array index as the React key
for FAQItem (FAQ_ITEMS.map(... key={index})), which can cause reconciliation
issues if the list ever changes; replace the unstable index key with a stable
identifier such as item.question or add a unique id property to each FAQ item
and use that (e.g., key={item.question} or key={item.id}) so the FAQItem
components maintain consistent identity across renders.
In @apps/web/src/components/landing/features-section.tsx:
- Around line 109-116: The inline style setting counterReset: 'feature' on the
div wrapping FEATURES / FeatureCard is unused; either remove the style or make
its intent explicit: if you rely on an external CSS counter (counter-increment
or counter() in a stylesheet), add a brief comment next to the div noting that
external CSS uses the 'feature' counter; otherwise delete the style property so
the div JSX no longer includes style={{ counterReset: 'feature' }}.
- Around line 25-27: The FeatureCard component obtains Icon =
iconMap[feature.icon] which can be undefined and will throw when rendered;
update FeatureCard to guard against a missing mapping by checking whether Icon
is truthy before rendering (e.g., render a defined fallback component or a
simple placeholder/empty element when Icon is undefined) and ensure any props
passed to Icon are only used when Icon exists; reference the FeatureCard
function, the iconMap lookup, and feature.icon to implement the conditional
fallback.
In @apps/web/src/components/landing/hero-section.tsx:
- Around line 16-59: The effect running GSAP animations in useEffect (using
heroRef, gsap.context, timeline and ctx.revert) ignores users'
prefers-reduced-motion setting; update the effect to detect the reduced motion
preference (via window.matchMedia('(prefers-reduced-motion: reduce)') or use
gsap.matchMedia()) and skip or simplify animations when reduced motion is
requested, i.e., if prefers-reduced-motion is true do not build/run the timeline
(or use a non-animated state) and still cleanup ctx.revert() appropriately.
In @apps/web/src/components/landing/how-it-works-section.tsx:
- Around line 10-32: Extract the local steps array (the steps constant in
how-it-works-section.tsx) into a shared constant in the project's constants
module (e.g., add and export HOW_IT_WORKS_STEPS or STEPS from
"@/lib/constants.ts"), then replace the local definition with an import of that
exported constant in how-it-works-section.tsx and remove the inline array; keep
the same object shape (number, title, description, icon, accent) and names
(Download, UserPlus, Radio) so the component logic doesn't change.
- Around line 4-8: The component mixes Framer Motion (imported as motion) for
the card animations and GSAP (imported as gsap) for the connector line,
increasing bundle size and risking inconsistent timing; pick one library and
convert the other animation to it. If you prefer Framer Motion (recommended to
match other card usage), remove the gsap import and reimplement the connector
line animation using Framer Motion's hooks (useInView or animate prop) and
CSS/SVG properties (e.g., animate stroke-dashoffset/stroke-dasharray on the
connector element); alternatively, if you prefer GSAP (to match the hero),
remove the motion import and replace the motion.div card animations with GSAP
timelines targeting card refs (use useRef/useEffect) and reuse the existing
gsap-config utilities. Update imports (remove unused motion or gsap) and adjust
the connector element or card JSX to the chosen API so all animations use the
same library.
In @apps/web/src/components/landing/pricing-section.tsx:
- Around line 20-22: The annual price calculation in PricingCard uses
Math.floor(tier.price * 0.5) which can understate the actual discount (e.g.,
Team: Math.floor(7 * 0.5) => 3), so update the logic in PricingCard to compute
the annual price without excessive rounding (use Math.round(tier.price * 0.5) or
keep full decimal and format for display) and ensure any discount messaging or
badge (e.g., the "50%" text) is adjusted to reflect either the precise computed
percentage or changed to a conservative label like "Save up to 50%". Ensure you
update references to price and discount calculation consistently within
PricingCard and any related display strings.
- Around line 147-169: The billing toggle is missing an accessible label for
screen readers; update the Switch usage (the Switch component controlled by
isAnnual and setIsAnnual) to include an accessible name by adding either an
aria-label or aria-labelledby that references the visible "Monthly"/"Annual"
text (or wrap the toggle and text in a label element and ensure the Switch has
an id), so screen readers understand the control and its purpose while
preserving the existing visual markup and state handlers.
In @apps/web/src/components/landing/social-proof-section.tsx:
- Around line 148-150: The key prop currently uses testimonial.author which may
not be unique; update the TESTIMONIALS data to include a unique id for each
testimonial and use that id as the key in the TestimonialCard map, or if adding
ids isn't feasible immediately, use a stable fallback combining the index (the
existing index variable) with author (e.g., `${testimonial.author}-${index}`) to
ensure uniqueness while rendering TestimonialCard.
- Around line 18-41: The suffix detection wrongly treats "ms" as "m" because
hasM is checked before hasMs; update the logic so hasMs =
stat.value.toLowerCase().includes('ms') is evaluated (it already is) but then
check/apply hasMs before hasM in the formatting block and use an if/else-if
chain (or return early) when building formatted to prevent multiple suffixes
(e.g., handle hasMs first, then hasK, then hasM, then plus).
In @apps/web/src/components/layout/container.tsx:
- Around line 17-28: The Container component currently doesn't forward refs, so
add ref forwarding by converting Container to a forwardRef component (use
React.forwardRef) that accepts a ref parameter and forwards it to the rendered
element (the polymorphic "as: Component" element); keep ContainerProps,
sizeClasses and cn unchanged, ensure the forwarded ref type covers the
underlying element (e.g., HTMLDivElement | HTMLElement), add
Container.displayName = 'Container' for devtools, and preserve the existing
className and prop handling while passing ref to <Component />.
In @apps/web/src/components/layout/footer.tsx:
- Around line 7-8: The Footer component recalculates currentYear on every
render; move the computation out of the component (e.g., define a top-level
constant like CURRENT_YEAR = new Date().getFullYear()) or memoize it with
React.useMemo inside Footer and then replace references to currentYear with that
constant/memoized value; update the function Footer to read from the new
constant/memoized variable to avoid repeated Date construction.
- Around line 46-56: Add an accessible <title> to the Discord SVG: inside the
SVG element used in the Link (the Discord icon), insert a
<title>Discord</title>, give it a unique id (e.g., discord-title) and reference
it via aria-labelledby="discord-title" (and optionally add role="img") so
assistive tech reads the SVG; ensure the id is unique if this icon is reused
elsewhere and keep the existing Link aria-label intact.
In @apps/web/src/components/layout/header.tsx:
- Around line 147-150: The mobile menu toggle button in header.tsx is missing an
explicit type which defaults to "submit" and can cause unintended form
submissions; update the <button> element that calls
setIsMobileMenuOpen(!isMobileMenuOpen) to include type="button" so it only
toggles the menu (refer to the button that toggles isMobileMenuOpen via
setIsMobileMenuOpen).
- Around line 235-240: The mobile GitHub Link using SITE_CONFIG.links.github
(rendering the Github icon) is missing the external-link attributes; update the
Link component to include target="_blank" and rel="noopener noreferrer" so it
matches the desktop GitHub link behavior and avoids opener/referrer issues.
In @apps/web/src/components/ui/accordion.tsx:
- Around line 56-59: AccordionContent uses Tailwind classes animate-accordion-up
and animate-accordion-down but those keyframes are missing; add definitions for
the corresponding @keyframes and animation utilities (names matching
animate-accordion-up and animate-accordion-down) either by adding the keyframe
and animation entries to your Tailwind config (extend.theme.keyframes and
extend.theme.animation) or by putting CSS @keyframes and
.animate-accordion-up/.animate-accordion-down rules into
apps/web/src/app/globals.css so AccordionPrimitive.Content’s classes resolve
correctly.
In @apps/web/src/components/ui/badge.tsx:
- Around line 37-43: The Badge component currently merges className outside the
variant helper which is inconsistent with Button; update the return so
badgeVariants receives the className (pass className into badgeVariants along
with { variant }) and remove the external cn merge so Comp's className is solely
the result of badgeVariants({ variant, className }), ensuring you still spread
{...props} and keep data-slot="badge" and Comp reference unchanged.
In @apps/web/src/components/ui/card.tsx:
- Around line 22-24: The Tailwind arbitrary selectors in CardHeader and
CardFooter are invalid; update the className usage in the CardHeader component
(where className includes "[.border-b]:pb-6") to use the element-aware form
"[&.border-b]:pb-6", and update the CardFooter usage (where className includes
"[.border-t]:pt-6") to "[&.border-t]:pt-6" (or use a :has(...) variant if you
intended to target child elements); modify the className strings in the
CardHeader and CardFooter definitions accordingly.
In @apps/web/src/components/ui/dropdown-menu.tsx:
- Around line 9-21: The data-slot attributes on non-rendering primitives should
be removed or moved because DropdownMenuPrimitive.Root,
DropdownMenuPrimitive.Portal, and DropdownMenuPrimitive.Sub are context
providers and do not produce DOM nodes; update the wrapper components
DropdownMenu, DropdownMenuPortal, and DropdownMenuSub to remove the
data-slot="..." props (or instead attach those data-slot attributes to the
actual rendering components like DropdownMenuTrigger/Content/Item) so the
attributes appear in the DOM and maintain consistency.
In @apps/web/src/components/ui/mode-toggle.tsx:
- Around line 9-18: The ModeToggle component currently reads theme from
useTheme() which can be undefined or 'system'; instead use resolvedTheme from
useTheme() to determine the actual active theme and drive toggling; update
ModeToggle to destructure resolvedTheme and setTheme from useTheme(), compute
the currentTheme = resolvedTheme ?? 'light' (or other safe default), and call
setTheme(currentTheme === 'dark' ? 'light' : 'dark') in the onClick so the
toggle always flips the actual rendered theme.
In @apps/web/src/components/ui/navigation-menu.tsx:
- Around line 61-83: navigationMenuTriggerStyle already includes the "group"
class, so remove the duplicate "group" passed into the className in
NavigationMenuTrigger; update the cn call in the NavigationMenuTrigger component
(the one that currently calls cn(navigationMenuTriggerStyle(), "group",
className)) to only combine navigationMenuTriggerStyle() and className (using
the cn helper) so "group" is not duplicated.
In @apps/web/src/components/ui/tooltip.tsx:
- Line 55: The TooltipPrimitive.Arrow SVG uses a redundant bg-foreground class
(background-color has no effect on SVG); remove bg-foreground from the className
on TooltipPrimitive.Arrow so the element only uses fill-foreground (and any
other relevant classes like z-50, size-2.5, translate-y..., rotate-45,
rounded-[2px]) to keep styling correct.
- Around line 21-29: The Tooltip component currently wraps each instance in its
own TooltipProvider which prevents shared state like skipDelayDuration; remove
the TooltipProvider from the Tooltip component so Tooltip only renders
<TooltipPrimitive.Root ... /> (keep the function name Tooltip and props
signature intact) and update consumers/layout to mount a single TooltipProvider
at a higher level (e.g., app or layout) to enable coordinated tooltip behavior.
In @apps/web/src/hooks/use-gsap.ts:
- Around line 203-221: The gsap.context call in useCounterAnimation is missing
the scope argument which can break proper scoping and cleanup; update the call
to pass the element/ref (e.g., element or valueRef.current or a container ref)
as the first argument to gsap.context, e.g. gsap.context(scope, () => { ... }),
and ensure the returned ctx is reverted in the hook cleanup to mirror other
hooks in this file.
In @apps/web/src/hooks/use-mobile.ts:
- Around line 45-67: The hook useWindowSize currently initializes size to
{width: 0, height: 0}, causing layout shifts; update useWindowSize to initialize
state with the actual window dimensions (using window.innerWidth/innerHeight if
available) or add an isReady boolean (e.g., {width, height, isReady}) that flips
true on the first handleResize call, ensure the effect still registers the
resize listener and cleans it up, and update any callers to handle the new shape
or isReady flag (or document the initial-zero behavior if you choose not to
change it).
- Around line 12-20: The subscribe function is attaching both a matchMedia
listener and a redundant window resize listener; remove the resize-related code
so only the MediaQueryList listener is used. Specifically, in subscribe (and its
returned cleanup) drop the window.addEventListener('resize', callback) and
window.removeEventListener('resize', callback') lines and rely solely on
mql.addEventListener('change', callback) / mql.removeEventListener('change',
callback); keep MOBILE_BREAKPOINT and the rest of the logic unchanged.
In @apps/web/src/lib/gsap-config.ts:
- Around line 148-165: animateFeatureCards can dereference cards[0] when passed
an empty array; add an early guard at the start of animateFeatureCards that
returns early (e.g., null or a no-op) when cards.length === 0 to avoid passing
undefined as the ScrollTrigger trigger, and apply the same guard to the
analogous function around lines 167-185 to prevent null/undefined triggers.
- Around line 205-216: In createParallax, guard against
element.closest('section') returning null by using the element as a fallback for
the ScrollTrigger trigger; replace the trigger value with something like const
trigger = element.closest('section') || element and pass that trigger into the
scrollTrigger config so ScrollTrigger never receives null.
- Around line 98-146: createHeroTimeline currently passes querySelector results
(e.g., '.hero-badge', '.hero-title', '.hero-subtitle', '.hero-cta',
'.hero-visual') directly to GSAP which can receive null and fail; update
createHeroTimeline to guard each queried element/NodeList before calling tl.from
(for NodeList use length check) — only add that tween if the element(s) exist,
and still return the timeline (or an early noop timeline) so callers get a valid
GSAP timeline even when some elements are missing.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (8)
apps/web/public/file.svgis excluded by!**/*.svgapps/web/public/globe.svgis excluded by!**/*.svgapps/web/public/logo.pngis excluded by!**/*.pngapps/web/public/next.svgis excluded by!**/*.svgapps/web/public/vercel.svgis excluded by!**/*.svgapps/web/public/window.svgis excluded by!**/*.svgapps/web/src/app/favicon.icois excluded by!**/*.icopnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (50)
apps/web/.gitignoreapps/web/README.mdapps/web/components.jsonapps/web/eslint.config.mjsapps/web/next.config.tsapps/web/package.jsonapps/web/pnpm-workspace.yamlapps/web/postcss.config.mjsapps/web/src/app/dashboard/page.tsxapps/web/src/app/docs/page.tsxapps/web/src/app/globals.cssapps/web/src/app/layout.tsxapps/web/src/app/page.tsxapps/web/src/app/pricing/page.tsxapps/web/src/app/privacy/page.tsxapps/web/src/app/terms/page.tsxapps/web/src/components/animations/index.tsapps/web/src/components/animations/radar-animation.tsxapps/web/src/components/landing/cta-section.tsxapps/web/src/components/landing/faq-section.tsxapps/web/src/components/landing/features-section.tsxapps/web/src/components/landing/hero-section.tsxapps/web/src/components/landing/how-it-works-section.tsxapps/web/src/components/landing/index.tsapps/web/src/components/landing/pricing-section.tsxapps/web/src/components/landing/social-proof-section.tsxapps/web/src/components/layout/container.tsxapps/web/src/components/layout/footer.tsxapps/web/src/components/layout/header.tsxapps/web/src/components/layout/index.tsapps/web/src/components/theme-provider.tsxapps/web/src/components/ui/accordion.tsxapps/web/src/components/ui/avatar.tsxapps/web/src/components/ui/badge.tsxapps/web/src/components/ui/button.tsxapps/web/src/components/ui/card.tsxapps/web/src/components/ui/dropdown-menu.tsxapps/web/src/components/ui/mode-toggle.tsxapps/web/src/components/ui/navigation-menu.tsxapps/web/src/components/ui/separator.tsxapps/web/src/components/ui/switch.tsxapps/web/src/components/ui/tabs.tsxapps/web/src/components/ui/tooltip.tsxapps/web/src/hooks/use-gsap.tsapps/web/src/hooks/use-mobile.tsapps/web/src/lib/constants.tsapps/web/src/lib/extension-version.tsapps/web/src/lib/gsap-config.tsapps/web/src/lib/utils.tsapps/web/tsconfig.json
🧰 Additional context used
🧬 Code graph analysis (27)
apps/web/src/components/layout/container.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/landing/how-it-works-section.tsx (2)
apps/web/src/components/landing/index.ts (1)
HowItWorksSection(3-3)apps/web/src/lib/gsap-config.ts (1)
gsap(232-232)
apps/web/src/components/ui/button.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/landing/faq-section.tsx (3)
apps/web/src/lib/constants.ts (1)
FAQ_ITEMS(153-194)apps/web/src/lib/utils.ts (1)
cn(4-6)apps/web/src/components/landing/index.ts (1)
FAQSection(6-6)
apps/web/src/app/layout.tsx (2)
apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)apps/web/src/components/theme-provider.tsx (1)
ThemeProvider(6-11)
apps/web/src/app/privacy/page.tsx (2)
apps/web/src/app/dashboard/page.tsx (1)
metadata(9-12)apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)
apps/web/src/components/landing/hero-section.tsx (3)
apps/web/src/lib/gsap-config.ts (1)
gsap(232-232)apps/web/src/lib/extension-version.ts (1)
EXTENSION_VERSION(2-2)apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)
apps/web/src/components/layout/footer.tsx (2)
apps/web/src/components/layout/container.tsx (1)
Container(17-28)apps/web/src/lib/constants.ts (2)
SITE_CONFIG(1-20)FOOTER_LINKS(28-49)
apps/web/src/components/landing/features-section.tsx (2)
apps/web/src/lib/constants.ts (1)
FEATURES(51-93)apps/web/src/components/landing/index.ts (1)
FeaturesSection(2-2)
apps/web/src/app/docs/page.tsx (1)
apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)
apps/web/src/components/landing/pricing-section.tsx (3)
apps/web/src/lib/constants.ts (2)
PRICING_TIERS(95-151)SITE_CONFIG(1-20)apps/web/src/lib/utils.ts (1)
cn(4-6)apps/web/src/components/landing/index.ts (1)
PricingSection(5-5)
apps/web/src/components/ui/navigation-menu.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/ui/accordion.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/app/terms/page.tsx (4)
apps/web/src/app/dashboard/page.tsx (1)
metadata(9-12)apps/web/src/app/docs/page.tsx (1)
metadata(9-13)apps/web/src/app/privacy/page.tsx (1)
metadata(8-11)apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)
apps/web/src/components/ui/card.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/animations/radar-animation.tsx (1)
apps/web/src/components/animations/index.ts (1)
RadarAnimation(1-1)
apps/web/src/app/dashboard/page.tsx (6)
apps/web/src/app/docs/page.tsx (1)
metadata(9-13)apps/web/src/app/pricing/page.tsx (1)
metadata(10-13)apps/web/src/app/privacy/page.tsx (1)
metadata(8-11)apps/web/src/app/terms/page.tsx (1)
metadata(8-11)apps/web/src/components/ui/card.tsx (2)
Card(85-85)CardContent(91-91)apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)
apps/web/src/hooks/use-gsap.ts (1)
apps/web/src/lib/gsap-config.ts (2)
gsap(232-232)ScrollTrigger(232-232)
apps/web/src/components/ui/tabs.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/app/pricing/page.tsx (3)
apps/web/src/lib/constants.ts (2)
PRICING_TIERS(95-151)SITE_CONFIG(1-20)apps/web/src/lib/utils.ts (1)
cn(4-6)apps/web/src/components/ui/button.tsx (1)
Button(62-62)
apps/web/src/components/ui/avatar.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/ui/mode-toggle.tsx (1)
apps/web/src/components/ui/button.tsx (1)
Button(62-62)
apps/web/src/components/ui/badge.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/ui/dropdown-menu.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/ui/separator.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/ui/tooltip.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
apps/web/src/components/ui/switch.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(4-6)
🪛 Biome (2.1.2)
apps/web/src/components/layout/header.tsx
[error] 147-150: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
apps/web/src/components/landing/faq-section.tsx
[error] 34-34: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
apps/web/src/components/layout/footer.tsx
[error] 53-53: Alternative text title element cannot be empty
For accessibility purposes, SVGs should have an alternative text, provided via title element. If the svg element has role="img", you should add the aria-label or aria-labelledby attribute.
(lint/a11y/noSvgWithoutTitle)
🪛 markdownlint-cli2 (0.18.1)
apps/web/README.md
1-1: First line in a file should be a top-level heading
(MD041, first-line-heading, first-line-h1)
🔇 Additional comments (62)
apps/web/src/components/ui/navigation-menu.tsx (8)
1-6: LGTM!Imports are clean and appropriate for the component's functionality. All referenced libraries match the manifest versions.
8-30: LGTM!Well-designed extension of the Radix primitive with a
viewportprop that enables both viewport-aware and viewport-less rendering modes. Thedata-viewportattribute enables conditional CSS styling, which is properly leveraged inNavigationMenuContent.
32-59: LGTM!Clean wrapper components with consistent patterns for data-slot attributes and prop forwarding.
85-100: LGTM!The complex class strings correctly handle both viewport and non-viewport rendering modes using Radix's data attributes and Tailwind v4's
**:descendant selector. The animation classes properly leverage Radix'sdata-motionattributes for enter/exit transitions.
102-122: LGTM!Good use of the wrapper div for positioning context and proper integration with Radix's CSS custom properties for dynamic viewport sizing.
124-138: LGTM!Smart use of the
[&_svg:not([class*='text-'])]selector to provide default icon colors while allowing explicit overrides. Focus states are properly handled for accessibility.
140-156: LGTM!The indicator arrow is well-implemented with proper animations for visibility state changes.
158-168: LGTM!All components and the
navigationMenuTriggerStyleutility are properly exported, enabling flexible composition by consumers.apps/web/src/lib/utils.ts (1)
1-6: LGTM!This is the standard and idiomatic utility pattern for composing Tailwind CSS class names. The combination of
clsxfor conditional class handling andtailwind-mergefor conflict resolution is well-established in the ecosystem.apps/web/src/components/ui/separator.tsx (1)
1-28: LGTM!Clean implementation of the Separator component following shadcn/ui patterns. Good use of:
- Radix UI primitive for accessibility
React.ComponentPropsfor proper type inference- Data-attribute selectors for orientation-responsive styling
- Sensible defaults for
orientationanddecorativeapps/web/components.json (1)
1-24: LGTM!Well-configured shadcn/ui setup. The empty
tailwind.configis appropriate for Tailwind CSS v4's CSS-first configuration approach where theme tokens are defined inglobals.cssvia@theme. The path aliases align with standard Next.js conventions.apps/web/.gitignore (1)
1-41: LGTM!Comprehensive .gitignore covering all essential patterns for a Next.js project: dependencies, build outputs, environment files, debug logs, and TypeScript artifacts. The Yarn allowances for patches/plugins are correctly configured.
apps/web/postcss.config.mjs (1)
1-7: LGTM!Correct PostCSS configuration for Tailwind CSS v4, using the
@tailwindcss/postcssplugin as required by the new version.apps/web/src/components/ui/switch.tsx (1)
8-29: LGTM!Well-structured Radix UI Switch wrapper with comprehensive styling covering dark mode, focus states, and disabled states. The component properly forwards all props and uses the
cnutility for className composition.apps/web/tsconfig.json (1)
1-34: LGTM!Well-configured TypeScript setup for Next.js with appropriate settings:
bundlermodule resolution,react-jsxtransform, strict mode, and correct path aliasing. The inclusion of.next/types/**/*.tsensures proper type generation for the App Router.apps/web/next.config.ts (1)
3-6: LGTM!The configuration correctly enables the React Compiler, which is supported by the
babel-plugin-react-compilerdependency in package.json. This is a minimal but appropriate setup for a new Next.js app.apps/web/src/components/theme-provider.tsx (1)
1-11: LGTM!This is a standard and correct pattern for wrapping
next-themesin Next.js App Router. The"use client"directive is required, and usingReact.ComponentPropsfor type inference keeps the wrapper flexible and type-safe.apps/web/package.json (1)
27-30: No issues found. The specified versions Next.js 16.1.1 and React 19.2.3 are valid and represent the current latest releases available on npm. These packages will install successfully.apps/web/src/lib/extension-version.ts (1)
1-2: No action needed — the import path is correctly configured.The relative import
../../../extension/package.jsoncorrectly resolves toapps/extension/package.jsonwithin this monorepo structure. The file exists,resolveJsonModuleis enabled in the tsconfig, and this is a standard pattern for importing package metadata across workspace directories.apps/web/src/components/ui/mode-toggle.tsx (1)
19-22: LGTM!The icon swap implementation using CSS transforms and the
absolutepositioning for the Moon icon is a clean pattern. Thesr-onlylabel ensures accessibility.apps/web/src/components/ui/button.tsx (2)
7-37: LGTM!The
buttonVariantsdefinition is well-structured with comprehensive variant and size options. The base styles include proper focus states, disabled handling, and SVG styling. The Tailwind v4 syntax (has-[>svg]:px-3, etc.) is correctly applied.
39-62: LGTM!The Button component implementation correctly handles the
asChildpattern with Radix Slot, provides sensible defaults, and properly forwards props. The data attributes (data-slot,data-variant,data-size) are useful for styling and testing hooks.apps/web/src/components/ui/badge.tsx (1)
7-26: LGTM!The
badgeVariantsdefinition is well-crafted. The[a&]:hoverselectors cleverly apply hover states only when the badge renders as an anchor element (viaasChild), which is a nice touch for interactive badges.apps/web/src/components/ui/avatar.tsx (1)
1-53: LGTM!Clean wrapper implementation around Radix UI Avatar primitives. The components are properly typed, forward all props, and include
data-slotattributes for styling hooks. The default styles are minimal and appropriate.apps/web/src/app/globals.css (1)
769-785: LGTM!Excellent accessibility consideration with the
prefers-reduced-motion: reducemedia query. The implementation properly disables all animations and smooth scrolling for users who prefer reduced motion.apps/web/src/components/ui/tabs.tsx (1)
1-66: LGTM!Well-structured Tabs component wrappers following the established shadcn/ui pattern. The
TabsTriggerstyling comprehensively handles active, focus, and disabled states with appropriate dark mode variants.apps/web/src/hooks/use-mobile.ts (1)
30-43: LGTM!The
useMediaQueryhook is well-implemented usinguseSyncExternalStorefor proper SSR compatibility and tear-safe subscriptions. The generic design allows for flexible media query matching.apps/web/src/components/ui/card.tsx (1)
1-92: LGTM overall — clean Card component family.The Card components follow a consistent pattern with proper prop forwarding,
data-slotattributes for composition, and thecnutility for class merging. The implementation aligns well with the UI library patterns established elsewhere in this PR.apps/web/src/app/dashboard/page.tsx (1)
9-12: LGTM — clean placeholder dashboard page.The metadata export follows the same pattern as other pages in the PR. The layout effectively communicates the sign-in requirement with appropriate placeholder content.
Also applies to: 14-63
apps/web/src/components/ui/accordion.tsx (1)
1-66: LGTM — well-structured Accordion component set.The implementation correctly wraps Radix UI primitives with consistent styling, proper accessibility structure (Header wrapping Trigger), and the chevron rotation pattern for visual feedback.
apps/web/src/components/ui/dropdown-menu.tsx (2)
62-83: Good implementation of variant support inDropdownMenuItem.The
variantprop with"default" | "destructive"options and corresponding data attribute styling is a clean pattern for providing visual differentiation for dangerous actions.
241-257: LGTM — comprehensive dropdown menu component set.The implementation provides a complete wrapper around Radix UI's dropdown menu primitives with consistent styling, proper accessibility patterns, and useful extensions (variants, inset support, shortcut display).
apps/web/src/lib/constants.ts (5)
1-20: LGTM! Well-structured site configuration.Clean centralization of site metadata with proper
as consttyping. The separation of links and emails into nested objects provides good organization.
22-49: LGTM! Navigation and footer links are well-organized.Good use of the
externalflag for marketplace links and consistent structure across link categories.
51-93: LGTM! Features array is well-defined.Each feature has a unique ID, clear description, and icon mapping. The structure supports easy iteration in UI components.
95-151: LGTM! Pricing tiers with detailed feature flags.The
includedboolean pattern for features enables clear rendering of checkmarks/crosses in pricing tables. Thehighlightedflag for the Pro tier is a nice touch for visual emphasis.
153-224: LGTM! FAQ, testimonials, and stats are comprehensive.FAQ content covers privacy concerns well, which is appropriate for a developer presence tool. Stats provide good social proof metrics.
apps/web/src/components/layout/footer.tsx (1)
60-117: LGTM! Link sections are well-structured.Clean iteration over footer link categories with proper external link handling. The consistent styling across sections maintains visual coherence.
apps/web/src/components/landing/faq-section.tsx (1)
57-67: Nice accordion animation pattern.The grid-based expand/collapse using
grid-rows-[1fr]andgrid-rows-[0fr]with overflow hidden is a clean CSS-only animation approach that avoids height calculations.apps/web/src/app/terms/page.tsx (2)
8-11: LGTM! Metadata is properly configured.Follows the same pattern as other pages in the app (dashboard, docs, privacy).
25-201: LGTM! Terms of Service content is comprehensive.Standard ToS sections covering acceptance, service description, acceptable use, privacy references, subscriptions, IP, disclaimers, liability, and contact information. The structure with Separators provides clear visual delineation.
apps/web/src/app/privacy/page.tsx (2)
20-36: Nice reusable PolicySection component with accent variants.Clean internal component design with color theming. The border-left accent provides good visual hierarchy across sections.
189-208: LGTM! Data security section is well-presented.Clear grid layout showing encryption standards and compliance status. The "SOC 2 (in progress)" is appropriately transparent.
apps/web/src/app/docs/page.tsx (2)
9-13: LGTM! Metadata follows established patterns.Consistent with other page metadata in the app.
104-131: LGTM! Quick start section is well-designed.Clear step-by-step layout with visual step numbers and code snippets. Good onboarding UX.
apps/web/src/components/animations/index.ts (1)
1-1: LGTM! Clean barrel export.Standard pattern for re-exporting components, enabling cleaner import paths.
apps/web/src/components/layout/index.ts (1)
1-3: LGTM!Clean barrel export pattern for consolidating layout component imports.
apps/web/src/app/page.tsx (1)
1-23: LGTM!Clean page composition using barrel imports. The section ordering is logical for a landing page flow.
apps/web/src/components/landing/hero-section.tsx (1)
61-137: LGTM!Good semantic HTML structure with proper heading hierarchy. The responsive grid layout and CTA buttons are well-implemented with appropriate link handling for both external (marketplace) and internal (#features) navigation.
apps/web/src/components/landing/how-it-works-section.tsx (1)
57-123: LGTM!Good responsive layout with proper mobile handling for the connector lines. The dynamic icon rendering pattern (
<step.icon />) is clean.apps/web/src/components/landing/pricing-section.tsx (2)
24-107: LGTM!Well-structured pricing card with proper TypeScript narrowing for the optional
priceNotefield. The conditional styling and link handling are correctly implemented.
109-197: LGTM overall!Good section structure with proper responsive layout and animation. The pricing grid and enterprise CTA are well-implemented.
apps/web/src/components/landing/index.ts (1)
1-7: LGTM!Clean barrel export pattern that consolidates all landing page section components for convenient importing. The naming is consistent and aligns with the module files.
apps/web/src/components/landing/cta-section.tsx (1)
1-100: LGTM!Well-structured CTA component with clean animation patterns using
motion/react. The decorative corner elements, responsive typography, and proper handling of external links (withrel="noopener noreferrer") are all implemented correctly.apps/web/src/components/layout/header.tsx (1)
21-91: LGTM!The sub-components (
Logo,NavItem,MagneticButton) are well-implemented with clean separation of concerns. The magnetic button effect usinguseMotionValueanduseSpringprovides a polished interaction.apps/web/src/app/pricing/page.tsx (2)
10-13: LGTM!Good metadata configuration that aligns with Next.js App Router patterns and the metadata template defined in the root layout.
55-64: LGTM!Clean helper component that handles both boolean (check/x icons) and string values for the comparison table cells.
apps/web/src/components/landing/features-section.tsx (1)
59-79: LGTM - GSAP context with proper cleanup.The GSAP animation setup correctly uses
gsap.context()for scoped animations and properly reverts on unmount. The ScrollTrigger configuration withonce: trueprevents repeated animations.apps/web/src/components/landing/social-proof-section.tsx (1)
71-109: LGTM - TestimonialCard implementation.The component correctly implements entrance animations with staggered delays and proper viewport detection.
apps/web/src/components/animations/radar-animation.tsx (1)
59-61: LGTM - Radar target positioning and animations.The polar-to-Cartesian coordinate conversion is mathematically correct, and the layered animations (pulsing dots, expanding rings, connector lines, info cards) create a cohesive radar effect.
Also applies to: 113-201
apps/web/src/hooks/use-gsap.ts (1)
142-183: LGTM - useHoverAnimation implementation.Proper event listener attachment and cleanup. The fallback
leaveAnimationwith default reset values is a good UX pattern.apps/web/src/lib/gsap-config.ts (1)
1-17: LGTM - GSAP initialization and exports.Good SSR safety with
typeof window !== 'undefined'guard. Re-exportinggsapandScrollTriggerfrom a central config ensures consistent plugin registration across the app.Also applies to: 232-232
| function Tooltip({ | ||
| ...props | ||
| }: React.ComponentProps<typeof TooltipPrimitive.Root>) { | ||
| return ( | ||
| <TooltipProvider> | ||
| <TooltipPrimitive.Root data-slot="tooltip" {...props} /> | ||
| </TooltipProvider> | ||
| ) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Each Tooltip instantiates its own TooltipProvider, breaking shared delay coordination.
Wrapping every Tooltip in its own TooltipProvider prevents multiple tooltips from sharing state (e.g., skipDelayDuration). Per Radix UI best practices, a single TooltipProvider should wrap the app or a subtree of tooltips.
Consider removing the nested provider and relying on consumers to place TooltipProvider higher in the tree:
Suggested fix
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
- <TooltipProvider>
- <TooltipPrimitive.Root data-slot="tooltip" {...props} />
- </TooltipProvider>
+ <TooltipPrimitive.Root data-slot="tooltip" {...props} />
)
}Then wrap your app or layout with <TooltipProvider> once.
📝 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.
| function Tooltip({ | |
| ...props | |
| }: React.ComponentProps<typeof TooltipPrimitive.Root>) { | |
| return ( | |
| <TooltipProvider> | |
| <TooltipPrimitive.Root data-slot="tooltip" {...props} /> | |
| </TooltipProvider> | |
| ) | |
| } | |
| function Tooltip({ | |
| ...props | |
| }: React.ComponentProps<typeof TooltipPrimitive.Root>) { | |
| return ( | |
| <TooltipPrimitive.Root data-slot="tooltip" {...props} /> | |
| ) | |
| } |
🤖 Prompt for AI Agents
In @apps/web/src/components/ui/tooltip.tsx around lines 21 - 29, The Tooltip
component currently wraps each instance in its own TooltipProvider which
prevents shared state like skipDelayDuration; remove the TooltipProvider from
the Tooltip component so Tooltip only renders <TooltipPrimitive.Root ... />
(keep the function name Tooltip and props signature intact) and update
consumers/layout to mount a single TooltipProvider at a higher level (e.g., app
or layout) to enable coordinated tooltip behavior.
| function subscribe(callback: () => void) { | ||
| const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); | ||
| mql.addEventListener('change', callback); | ||
| window.addEventListener('resize', callback); | ||
| return () => { | ||
| mql.removeEventListener('change', callback); | ||
| window.removeEventListener('resize', callback); | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Redundant event listeners in subscribe function.
The subscribe function attaches both a matchMedia change listener and a resize listener. Since the media query listener already fires when the viewport crosses the breakpoint, the resize listener is largely redundant and adds unnecessary overhead.
♻️ Suggested simplification
function subscribe(callback: () => void) {
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
mql.addEventListener('change', callback);
- window.addEventListener('resize', callback);
return () => {
mql.removeEventListener('change', callback);
- window.removeEventListener('resize', callback);
};
}📝 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.
| function subscribe(callback: () => void) { | |
| const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); | |
| mql.addEventListener('change', callback); | |
| window.addEventListener('resize', callback); | |
| return () => { | |
| mql.removeEventListener('change', callback); | |
| window.removeEventListener('resize', callback); | |
| }; | |
| } | |
| function subscribe(callback: () => void) { | |
| const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); | |
| mql.addEventListener('change', callback); | |
| return () => { | |
| mql.removeEventListener('change', callback); | |
| }; | |
| } |
🤖 Prompt for AI Agents
In @apps/web/src/hooks/use-mobile.ts around lines 12 - 20, The subscribe
function is attaching both a matchMedia listener and a redundant window resize
listener; remove the resize-related code so only the MediaQueryList listener is
used. Specifically, in subscribe (and its returned cleanup) drop the
window.addEventListener('resize', callback) and
window.removeEventListener('resize', callback') lines and rely solely on
mql.addEventListener('change', callback) / mql.removeEventListener('change',
callback); keep MOBILE_BREAKPOINT and the rest of the logic unchanged.
| export function useWindowSize() { | ||
| const [size, setSize] = useState({ | ||
| width: 0, | ||
| height: 0, | ||
| }); | ||
|
|
||
| useEffect(() => { | ||
| const handleResize = () => { | ||
| setSize({ | ||
| width: window.innerWidth, | ||
| height: window.innerHeight, | ||
| }); | ||
| }; | ||
|
|
||
| // Initial size | ||
| handleResize(); | ||
|
|
||
| window.addEventListener('resize', handleResize); | ||
| return () => window.removeEventListener('resize', handleResize); | ||
| }, []); | ||
|
|
||
| return size; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Initial zero values may cause layout shifts.
useWindowSize initializes to { width: 0, height: 0 }, which could cause brief layout shifts before the effect runs. If components depend on these values for conditional rendering, consider documenting this behavior or providing an isReady flag.
♻️ Alternative with loading state
export function useWindowSize() {
const [size, setSize] = useState<{
width: number;
height: number;
isReady: boolean;
}>({
width: 0,
height: 0,
isReady: false,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
isReady: true,
});
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}🤖 Prompt for AI Agents
In @apps/web/src/hooks/use-mobile.ts around lines 45 - 67, The hook
useWindowSize currently initializes size to {width: 0, height: 0}, causing
layout shifts; update useWindowSize to initialize state with the actual window
dimensions (using window.innerWidth/innerHeight if available) or add an isReady
boolean (e.g., {width, height, isReady}) that flips true on the first
handleResize call, ensure the effect still registers the resize listener and
cleans it up, and update any callers to handle the new shape or isReady flag (or
document the initial-zero behavior if you choose not to change it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review continued from previous batch...
| useEffect(() => { | ||
| if (!ref.current) return; | ||
|
|
||
| const ctx = gsap.context(() => { | ||
| const fromVars = options?.from || { y: 50, opacity: 0 }; | ||
|
|
||
| gsap.fromTo(ref.current, fromVars, { | ||
| ...animation, | ||
| scrollTrigger: { | ||
| trigger: ref.current, | ||
| start: 'top 85%', | ||
| once: true, | ||
| ...options?.trigger, | ||
| }, | ||
| }); | ||
| }, ref); | ||
|
|
||
| return () => ctx.revert(); | ||
| }, [animation, options]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object dependencies may cause infinite re-renders.
The animation and options parameters are objects. If the caller doesn't memoize them with useMemo, the effect will re-run on every render, potentially causing infinite loops or performance issues. Consider using useRef to store previous values or document the memoization requirement.
🔧 Suggested fix - use JSON comparison or refs
+import { useMemo } from 'react';
+
+// Helper to stabilize object references
+function useStableValue<T>(value: T): T {
+ const ref = useRef<T>(value);
+ const serialized = JSON.stringify(value);
+ const prevSerialized = useRef(serialized);
+
+ if (prevSerialized.current !== serialized) {
+ ref.current = value;
+ prevSerialized.current = serialized;
+ }
+
+ return ref.current;
+}
export function useScrollTrigger<T extends HTMLElement = HTMLDivElement>(
animation: gsap.TweenVars,
options?: {
from?: gsap.TweenVars;
trigger?: ScrollTrigger.Vars;
}
) {
const ref = useRef<T>(null);
+ const stableAnimation = useStableValue(animation);
+ const stableOptions = useStableValue(options);
useEffect(() => {
// ... effect body
- }, [animation, options]);
+ }, [stableAnimation, stableOptions]);Alternatively, document the requirement for callers to memoize these values.
| const ctx = gsap.context(() => { | ||
| gsap.to(valueRef.current, { | ||
| value: endValue, | ||
| duration, | ||
| ease: 'power2.out', | ||
| onUpdate: () => { | ||
| const formatted = | ||
| decimals > 0 | ||
| ? valueRef.current.value.toFixed(decimals) | ||
| : Math.round(valueRef.current.value).toLocaleString(); | ||
| element.textContent = `${prefix}${formatted}${suffix}`; | ||
| }, | ||
| scrollTrigger: { | ||
| trigger: element, | ||
| start: 'top 85%', | ||
| once: true, | ||
| }, | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing scope argument in gsap.context().
Unlike other hooks in this file, useCounterAnimation doesn't pass a scope to gsap.context(). This inconsistency could cause cleanup issues. Pass the element or ref for proper scoping.
🔧 Suggested fix
const ctx = gsap.context(() => {
gsap.to(valueRef.current, {
// ... animation config
});
- });
+ }, element);
return () => ctx.revert();📝 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.
| const ctx = gsap.context(() => { | |
| gsap.to(valueRef.current, { | |
| value: endValue, | |
| duration, | |
| ease: 'power2.out', | |
| onUpdate: () => { | |
| const formatted = | |
| decimals > 0 | |
| ? valueRef.current.value.toFixed(decimals) | |
| : Math.round(valueRef.current.value).toLocaleString(); | |
| element.textContent = `${prefix}${formatted}${suffix}`; | |
| }, | |
| scrollTrigger: { | |
| trigger: element, | |
| start: 'top 85%', | |
| once: true, | |
| }, | |
| }); | |
| }); | |
| const ctx = gsap.context(() => { | |
| gsap.to(valueRef.current, { | |
| value: endValue, | |
| duration, | |
| ease: 'power2.out', | |
| onUpdate: () => { | |
| const formatted = | |
| decimals > 0 | |
| ? valueRef.current.value.toFixed(decimals) | |
| : Math.round(valueRef.current.value).toLocaleString(); | |
| element.textContent = `${prefix}${formatted}${suffix}`; | |
| }, | |
| scrollTrigger: { | |
| trigger: element, | |
| start: 'top 85%', | |
| once: true, | |
| }, | |
| }); | |
| }, element); |
🤖 Prompt for AI Agents
In @apps/web/src/hooks/use-gsap.ts around lines 203 - 221, The gsap.context call
in useCounterAnimation is missing the scope argument which can break proper
scoping and cleanup; update the call to pass the element/ref (e.g., element or
valueRef.current or a container ref) as the first argument to gsap.context, e.g.
gsap.context(scope, () => { ... }), and ensure the returned ctx is reverted in
the hook cleanup to mirror other hooks in this file.
| export function createHeroTimeline(container: HTMLElement) { | ||
| const tl = gsap.timeline({ defaults: { ease: EASINGS.smoothOut } }); | ||
|
|
||
| tl.from(container.querySelector('.hero-badge'), { | ||
| y: 20, | ||
| opacity: 0, | ||
| duration: 0.6, | ||
| }) | ||
| .from( | ||
| container.querySelector('.hero-title'), | ||
| { | ||
| y: 40, | ||
| opacity: 0, | ||
| duration: 0.8, | ||
| }, | ||
| '-=0.3' | ||
| ) | ||
| .from( | ||
| container.querySelector('.hero-subtitle'), | ||
| { | ||
| y: 30, | ||
| opacity: 0, | ||
| duration: 0.7, | ||
| }, | ||
| '-=0.4' | ||
| ) | ||
| .from( | ||
| container.querySelectorAll('.hero-cta'), | ||
| { | ||
| y: 20, | ||
| opacity: 0, | ||
| duration: 0.5, | ||
| stagger: 0.1, | ||
| }, | ||
| '-=0.3' | ||
| ) | ||
| .from( | ||
| container.querySelector('.hero-visual'), | ||
| { | ||
| scale: 0.9, | ||
| opacity: 0, | ||
| duration: 1, | ||
| ease: EASINGS.back, | ||
| }, | ||
| '-=0.5' | ||
| ); | ||
|
|
||
| return tl; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing null checks for queried elements.
createHeroTimeline queries elements by class but doesn't verify they exist. If any element is missing (e.g., .hero-badge), GSAP will receive null and may fail silently or throw errors.
🛡️ Suggested fix - add guards
export function createHeroTimeline(container: HTMLElement) {
const tl = gsap.timeline({ defaults: { ease: EASINGS.smoothOut } });
- tl.from(container.querySelector('.hero-badge'), {
+ const badge = container.querySelector('.hero-badge');
+ const title = container.querySelector('.hero-title');
+ const subtitle = container.querySelector('.hero-subtitle');
+ const ctas = container.querySelectorAll('.hero-cta');
+ const visual = container.querySelector('.hero-visual');
+
+ if (badge) {
+ tl.from(badge, {
y: 20,
opacity: 0,
duration: 0.6,
- })
+ });
+ }
+ // ... similar guards for other elements🤖 Prompt for AI Agents
In @apps/web/src/lib/gsap-config.ts around lines 98 - 146, createHeroTimeline
currently passes querySelector results (e.g., '.hero-badge', '.hero-title',
'.hero-subtitle', '.hero-cta', '.hero-visual') directly to GSAP which can
receive null and fail; update createHeroTimeline to guard each queried
element/NodeList before calling tl.from (for NodeList use length check) — only
add that tween if the element(s) exist, and still return the timeline (or an
early noop timeline) so callers get a valid GSAP timeline even when some
elements are missing.
| export function animateFeatureCards(cards: HTMLElement[]) { | ||
| return gsap.fromTo( | ||
| cards, | ||
| { y: 60, opacity: 0 }, | ||
| { | ||
| y: 0, | ||
| opacity: 1, | ||
| duration: 0.7, | ||
| stagger: 0.1, | ||
| ease: EASINGS.smoothOut, | ||
| scrollTrigger: { | ||
| trigger: cards[0]?.parentElement, | ||
| start: 'top 80%', | ||
| once: true, | ||
| }, | ||
| } | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null dereference when cards array is empty.
cards[0]?.parentElement returns undefined if cards is empty, which will cause ScrollTrigger to use undefined as the trigger. Consider adding an early return guard.
🛡️ Suggested fix
export function animateFeatureCards(cards: HTMLElement[]) {
+ if (cards.length === 0) return null;
+
return gsap.fromTo(
cards,
// ...
);
}
export function animatePricingCards(cards: HTMLElement[]) {
+ if (cards.length === 0) return null;
+
return gsap.fromTo(
cards,
// ...
);
}Also applies to: 167-185
🤖 Prompt for AI Agents
In @apps/web/src/lib/gsap-config.ts around lines 148 - 165, animateFeatureCards
can dereference cards[0] when passed an empty array; add an early guard at the
start of animateFeatureCards that returns early (e.g., null or a no-op) when
cards.length === 0 to avoid passing undefined as the ScrollTrigger trigger, and
apply the same guard to the analogous function around lines 167-185 to prevent
null/undefined triggers.
| export function createParallax(element: HTMLElement, speed: number = 0.5) { | ||
| return gsap.to(element, { | ||
| yPercent: -30 * speed, | ||
| ease: 'none', | ||
| scrollTrigger: { | ||
| trigger: element.closest('section'), | ||
| start: 'top bottom', | ||
| end: 'bottom top', | ||
| scrub: 1, | ||
| }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
element.closest('section') may return null.
If the element isn't inside a <section>, closest('section') returns null, which will cause the ScrollTrigger to use null as the trigger. Add a fallback to the element itself.
🔧 Suggested fix
export function createParallax(element: HTMLElement, speed: number = 0.5) {
return gsap.to(element, {
yPercent: -30 * speed,
ease: 'none',
scrollTrigger: {
- trigger: element.closest('section'),
+ trigger: element.closest('section') ?? element,
start: 'top bottom',
end: 'bottom top',
scrub: 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.
| export function createParallax(element: HTMLElement, speed: number = 0.5) { | |
| return gsap.to(element, { | |
| yPercent: -30 * speed, | |
| ease: 'none', | |
| scrollTrigger: { | |
| trigger: element.closest('section'), | |
| start: 'top bottom', | |
| end: 'bottom top', | |
| scrub: 1, | |
| }, | |
| }); | |
| } | |
| export function createParallax(element: HTMLElement, speed: number = 0.5) { | |
| return gsap.to(element, { | |
| yPercent: -30 * speed, | |
| ease: 'none', | |
| scrollTrigger: { | |
| trigger: element.closest('section') ?? element, | |
| start: 'top bottom', | |
| end: 'bottom top', | |
| scrub: 1, | |
| }, | |
| }); | |
| } |
🤖 Prompt for AI Agents
In @apps/web/src/lib/gsap-config.ts around lines 205 - 216, In createParallax,
guard against element.closest('section') returning null by using the element as
a fallback for the ScrollTrigger trigger; replace the trigger value with
something like const trigger = element.closest('section') || element and pass
that trigger into the scrollTrigger config so ScrollTrigger never receives null.
This commit addresses all valid codereview items from the PR #16 feedback: Configuration & Scripts: - Update package.json lint script to pass explicit target (eslint .) - Remove nested pnpm-workspace.yaml file Documentation: - Add top-level heading to README.md Accessibility & Security: - Add external link attributes (target=_blank, rel=noopener noreferrer) to dashboard, header mobile GitHub link - Add type='button' to toggle buttons in FAQ and header mobile menu - Add aria-label to billing toggle switch - Add accessible title to Discord SVG icon - Add priority prop to radar Image for LCP optimization Code Quality & Best Practices: - Consolidate duplicate :root and .dark blocks in globals.css - Remove duplicate .text-glow CSS rule - Replace hardcoded hex colors with theme tokens in pricing page - Add React.Fragment with key to comparisonFeatures map - Centralize lastUpdated constant (LEGAL_CONFIG) - Use objects instead of split strings for data items - Fix Tailwind arbitrary selectors in Card components - Move data-slot attributes from non-rendering primitives in dropdown-menu - Remove duplicate group class in navigation-menu - Remove redundant bg-foreground from tooltip Arrow - Forward ref in Container component - Memoize currentYear in Footer - Fix suffix detection logic in AnimatedStat - Use unique keys for TestimonialCard components - Guard against undefined Icon in FeatureCard - Add prefers-reduced-motion check in hero-section animations - Extract steps constant to shared module - Fix annual price calculation (Math.round vs Math.floor) - Add JSDoc docstrings to hooks and utils CSS Fixes: - Add accordion keyframe animations - Fix radar-tick responsive transform properties Verified with lint and build checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 37
🤖 Fix all issues with AI agents
In @apps/web/package.json:
- Around line 27-31: The package.json dependency versioning is inconsistent:
core packages "next", "react", and "react-dom" are pinned to exact versions
while others use caret ranges; choose a consistent strategy and update the
dependency strings accordingly (either pin all core deps by removing caret
ranges from "next-themes" and "tailwind-merge" or relax "next", "react", and
"react-dom" to caret ranges) and ensure the package.json entries for "next",
"react", "react-dom", "next-themes", and "tailwind-merge" reflect the chosen
approach so versioning is uniform.
In @apps/web/src/app/dashboard/page.tsx:
- Around line 9-12: The metadata.description currently advertises a full
dashboard while the page renders a sign-in prompt, causing a mismatch; either
update the exported metadata constant (metadata.description) to mention that
authentication/sign-in is required, or implement a server-side auth check inside
the page's server component (the default export in page.tsx) to detect
unauthenticated users and perform a redirect to the login route (e.g., /login)
before rendering; locate and update the metadata object or add the auth-check
and redirect logic in the page component to ensure metadata and actual behavior
align.
In @apps/web/src/app/globals.css:
- Around line 498-502: The .cursor-blink::after rule hardcodes color: #171717
which breaks dark mode; change it to use an inheritable value (e.g., color:
currentColor or color: var(--cursor-caret)) and define that variable in your
theme roots or component styles so the caret inherits the appropriate light/dark
color; update the .cursor-blink::after selector and ensure any theme root (e.g.,
:root / .dark) sets --cursor-caret (or rely on currentColor by ensuring the
element's color is set) so the blinking cursor adapts to the active theme.
- Around line 536-550: The media queries for .radar-tick currently replace the
entire transform, removing the base translate and rotate; update the .radar-tick
rules inside the @media (min-width: 768px) and @media (min-width: 1024px) blocks
to preserve translate(-50%, -50%) and rotate(var(--deg, 0deg)) while only
changing the translateY value (e.g., set transform to translate(-50%, -50%)
rotate(var(--deg, 0deg)) translateY(...)), so the centering and rotation from
the base rule remain intact across breakpoints.
- Around line 202-205: The ::selection rule uses hardcoded light-mode colors
causing poor contrast in dark mode; update it to use theme-aware CSS variables
or media queries instead. Replace the fixed background and color in ::selection
with references to your theme variables (e.g., --selection-bg,
--selection-color) or add a @media (prefers-color-scheme: dark) /
[data-theme="dark"] block to override ::selection for dark mode so selection
background and text color adapt appropriately (adjust the variables or overrides
where :root or your theme declarations are defined).
In @apps/web/src/app/layout.tsx:
- Around line 66-72: The twitter creator extraction in layout.tsx is brittle
because SITE_CONFIG.links.twitter.replace('https://twitter.com/', '@') assumes a
fixed URL format; add a dedicated twitter handle field (e.g.,
SITE_CONFIG.social.twitterHandle or SITE_CONFIG.twitterHandle) in SITE_CONFIG
and update the twitter.creator usage in layout.tsx to read that field (falling
back to undefined or a safe default if absent) so you use a canonical handle
instead of parsing the URL at runtime.
In @apps/web/src/app/pricing/page.tsx:
- Around line 138-202: The table uses hardcoded hex color classes (e.g.,
text-[#FAFAFA], text-[#737373], border-[#262626], bg-[#0A0A0A], bg-[#FFB800]/5,
border-[#1A1A1A], text-[#FFB800]) in the pricing table rendering (the <h2>,
paragraph, the table header <th>, category row <tr>, feature cells <td>, and the
Pro column highlight) — replace those with the design system semantic Tailwind
tokens (e.g., text-foreground, text-muted-foreground, border-border, bg-card,
text-primary, bg-primary/5 or bg-accent/5) in the JSX where comparisonFeatures
is mapped and FeatureValue is used so the table respects theme changes and stays
consistent with the rest of the app.
- Around line 56-65: FeatureValue uses hardcoded hex colors for icons and text
which bypasses the design tokens; update the JSX in FeatureValue so the Check
icon uses the design token class text-primary, the X icon uses text-muted, and
the returned span uses text-foreground instead of the hex values (keep the
component signature and boolean/string handling the same, and only replace the
className color tokens on Check, X, and the span).
In @apps/web/src/app/privacy/page.tsx:
- Around line 20-36: The accent color mapping in PolicySection uses hardcoded
hex values in accentColors; replace those hex strings with design tokens (CSS
variables) or Tailwind theme classes to keep colors consistent with the design
system — e.g., update the accentColors entries in the PolicySection function to
use variables like "border-l-[var(--color-amber)]" or Tailwind token classes
like "border-l-amber-500", and if using Tailwind tokens add the corresponding
colors to the Tailwind config/theme so the mapping (accentColors in
PolicySection) references the design-token names rather than literal hex codes.
- Around line 68-94: The three static arrays weCollect, neverCollect, and
yourControls are defined inside the PrivacyPage component causing them to be
recreated on every render; move them to module-level constants outside the
PrivacyPage function (e.g., export or const weCollect = [...], neverCollect =
[...], yourControls = [...]) and keep using those names inside the component so
renders reuse the same references.
In @apps/web/src/components/animations/radar-animation.tsx:
- Around line 68-79: The inline transform on the radar tick elements overrides
the CSS .radar-tick transform (which includes the translateY offset); instead of
setting transform in the JSX map, set a CSS custom property with the degree and
let the .radar-tick CSS perform the translate/rotate using that property—replace
the inline transform in the map with a style that only sets --deg: `${deg}deg`
(keep other inline styles like boxShadow if needed) so the class rule (e.g.,
using rotate(var(--deg)) plus the existing
translateY(-148px)/translate(-50%,-50%)) controls final positioning.
In @apps/web/src/components/landing/faq-section.tsx:
- Around line 103-113: Add a stable unique id to each entry in FAQ_ITEMS and
switch the list rendering to use that id for the React key; update the FAQ_ITEMS
data structure (and its type/interface if present) to include an id property for
every item, then change the map call that renders <FAQItem ... /> to pass
key={item.id} instead of key={item.question} and ensure any consumers (e.g.,
FAQItem props) still receive the same item shape.
In @apps/web/src/components/landing/features-section.tsx:
- Around line 25-31: The FeatureCard currently returns null when
iconMap[feature.icon] is missing, causing silent removal of the card; update
FeatureCard to use a fallback icon (e.g., DefaultIcon) when Icon is undefined
and, in development only, emit a console.warn or logger message including
feature.icon and index to aid debugging; ensure you reference iconMap,
feature.icon, Icon, and FeatureCard when implementing the fallback and the
dev-only warning so the card still renders with the fallback icon instead of
disappearing.
In @apps/web/src/components/landing/how-it-works-section.tsx:
- Around line 20-38: The GSAP animation in the useEffect runs unconditionally;
add a prefers-reduced-motion guard (same approach used in hero-section.tsx) so
animations are skipped for users who request reduced motion: inside the
useEffect, detect reduced motion via window.matchMedia('(prefers-reduced-motion:
reduce)').matches (or reuse an existing prefersReducedMotion helper) and return
early if true before creating gsap.context/gsap.from for '.step-connector',
ensuring you still call ctx.revert() only when the animation was created.
- Around line 89-94: The IIFE used to render icons is unnecessary; before
returning the JSX inside the HOW_IT_WORKS_STEPS.map callback, perform the icon
lookup once (e.g., const StepIcon = stepIconMap[step.icon]) and then use
conditional rendering in the JSX (StepIcon && <StepIcon ... />) instead of the
self-invoking function; update the map callback where stepIconMap is referenced
so the icon variable is declared above the returned markup and remove the IIFE
wrapper.
In @apps/web/src/components/landing/pricing-section.tsx:
- Around line 90-102: Extract the nested ternary into a small helper that
returns the href for a tier (e.g., getTierHref or computeCtaHref) and replace
the inline expression in the Link href with a call to that helper; the helper
should accept the tier (or tier.id) and use SITE_CONFIG to return
`mailto:${SITE_CONFIG.email.hello}` for tier.id === 'team',
SITE_CONFIG.links.marketplace for tier.id === 'free', and '/dashboard'
otherwise, keeping the Link, Button and tier.cta usage unchanged.
In @apps/web/src/components/landing/social-proof-section.tsx:
- Around line 60-74: The AnimatedStat component currently uses two separate
in-view triggers (the useInView hook and motion.div's whileInView) which can
fire at different times; change it to a single trigger by removing whileInView
and using the useInView result (inView) to drive the motion.div's
animate/initial state and to start the counter (displayValue) so both
opacity/translate and the number animation use the same inView margins; update
the ref usage (ref passed to useInView) and replace whileInView with animate={{
opacity: inView ? 1 : 0, y: inView ? 0 : 40 }} (or equivalent) so the counter
and fade/slide always start together.
- Around line 10-58: AnimatedStat's numeric parsing strips decimals and loses
the percent suffix, causing values like "99.9%" to animate to 999; update the
parsing and formatting to preserve decimals and percent signs: detect a percent
suffix (e.g., hasPercent = stat.value.includes('%')), allow the decimal point in
the regex (e.g., replace(/[^0-9.]/g, '')), parse with parseFloat instead of
parseInt, and compute targetValue as a number (possibly fractional); during each
animation tick format the current value with the same decimal precision as the
original (determine precision from stat.value.indexOf('.') and toFixed),
preserve and append the correct suffixes (%, +, K/M/ms) and use locale
formatting for integer/decimal parts where appropriate, then
setDisplayValue(formatted) and still snap to stat.value at the end; update
references in the AnimatedStat function (variables numericValue, targetValue,
hasPlus, hasK, hasM, hasMs) and the formatting block so percent and decimal
values animate smoothly.
In @apps/web/src/components/layout/container.tsx:
- Around line 4-9: ContainerProps and the component's forwardRef are currently
tied to HTMLDivElement while the as prop allows 'section'|'article'|'main';
change the typing to be broader by replacing HTMLDivElement with HTMLElement in
both the forwardRef generic and the base props (e.g., extend
React.HTMLAttributes<HTMLElement> and use forwardRef<HTMLElement,
ContainerProps>) or alternatively convert ContainerProps into a generic
polymorphic prop pattern; update any ref usages accordingly to use HTMLElement
so refs remain correct for section/article/main variants.
In @apps/web/src/components/layout/footer.tsx:
- Around line 1-9: The Footer component uses the React hook useMemo for
currentYear inside the function Footer, so either mark the file as a Client
Component by adding the 'use client' directive at the very top of the file or
remove the useMemo usage and replace it with a plain calculation (e.g., assign
currentYear = new Date().getFullYear() directly) to keep Footer a Server
Component; update the declaration referencing currentYear accordingly.
- Around line 91-125: The Company and Legal sections in footer.tsx iterate
FOOTER_LINKS.company and FOOTER_LINKS.legal but do not honor the link.external
flag like the Product section does; update the map callbacks for both COMPANY
and LEGAL blocks to follow the same pattern used in the Product section: check
link.external and when true render an external anchor with target="_blank" and
rel="noopener noreferrer" (or use the same ExternalLink handling used
elsewhere), otherwise render the internal Next.js Link; reference the same Link
usage and key={link.href} in each <li> so behavior is consistent across
FOOTER_LINKS entries.
In @apps/web/src/components/layout/header.tsx:
- Around line 147-175: The mobile menu toggle button (the button that calls
setIsMobileMenuOpen and uses isMobileMenuOpen to render X/Menu) lacks an
accessible name and state exposure; update that button to include an aria-label
(e.g., "Open mobile menu" / "Close mobile menu" determined by isMobileMenuOpen)
and add aria-expanded={isMobileMenuOpen} (and optionally aria-controls
referencing the mobile menu container id) so screen readers know its purpose and
current state.
In @apps/web/src/components/ui/badge.tsx:
- Line 5: Remove the unused import of the utility cn from the top of the file;
since badgeVariants (from cva) handles merging classNames, delete the import
statement "import { cn } from '@/lib/utils';" and ensure no remaining references
to cn exist in the Badge component or its props (leave badgeVariants usage
as-is).
In @apps/web/src/components/ui/card.tsx:
- Around line 65-72: The Tailwind selector used in CardFooter is invalid —
replace the current `[.border-t]:pt-6` token in the className of the CardFooter
component with the correct self-selector `[&.border-t]:pt-6` so the element
applies pt-6 only when it itself has the border-t class; update the className in
the CardFooter function accordingly.
In @apps/web/src/components/ui/dropdown-menu.tsx:
- Around line 25-43: DropdownMenuContent currently always wraps its children in
DropdownMenuPrimitive.Portal, which duplicates/invalidates the separate
DropdownMenuPortal export; remove the internal Portal from DropdownMenuContent
(keep the component as a thin wrapper that renders DropdownMenuPrimitive.Content
directly) and rely on the existing DropdownMenuPortal export for consumers who
need portal behavior, or alternatively remove the DropdownMenuPortal export if
you prefer the component to always portal — choose one approach and update
DropdownMenuContent or DropdownMenuPortal accordingly (refer to
DropdownMenuContent and DropdownMenuPortal symbols to locate the changes).
In @apps/web/src/components/ui/navigation-menu.tsx:
- Around line 1-6: This file is missing the 'use client' directive, causing
Radix UI primitives (NavigationMenuPrimitive/ChevronDownIcon) that rely on
hooks/browser APIs to fail in Server Components; fix by adding the "use client"
directive as the very first line of the module (before any imports) in
apps/web/src/components/ui/navigation-menu.tsx so the NavigationMenu component
and imports from @radix-ui/react-navigation-menu render on the client.
In @apps/web/src/components/ui/tooltip.tsx:
- Around line 21-27: The Tooltip component currently wraps every instance with
its own TooltipProvider (see Tooltip and TooltipProvider around
TooltipPrimitive.Root), which prevents shared timing like skipDelayDuration
across multiple tooltips; either add documentation explaining consumers can wrap
a larger tree with a single TooltipProvider to coordinate delays (mention
skipDelayDuration) or move the TooltipProvider out of the Tooltip component into
a higher-level component (e.g., root layout) so TooltipPrimitive.Root instances
share provider state; update docs or the layout accordingly to reflect the
chosen approach.
In @apps/web/src/hooks/use-gsap.ts:
- Around line 94-135: The effect in useStaggerAnimation is re-running infinitely
because complex objects (animation and options) are used directly in the
dependency array; change the dependency tracking to stable serializable values
by replacing [selector, animation, options] with [selector,
JSON.stringify(animation), JSON.stringify(options)] inside useEffect to prevent
unnecessary reruns, and add a comment in useStaggerAnimation indicating that if
callers pass non-serializable values (like functions), they must memoize them
(e.g., useMemo or useRef) so the hook stays stable.
In @apps/web/src/lib/constants.ts:
- Around line 196-224: The TESTIMONIALS constant includes an unused avatar
field; update either the data or the component: remove the avatar property from
each object in TESTIMONIALS if you want to keep TestimonialCard (in
social-proof-section.tsx) rendering initials only, or modify TestimonialCard to
render an <img> with src set to the testimonial.avatar (fallback to initial if
missing) so the avatar path is used; reference TESTIMONIALS and the
TestimonialCard component in social-proof-section.tsx when making the change.
- Around line 28-49: FOOTER_LINKS currently uses a top-level "as const" but has
inconsistent nested shapes (some entries include external, some include icon)
causing weaker IDE/type support; define an explicit type (e.g., a union
FooterLink = { href: string; label: string; external?: true } | { href: string;
label: string; icon: 'github' | 'twitter' | 'discord' } and typed groups like
Record<'product'|'company'|'legal'|'social', ReadonlyArray<FooterLink>>) and
annotate the FOOTER_LINKS constant with that type instead of relying only on "as
const" so each nested array element has a clear, consistent shape and IDE
autocomplete/type-checking works.
- Around line 219-224: The uptime stat in STATS uses a decimal percentage string
("99.9%") which AnimatedStat can't animate correctly; update the STATS entry
(the { value: '99.9%', label: 'Uptime' } item in apps/web/src/lib/constants.ts)
to use a whole-number value or separate numeric value and suffix (e.g., { value:
'99%', label: 'Uptime' } or { value: '99', suffix: '%', label: 'Uptime' }) so
AnimatedStat in social-proof-section.tsx receives an integer-only value it can
animate properly.
- Around line 35-39: The navigation 'company' array in constants.ts currently
contains links to '/about', '/blog', and '/careers' that aren't implemented and
cause 404s; either create corresponding pages (add page.tsx exports under
app/about, app/blog, app/careers) that render the expected content or
remove/replace those entries from the company array so the navigation only
points to existing routes; if you prefer redirects, add route handlers or
next.config redirects for those paths and update the company array accordingly
to reference the implemented targets.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (28)
apps/web/README.mdapps/web/package.jsonapps/web/src/app/dashboard/page.tsxapps/web/src/app/docs/page.tsxapps/web/src/app/globals.cssapps/web/src/app/layout.tsxapps/web/src/app/pricing/page.tsxapps/web/src/app/privacy/page.tsxapps/web/src/app/terms/page.tsxapps/web/src/components/animations/radar-animation.tsxapps/web/src/components/landing/faq-section.tsxapps/web/src/components/landing/features-section.tsxapps/web/src/components/landing/hero-section.tsxapps/web/src/components/landing/how-it-works-section.tsxapps/web/src/components/landing/pricing-section.tsxapps/web/src/components/landing/social-proof-section.tsxapps/web/src/components/layout/container.tsxapps/web/src/components/layout/footer.tsxapps/web/src/components/layout/header.tsxapps/web/src/components/ui/badge.tsxapps/web/src/components/ui/card.tsxapps/web/src/components/ui/dropdown-menu.tsxapps/web/src/components/ui/mode-toggle.tsxapps/web/src/components/ui/navigation-menu.tsxapps/web/src/components/ui/tooltip.tsxapps/web/src/hooks/use-gsap.tsapps/web/src/lib/constants.tsapps/web/src/lib/utils.ts
🧰 Additional context used
🧬 Code graph analysis (14)
apps/web/src/app/dashboard/page.tsx (6)
apps/web/src/app/docs/page.tsx (1)
metadata(9-13)apps/web/src/app/layout.tsx (1)
metadata(31-77)apps/web/src/app/pricing/page.tsx (1)
metadata(11-14)apps/web/src/app/privacy/page.tsx (1)
metadata(8-11)apps/web/src/components/ui/card.tsx (2)
Card(75-75)CardContent(75-75)apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)
apps/web/src/components/ui/mode-toggle.tsx (1)
apps/web/src/components/ui/button.tsx (1)
Button(62-62)
apps/web/src/components/layout/header.tsx (3)
apps/web/src/components/layout/container.tsx (1)
Container(18-29)apps/web/src/lib/constants.ts (2)
NAV_LINKS(22-26)SITE_CONFIG(1-20)apps/web/src/components/ui/mode-toggle.tsx (1)
ModeToggle(9-25)
apps/web/src/components/ui/card.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(9-11)
apps/web/src/app/docs/page.tsx (7)
apps/web/src/app/dashboard/page.tsx (1)
metadata(9-12)apps/web/src/app/layout.tsx (1)
metadata(31-77)apps/web/src/app/pricing/page.tsx (1)
metadata(11-14)apps/web/src/app/privacy/page.tsx (1)
metadata(8-11)apps/web/src/app/terms/page.tsx (1)
metadata(8-11)apps/web/src/components/layout/container.tsx (1)
Container(18-29)apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)
apps/web/src/components/ui/tooltip.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(9-11)
apps/web/src/components/layout/container.tsx (2)
apps/web/src/components/layout/index.ts (1)
Container(1-1)apps/web/src/lib/utils.ts (1)
cn(9-11)
apps/web/src/components/animations/radar-animation.tsx (1)
apps/web/src/components/animations/index.ts (1)
RadarAnimation(1-1)
apps/web/src/app/layout.tsx (3)
apps/web/src/lib/constants.ts (1)
SITE_CONFIG(1-20)apps/web/src/components/theme-provider.tsx (1)
ThemeProvider(6-11)apps/web/src/components/layout/footer.tsx (1)
Footer(8-141)
apps/web/src/app/privacy/page.tsx (3)
apps/web/src/app/layout.tsx (1)
metadata(31-77)apps/web/src/components/layout/container.tsx (1)
Container(18-29)apps/web/src/lib/constants.ts (2)
LEGAL_CONFIG(250-252)SITE_CONFIG(1-20)
apps/web/src/components/ui/navigation-menu.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(9-11)
apps/web/src/app/pricing/page.tsx (3)
apps/web/src/lib/constants.ts (2)
PRICING_TIERS(95-151)SITE_CONFIG(1-20)apps/web/src/lib/utils.ts (1)
cn(9-11)apps/web/src/components/ui/button.tsx (1)
Button(62-62)
apps/web/src/hooks/use-gsap.ts (1)
apps/web/src/lib/gsap-config.ts (2)
gsap(232-232)ScrollTrigger(232-232)
apps/web/src/components/ui/dropdown-menu.tsx (1)
apps/web/src/lib/utils.ts (1)
cn(9-11)
🪛 GitHub Check: Build and Test
apps/web/src/components/ui/badge.tsx
[warning] 5-5:
'cn' is defined but never used
🔇 Additional comments (67)
apps/web/README.md (1)
1-38: LGTM!Standard Next.js boilerplate README with appropriate documentation for getting started, development, and deployment. Consider customizing with project-specific details as the application matures.
apps/web/src/lib/utils.ts (1)
1-11: LGTM!Standard and correct implementation of the
cnutility usingclsxandtailwind-merge. This is the idiomatic pattern for merging Tailwind classes with proper precedence handling in shadcn/ui-style component libraries.apps/web/src/components/ui/mode-toggle.tsx (1)
9-24: LGTM!Well-implemented theme toggle with good accessibility (sr-only label) and smooth CSS transitions between icons. The fallback to
'light'on line 11 handles the initial undefined state fromresolvedThemeappropriately.Note: On first render before hydration,
resolvedThemeisundefined, so the icons may briefly show the light theme state regardless of the actual theme. This is generally acceptable behavior withnext-themes, but if you want to avoid the flash, consider addingsuppressHydrationWarningor conditionally rendering based on mount state.apps/web/src/components/ui/badge.tsx (1)
26-37: LGTM!Well-structured Badge component following the shadcn/ui pattern with proper variant typing,
asChildsupport via Radix Slot, and comprehensive styling for accessibility states (focus-visible, aria-invalid).apps/web/package.json (1)
34-42: Tailwind CSS v4 is correctly configured.The project uses Tailwind CSS v4 (
tailwindcss: ^4,@tailwindcss/postcss: ^4). Verification confirms the setup follows v4 conventions:postcss.config.mjsuses the@tailwindcss/postcssplugin,globals.cssuses the new@import 'tailwindcss'syntax, and the@theme inlinedirective provides CSS-first configuration. No action needed.apps/web/src/components/layout/container.tsx (1)
11-16: LGTM!The
sizeClassesmap provides clear max-width variants, and the base classes (mx-auto w-full px-4 sm:px-6 lg:px-8) establish a solid responsive foundation with proper centering and progressive padding.Also applies to: 23-26
apps/web/src/app/globals.css (3)
1-47: LGTM!The Tailwind v4 configuration with
@theme inlineproperly maps CSS variables to design tokens, enabling consistent theming across the application.
49-183: LGTM!The CSS variable system is well-organized with comprehensive light/dark theme coverage. The semantic naming (primary, secondary, muted, accent, destructive) aligns with common design system patterns and enables consistent theming.
687-702: LGTM!Excellent accessibility consideration with
prefers-reduced-motionsupport. The implementation properly disables continuous animations while using minimal transition durations for essential UI feedback.apps/web/src/components/ui/tooltip.tsx (1)
33-55: LGTM!The
TooltipContentimplementation properly wraps Radix primitives with Portal for correct stacking context, includes comprehensive animation states, and renders a styled arrow. ThesideOffset=0is a valid design choice for flush positioning.apps/web/src/components/ui/card.tsx (1)
5-16: LGTM!The Card component set is well-structured with semantic
data-slotattributes enabling flexible slot-based composition. TheCardHeadergrid layout withhas-data-[slot=card-action]is a nice pattern for responsive action button placement.Also applies to: 18-29, 31-63
apps/web/src/components/animations/radar-animation.tsx (2)
6-57: LGTM!The
RADAR_TARGETSconstant is well-structured with theas constassertion providing proper type inference. The angles are distributed around the radar face, and the staggered delays create a nice reveal effect.
113-201: LGTM!The polar-to-cartesian coordinate conversion is correctly implemented with the
-90offset positioning 0° at the top. The staggered animation delays create a pleasing reveal sequence, and the directional card positioning (isLeftSide) properly handles left/right placement based on angle.apps/web/src/components/landing/hero-section.tsx (3)
16-23: Elements invisible when reduced motion is preferred.When
prefers-reduced-motionis enabled, the effect returns early without running animations. However, GSAP's.from()sets initial styles (opacity: 0, transforms) that never get applied or reverted, so the hero content remains visible. This is actually fine since.from()animates from the specified values to the current state—if the animation never runs, elements stay at their natural CSS state.After re-examination: the logic is correct. GSAP
.from()would animate from hidden to visible, so skipping it means elements stay visible by default.
25-65: GSAP animation setup is well-structured.Good use of
gsap.context()for scoped selector queries and proper cleanup viactx.revert(). The timeline sequencing with staggered animations creates a polished entrance effect.
67-143: Hero section structure looks good.The two-column responsive grid, proper external link handling (
rel="noopener noreferrer"), and semantic HTML structure are well implemented. TheasChildpattern on Button components correctly composes with Next.js Link.apps/web/src/components/ui/dropdown-menu.tsx (2)
49-70: Well-structured DropdownMenuItem with variant support.The
insetandvariantprops extend Radix's base functionality cleanly. Thedata-*attributes enable CSS-based styling and testing hooks.
212-228: Exports are comprehensive.All necessary components are exported for consumers to compose dropdown menus flexibly.
apps/web/src/components/ui/navigation-menu.tsx (3)
8-30: NavigationMenu with optional viewport is well-designed.The
viewportprop provides flexibility for different navigation patterns, and thedata-viewportattribute enables conditional CSS styling. Good composition pattern.
58-80: NavigationMenuTrigger is properly implemented.The shared
navigationMenuTriggerStyleviacvapromotes consistency. The chevron icon correctly usesaria-hidden="true"since it's decorative.
151-161: Exports are complete.All components and the
navigationMenuTriggerStyleare exported, enabling full composition flexibility.apps/web/src/components/layout/footer.tsx (1)
28-65: Social icons have good accessibility.Each link has an appropriate
aria-label, and the custom Discord SVG correctly usesrole="img"witharia-labelledbypointing to a<title>element.apps/web/src/components/landing/how-it-works-section.tsx (2)
46-63: Motion animations are well-configured.Good use of
whileInViewwithviewport={{ once: true }}to prevent re-triggering. The staggered delays create a pleasant reveal effect.
65-108: Steps grid implementation is solid.The conditional connector line rendering for medium+ screens and the motion-based entrance animations create a polished experience.
apps/web/src/components/landing/pricing-section.tsx (2)
1-18: LGTM!Clean imports and well-typed interface definition. The
PricingCardPropsinterface correctly derives the tier type fromPRICING_TIERS.
109-198: LGTM!The
PricingSectioncomponent is well-structured with proper state management, good accessibility (aria-label on the billing toggle), and clean animation setup with staggered delays.apps/web/src/components/landing/faq-section.tsx (1)
11-73: LGTM!The
FAQItemcomponent has good accessibility patterns: propertype="button"attribute, semantic structure, and smooth CSS-based expand/collapse animation usinggrid-rows.apps/web/src/app/terms/page.tsx (2)
1-22: LGTM!Clean setup with proper metadata export and consistent structure matching other legal pages in the PR. The use of
LEGAL_CONFIG.lastUpdatedcentralizes the update timestamp for maintainability.
23-199: LGTM!Well-structured Terms of Service content with proper semantic HTML, consistent styling, and appropriate internal/external links. The use of
Separatorcomponents provides clear visual separation between sections.apps/web/src/app/dashboard/page.tsx (1)
14-65: LGTM!Clean placeholder UI with proper security attributes on the external link (
target="_blank"withrel="noopener noreferrer"). The centered layout with Lock icon clearly communicates the authentication requirement.apps/web/src/app/layout.tsx (2)
10-29: LGTM!Good font configuration with proper CSS variable assignments,
display: 'swap'for performance, and appropriate weight selections for each font's use case (display, mono, body).
79-112: LGTM!Well-structured root layout with:
- Appropriate
suppressHydrationWarningfor theme hydration- Standard
ThemeProviderconfiguration with system theme support- Proper semantic structure with Header, main, and Footer
apps/web/src/app/docs/page.tsx (6)
1-13: LGTM!Imports and metadata are well-structured. The metadata follows the established pattern used across other pages (dashboard, privacy, pricing, terms) and will properly integrate with the root layout's template configuration.
15-34: LGTM!The quick start steps data structure is clean and self-documenting. Keeping this page-specific content colocated with the component is a reasonable choice.
50-97: LGTM!The hero section is well-structured with proper responsive design, semantic HTML, and consistent use of design tokens. The CTA buttons correctly use the
asChildpattern with Next.jsLinkcomponents.
99-126: LGTM!The Quick Start section renders the steps cleanly with good visual hierarchy. The code snippet display with the Terminal icon provides clear context for users.
128-162: LGTM!The Explore the Docs grid section is well-implemented with proper responsive columns and hover states. The card-hover interaction provides good feedback.
164-191: LGTM!The help section provides clear contact options with proper external link handling for Discord and correct mailto format for support email.
apps/web/src/components/landing/features-section.tsx (4)
1-18: LGTM!The imports and icon mapping are well-organized. Using a Record type for the icon map with explicit LucideIcon typing provides good type safety for dynamic icon resolution.
33-61: LGTM!The FeatureCard animation implementation is solid with good use of
whileInView, appropriate viewport margins, and smooth easing. The alternating x-offset creates visual interest without being distracting.
63-83: LGTM!The GSAP setup is well-implemented with proper context scoping to
sectionRefand correct cleanup viactx.revert(). The scroll-triggered animation for the section line is a nice polish detail.
85-121: LGTM!The section structure is clean with proper semantic HTML, responsive grid layouts, and good separation between the header and feature cards grid. The background layers with
z-10on the container ensure proper stacking.apps/web/src/app/privacy/page.tsx (5)
1-11: LGTM!Imports and metadata are well-structured. The metadata description clearly communicates the page purpose for SEO.
38-66: LGTM!The DataList component effectively communicates what is/isn't collected with clear visual indicators. Using index as key is acceptable here since the data arrays are static and never reordered.
95-161: LGTM!The page header and data collection sections are well-structured with clear visual hierarchy. Using
LEGAL_CONFIG.lastUpdatedfor the timestamp is good practice for centralized legal date management.
163-211: LGTM!The Data Sharing and Security sections effectively communicate privacy commitments with clear visual formatting. The security grid provides scannable information about encryption and compliance status.
213-294: LGTM!The remaining sections (Data Retention, Third-Party Services, Children's Privacy, Changes, and Contact) provide comprehensive privacy coverage. The retention tiers visualization and contact CTA are well-implemented.
apps/web/src/app/pricing/page.tsx (5)
1-14: LGTM!Imports and metadata are properly structured. The explicit React import for
React.Fragmentusage is appropriate.
16-54: LGTM!The comparison features data is well-organized by category with consistent structure. The mix of boolean and string values for feature availability is handled correctly by the FeatureValue component.
67-90: LGTM!The hero section follows the established pattern from other pages with proper responsive typography and visual hierarchy.
92-136: LGTM!The pricing tiers grid correctly renders from
PRICING_TIERSwith proper conditional styling for the highlighted tier. The CTA link logic correctly routes Team tier to sales contact and others to the marketplace.
204-228: LGTM!The CTA section provides a clear call-to-action with proper link configuration to the marketplace.
apps/web/src/components/layout/header.tsx (7)
1-19: LGTM!Imports are well-organized with motion utilities from
motion/reactand proper component/constant imports. The separation of animation-related imports is clean.
21-32: LGTM!The Logo component is well-implemented with explicit image dimensions (preventing layout shift) and accessible alt text. The z-50 ensures it remains clickable when the mobile menu is open.
34-44: LGTM!The NavItem component provides clean hover feedback with the animated underline. The CSS-only approach is performant.
99-108: LGTM!The body overflow management correctly prevents background scrolling when the mobile menu is open, with proper cleanup on unmount.
110-145: LGTM!The header structure is well-implemented with proper scroll-based opacity transition, responsive navigation layout, and correctly configured external links with
rel="noopener noreferrer".
180-249: LGTM!The mobile menu implementation is polished with smooth slide animation, staggered link reveals, and proper menu closure on navigation. The CTA and social links provide good mobile UX.
46-91: LGTM. The magnetic button effect is well-implemented with proper spring physics and the shine animation adds polish. Thebg-linear-to-rsyntax on line 86 is correct for Tailwind v4.apps/web/src/components/landing/social-proof-section.tsx (2)
77-115: LGTM!The
TestimonialCardcomponent is well-structured with appropriate entrance animations, proper use of semantic HTML (blockquote), and clean styling. The staggered delay based on index creates a nice cascading effect.
117-165: LGTM!The
SocialProofSectioncomposition is clean with proper responsive grid layouts, consistent animation patterns, and good separation of concerns between the stats section and testimonials section.apps/web/src/hooks/use-gsap.ts (2)
11-45: LGTM!The
useGsapAnimationhook correctly manages timeline lifecycle with proper cleanup. UsinguseCallbackfor theanimateandcreateTimelinefunctions ensures stable references.
143-166: LGTM!The
useParallaxhook is well-implemented with a primitivespeeddependency that won't cause re-render issues. The GSAP context cleanup is properly handled.apps/web/src/lib/constants.ts (4)
1-20: LGTM!The
SITE_CONFIGconstant is well-structured with proper organization of metadata, links, and email addresses. Usingas constensures type safety with literal types.
51-93: LGTM!The
FEATURESarray is well-organized with consistent structure. Each feature has a unique id, clear title, descriptive text, and associated icon name.
95-151: LGTM!The
PRICING_TIERSconstant has a clean structure with progressive feature sets. Thehighlightedflag for the Pro tier and optionalpriceNotefor Team tier show good flexibility in the data model.
226-252: LGTM!The
HOW_IT_WORKS_STEPSandLEGAL_CONFIGconstants are well-defined with appropriate structure and sensible defaults.
| "next": "16.1.1", | ||
| "next-themes": "^0.4.6", | ||
| "react": "19.2.3", | ||
| "react-dom": "19.2.3", | ||
| "tailwind-merge": "^3.4.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify that the specified versions exist on npm
echo "Checking Next.js 16.1.1..."
npm view next@16.1.1 version 2>/dev/null || echo "Next.js 16.1.1 NOT FOUND"
echo ""
echo "Checking React 19.2.3..."
npm view react@19.2.3 version 2>/dev/null || echo "React 19.2.3 NOT FOUND"
echo ""
echo "Checking react-dom 19.2.3..."
npm view react-dom@19.2.3 version 2>/dev/null || echo "react-dom 19.2.3 NOT FOUND"
echo ""
echo "Latest versions available:"
npm view next version 2>/dev/null
npm view react version 2>/dev/nullRepository: senutpal/devRadar
Length of output: 206
Versioning approach is acceptable; consider consistency if desired.
Next.js 16.1.1 and React 19.2.3 are valid versions and currently the latest available on npm. The decision to pin exact versions for core dependencies (next, react, react-dom) while using caret ranges for others is a common pattern. If you prefer a more consistent strategy across all dependencies, consider either pinning all major dependencies or relaxing constraints for these core packages.
🤖 Prompt for AI Agents
In @apps/web/package.json around lines 27 - 31, The package.json dependency
versioning is inconsistent: core packages "next", "react", and "react-dom" are
pinned to exact versions while others use caret ranges; choose a consistent
strategy and update the dependency strings accordingly (either pin all core deps
by removing caret ranges from "next-themes" and "tailwind-merge" or relax
"next", "react", and "react-dom" to caret ranges) and ensure the package.json
entries for "next", "react", "react-dom", "next-themes", and "tailwind-merge"
reflect the chosen approach so versioning is uniform.
| export const metadata: Metadata = { | ||
| title: 'Dashboard', | ||
| description: 'DevRadar Dashboard - Manage your account and view your coding activity.', | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider metadata alignment with page content.
The metadata describes this as a dashboard for managing accounts and viewing stats, but the actual page renders a sign-in prompt with placeholder values. This may confuse users arriving via search results expecting dashboard functionality.
Consider either:
- Updating the description to reflect the sign-in requirement, or
- Implementing server-side auth check to redirect unauthenticated users to a login page
🤖 Prompt for AI Agents
In @apps/web/src/app/dashboard/page.tsx around lines 9 - 12, The
metadata.description currently advertises a full dashboard while the page
renders a sign-in prompt, causing a mismatch; either update the exported
metadata constant (metadata.description) to mention that authentication/sign-in
is required, or implement a server-side auth check inside the page's server
component (the default export in page.tsx) to detect unauthenticated users and
perform a redirect to the login route (e.g., /login) before rendering; locate
and update the metadata object or add the auth-check and redirect logic in the
page component to ensure metadata and actual behavior align.
| const docSections = [ | ||
| { | ||
| title: 'Getting Started', | ||
| href: '/docs', | ||
| items: ['Installation', 'Authentication', 'First Steps'], | ||
| }, | ||
| { | ||
| title: 'Privacy & Security', | ||
| href: '/privacy', | ||
| items: ['Data Collection', 'Ghost Mode', 'Blacklisting'], | ||
| }, | ||
| { title: 'Terms', href: '/terms', items: ['Terms of Service', 'Acceptable Use', 'Privacy'] }, | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-referential link and non-functional item list.
The "Getting Started" section links to /docs, which is the current page. Additionally, the items arrays list topics that don't appear to have corresponding anchor links or sub-pages, making them purely decorative rather than navigable.
Consider either:
- Linking to specific anchors (e.g.,
href: '/docs#installation') and adding corresponding sections - Removing the link from "Getting Started" since users are already on this page
- Clarifying these are planned topics if this is a placeholder
| ::selection { | ||
| background: rgba(23, 23, 23, 0.2); | ||
| color: #0a0a0a; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Selection colors don't adapt to dark mode.
The ::selection styling uses hardcoded light-mode colors. In dark mode, color: #0a0a0a (near-black text) on a dark background will result in poor visibility.
🐛 Proposed fix
::selection {
- background: rgba(23, 23, 23, 0.2);
- color: #0a0a0a;
+ background: color-mix(in srgb, var(--primary) 20%, transparent);
+ color: var(--foreground);
}📝 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.
| ::selection { | |
| background: rgba(23, 23, 23, 0.2); | |
| color: #0a0a0a; | |
| } | |
| ::selection { | |
| background: color-mix(in srgb, var(--primary) 20%, transparent); | |
| color: var(--foreground); | |
| } |
🤖 Prompt for AI Agents
In @apps/web/src/app/globals.css around lines 202 - 205, The ::selection rule
uses hardcoded light-mode colors causing poor contrast in dark mode; update it
to use theme-aware CSS variables or media queries instead. Replace the fixed
background and color in ::selection with references to your theme variables
(e.g., --selection-bg, --selection-color) or add a @media (prefers-color-scheme:
dark) / [data-theme="dark"] block to override ::selection for dark mode so
selection background and text color adapt appropriately (adjust the variables or
overrides where :root or your theme declarations are defined).
| .cursor-blink::after { | ||
| content: '|'; | ||
| animation: blink 1s step-end infinite; | ||
| color: #171717; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor blink color is hardcoded.
The cursor blink pseudo-element uses a hardcoded color that won't adapt to dark mode.
🐛 Proposed fix
.cursor-blink::after {
content: '|';
animation: blink 1s step-end infinite;
- color: #171717;
+ color: var(--foreground);
}📝 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.
| .cursor-blink::after { | |
| content: '|'; | |
| animation: blink 1s step-end infinite; | |
| color: #171717; | |
| } | |
| .cursor-blink::after { | |
| content: '|'; | |
| animation: blink 1s step-end infinite; | |
| color: var(--foreground); | |
| } |
🤖 Prompt for AI Agents
In @apps/web/src/app/globals.css around lines 498 - 502, The
.cursor-blink::after rule hardcodes color: #171717 which breaks dark mode;
change it to use an inheritable value (e.g., color: currentColor or color:
var(--cursor-caret)) and define that variable in your theme roots or component
styles so the caret inherits the appropriate light/dark color; update the
.cursor-blink::after selector and ensure any theme root (e.g., :root / .dark)
sets --cursor-caret (or rely on currentColor by ensuring the element's color is
set) so the blinking cursor adapts to the active theme.
| export function useCounterAnimation( | ||
| endValue: number, | ||
| options?: { | ||
| duration?: number; | ||
| prefix?: string; | ||
| suffix?: string; | ||
| decimals?: number; | ||
| } | ||
| ) { | ||
| const ref = useRef<HTMLElement>(null); | ||
| const valueRef = useRef({ value: 0 }); | ||
|
|
||
| useEffect(() => { | ||
| if (!ref.current) return; | ||
|
|
||
| const element = ref.current; | ||
| const { duration = 2, prefix = '', suffix = '', decimals = 0 } = options || {}; | ||
|
|
||
| const ctx = gsap.context(() => { | ||
| gsap.to(valueRef.current, { | ||
| value: endValue, | ||
| duration, | ||
| ease: 'power2.out', | ||
| onUpdate: () => { | ||
| const formatted = | ||
| decimals > 0 | ||
| ? valueRef.current.value.toFixed(decimals) | ||
| : Math.round(valueRef.current.value).toLocaleString(); | ||
| element.textContent = `${prefix}${formatted}${suffix}`; | ||
| }, | ||
| scrollTrigger: { | ||
| trigger: element, | ||
| start: 'top 85%', | ||
| once: true, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| return () => ctx.revert(); | ||
| }, [endValue, options]); | ||
|
|
||
| return ref; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object dependency and stale valueRef issue.
- The
optionsobject dependency (line 263) has the same infinite re-render problem. - The
valueRef.current.valueis never reset to 0 between re-runs, so ifendValuechanges, the animation starts from the previous value rather than 0.
Proposed fix
export function useCounterAnimation(
endValue: number,
options?: {
duration?: number;
prefix?: string;
suffix?: string;
decimals?: number;
}
) {
const ref = useRef<HTMLElement>(null);
const valueRef = useRef({ value: 0 });
+ const optionsKey = JSON.stringify(options);
useEffect(() => {
if (!ref.current) return;
+ // Reset counter value for new animations
+ valueRef.current.value = 0;
+
const element = ref.current;
const { duration = 2, prefix = '', suffix = '', decimals = 0 } = options || {};
const ctx = gsap.context(() => {
// ... animation unchanged
});
return () => ctx.revert();
- }, [endValue, options]);
+ }, [endValue, optionsKey]);
return ref;
}| export const FOOTER_LINKS = { | ||
| product: [ | ||
| { href: '/#features', label: 'Features' }, | ||
| { href: '/pricing', label: 'Pricing' }, | ||
| { href: '/docs', label: 'Documentation' }, | ||
| { href: SITE_CONFIG.links.marketplace, label: 'VS Code Extension', external: true }, | ||
| ], | ||
| company: [ | ||
| { href: '/about', label: 'About' }, | ||
| { href: '/blog', label: 'Blog' }, | ||
| { href: '/careers', label: 'Careers' }, | ||
| ], | ||
| legal: [ | ||
| { href: '/privacy', label: 'Privacy Policy' }, | ||
| { href: '/terms', label: 'Terms of Service' }, | ||
| ], | ||
| social: [ | ||
| { href: SITE_CONFIG.links.github, label: 'GitHub', icon: 'github' }, | ||
| { href: SITE_CONFIG.links.twitter, label: 'Twitter', icon: 'twitter' }, | ||
| { href: SITE_CONFIG.links.discord, label: 'Discord', icon: 'discord' }, | ||
| ], | ||
| } as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding as const to FOOTER_LINKS for type consistency.
Unlike other constants, FOOTER_LINKS uses as const on the whole object but the nested arrays with external and icon properties have inconsistent shapes. This works but could benefit from explicit typing for better IDE support.
🤖 Prompt for AI Agents
In @apps/web/src/lib/constants.ts around lines 28 - 49, FOOTER_LINKS currently
uses a top-level "as const" but has inconsistent nested shapes (some entries
include external, some include icon) causing weaker IDE/type support; define an
explicit type (e.g., a union FooterLink = { href: string; label: string;
external?: true } | { href: string; label: string; icon: 'github' | 'twitter' |
'discord' } and typed groups like Record<'product'|'company'|'legal'|'social',
ReadonlyArray<FooterLink>>) and annotate the FOOTER_LINKS constant with that
type instead of relying only on "as const" so each nested array element has a
clear, consistent shape and IDE autocomplete/type-checking works.
| company: [ | ||
| { href: '/about', label: 'About' }, | ||
| { href: '/blog', label: 'Blog' }, | ||
| { href: '/careers', label: 'Careers' }, | ||
| ], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, explore the app directory structure
find apps/web/src/app -type d -maxdepth 2 | head -20Repository: senutpal/devRadar
Length of output: 199
🏁 Script executed:
# Check for page.tsx files in the app directory
find apps/web/src/app -name "page.tsx" -o -name "page.ts" | sortRepository: senutpal/devRadar
Length of output: 253
🏁 Script executed:
# Search for any references to these routes in the codebase
rg "about|blog|careers" apps/web/src/app --type tsx --type ts -lRepository: senutpal/devRadar
Length of output: 88
🏁 Script executed:
# Check for route handlers or middleware that might handle these routes
find apps/web/src/app -name "route.ts" -o -name "route.js"Repository: senutpal/devRadar
Length of output: 43
🏁 Script executed:
# Check for any redirect logic in middleware or next.config
find apps/web -maxdepth 2 -name "middleware.ts" -o -name "next.config.*"Repository: senutpal/devRadar
Length of output: 84
🏁 Script executed:
# Search for any references to about, blog, careers routes anywhere
rg "about|blog|careers" apps/web/src/app -l --type js --type jsxRepository: senutpal/devRadar
Length of output: 88
🏁 Script executed:
# Check next.config.ts for redirects
cat apps/web/next.config.tsRepository: senutpal/devRadar
Length of output: 216
🏁 Script executed:
# Search for references to about, blog, careers using rg without specific file type
rg "about|blog|careers" apps/web/src/appRepository: senutpal/devRadar
Length of output: 378
Implement missing routes or remove broken navigation links.
The company section links to /about, /blog, and /careers, but these routes are not implemented in apps/web/src/app/. Currently, no page.tsx files exist for these paths, and there are no redirects configured. Users clicking these links will encounter 404 errors. Either implement these pages or remove the broken links from the navigation.
🤖 Prompt for AI Agents
In @apps/web/src/lib/constants.ts around lines 35 - 39, The navigation 'company'
array in constants.ts currently contains links to '/about', '/blog', and
'/careers' that aren't implemented and cause 404s; either create corresponding
pages (add page.tsx exports under app/about, app/blog, app/careers) that render
the expected content or remove/replace those entries from the company array so
the navigation only points to existing routes; if you prefer redirects, add
route handlers or next.config redirects for those paths and update the company
array accordingly to reference the implemented targets.
| export const TESTIMONIALS = [ | ||
| { | ||
| quote: | ||
| "Finally, the social coding experience I've always wanted. It's like Discord but for my IDE.", | ||
| author: 'Sarah Chen', | ||
| role: 'Senior Engineer at Stripe', | ||
| avatar: '/avatars/sarah.jpg', | ||
| }, | ||
| { | ||
| quote: | ||
| 'The merge conflict radar alone has saved us hours of rework. Essential for any distributed team.', | ||
| author: 'Marcus Johnson', | ||
| role: 'Tech Lead at Vercel', | ||
| avatar: '/avatars/marcus.jpg', | ||
| }, | ||
| { | ||
| quote: "I love seeing my friends' coding streaks. It motivates me to ship more consistently.", | ||
| author: 'Priya Sharma', | ||
| role: 'Indie Hacker', | ||
| avatar: '/avatars/priya.jpg', | ||
| }, | ||
| ] as const; | ||
|
|
||
| export const STATS = [ | ||
| { value: '10,000+', label: 'Active Developers' }, | ||
| { value: '50M+', label: 'Lines Tracked' }, | ||
| { value: '99.9%', label: 'Uptime' }, | ||
| { value: '<50ms', label: 'Latency' }, | ||
| ] as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Note: avatar field in TESTIMONIALS is unused in TestimonialCard.
The TESTIMONIALS entries include an avatar path, but TestimonialCard in social-proof-section.tsx displays only the first letter of the author's name instead of rendering the avatar image. Either remove the unused avatar field or implement avatar rendering.
🤖 Prompt for AI Agents
In @apps/web/src/lib/constants.ts around lines 196 - 224, The TESTIMONIALS
constant includes an unused avatar field; update either the data or the
component: remove the avatar property from each object in TESTIMONIALS if you
want to keep TestimonialCard (in social-proof-section.tsx) rendering initials
only, or modify TestimonialCard to render an <img> with src set to the
testimonial.avatar (fallback to initial if missing) so the avatar path is used;
reference TESTIMONIALS and the TestimonialCard component in
social-proof-section.tsx when making the change.
| export const STATS = [ | ||
| { value: '10,000+', label: 'Active Developers' }, | ||
| { value: '50M+', label: 'Lines Tracked' }, | ||
| { value: '99.9%', label: 'Uptime' }, | ||
| { value: '<50ms', label: 'Latency' }, | ||
| ] as const; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 99.9% stat will cause animation issues.
As noted in the social-proof-section.tsx review, the AnimatedStat component doesn't handle decimal values or percentage suffixes correctly. The 99.9% value will animate incorrectly (counting to 999 then snapping to 99.9%).
🤖 Prompt for AI Agents
In @apps/web/src/lib/constants.ts around lines 219 - 224, The uptime stat in
STATS uses a decimal percentage string ("99.9%") which AnimatedStat can't
animate correctly; update the STATS entry (the { value: '99.9%', label: 'Uptime'
} item in apps/web/src/lib/constants.ts) to use a whole-number value or separate
numeric value and suffix (e.g., { value: '99%', label: 'Uptime' } or { value:
'99', suffix: '%', label: 'Uptime' }) so AnimatedStat in
social-proof-section.tsx receives an integer-only value it can animate properly.
This commit addresses all valid codereview items from the PR #16 feedback: Configuration & Scripts: - Update package.json lint script to pass explicit target (eslint .) - Remove nested pnpm-workspace.yaml file Documentation: - Add top-level heading to README.md Accessibility & Security: - Add external link attributes (target=_blank, rel=noopener noreferrer) to dashboard, header mobile GitHub link - Add type='button' to toggle buttons in FAQ and header mobile menu - Add aria-label to billing toggle switch - Add accessible title to Discord SVG icon - Add priority prop to radar Image for LCP optimization Code Quality & Best Practices: - Consolidate duplicate :root and .dark blocks in globals.css - Remove duplicate .text-glow CSS rule - Replace hardcoded hex colors with theme tokens in pricing page - Add React.Fragment with key to comparisonFeatures map - Centralize lastUpdated constant (LEGAL_CONFIG) - Use objects instead of split strings for data items - Fix Tailwind arbitrary selectors in Card components - Move data-slot attributes from non-rendering primitives in dropdown-menu - Remove duplicate group class in navigation-menu - Remove redundant bg-foreground from tooltip Arrow - Forward ref in Container component - Memoize currentYear in Footer - Fix suffix detection logic in AnimatedStat - Use unique keys for TestimonialCard components - Guard against undefined Icon in FeatureCard - Add prefers-reduced-motion check in hero-section animations - Extract steps constant to shared module - Fix annual price calculation (Math.round vs Math.floor) - Add JSDoc docstrings to hooks and utils CSS Fixes: - Add accordion keyframe animations - Fix radar-tick responsive transform properties Verified with lint and build checks.
Summary by CodeRabbit
Release Notes
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.