You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds a public landing page at /, replacing the previous redirect("/dashboard") stub.
proxy.ts already treats / as public and redirects authenticated users to /dashboard, so signed-in users still land on the dashboard while visitors now get the marketing page.
What's included
Route groupapp/(landing)/ with its own layout (navbar + footer) and metadata
Sections — hero with a spec datasheet, full-bleed demo dashboard preview, tech stack, features grid, page inventory, 3-step workflow, FAQ, closing CTA
GitHub star count (lib/github.ts) fetched server-side, revalidated hourly, rendered in the header pill and hero CTA. Returns null and drops the count segment on any API failure rather than breaking the page
Theme customizer on the right edge, reusing the dashboard's existing ThemeTab so every Shadcn/Tweakcn preset, radius, brand colour and Import Theme option is available. Layout tab is omitted — there is no sidebar to configure here
Copy-to-clipboard command pills in the workflow and CTA sections
Design
Monochrome by default: the page uses primary/foreground/muted only, and --primary is already near-black (light) / near-white (dark) in globals.css. Presets therefore tint the logo, buttons, eyebrows, icon tiles and demo dashboard together, while KPI numbers and table text stay high-contrast. The one colour accent is the gradient on "already finished".
The customizer applies nothing on mount, so a fresh load renders exactly what globals.css defines instead of stamping inline CSS variables.
Drive-by fix
hooks/use-circular-transitions.ts — transition.finished.finally() did not handle rejection, so an aborted View Transition surfaced as InvalidStateError: Transition was aborted (an unhandled rejection). Added .catch(). This is shared code, so it also affects the dashboard customizer.
Verification
tsc --noEmit clean
eslint clean
next build compiles
Checked in light and dark, and with a preset applied and reset
Known limitations
Preset fonts do not apply. Presets set --font-sans, but globals.css has :root.font-inter body { font-family: var(--font-inter) } and FontProvider puts font-inter on <html>, which wins. Pre-existing behaviour shared with the dashboard; fixing it means deciding whether presets or FontProvider own typography.
Breakpoints are written mobile-first but have only been verified at desktop widths.
Code Review: feat: add landing page with GitHub star count and theme customizer
Summary
Well-executed, low-risk addition: a fully static-data marketing page built from small, focused Server Components, plus a legitimate one-line fix for an unhandled View Transition rejection. No blocking bugs found. The main thing worth addressing before/after merge is a redundant auth check that also forces the page to render dynamically on every request, which undercuts the "cacheable marketing page" goal.
Critical Issues
No critical issues identified.
Medium Issues
1. Redundant session check makes the whole page dynamic (features/landing/components/landing-navbar.tsx)
exportasyncfunctionLandingNavbar(){constauthenticated=awaitisAuthenticated();
...
{authenticated ? (<Linkhref="/dashboard"...>Dashboard ...</Link>) : (<>...Sign in / Live demo...</>)}
proxy.ts already redirects any authenticated session away from / before this route renders (pathname === "/" && session → redirect to /dashboard). So by the time LandingNavbar executes, session is guaranteed falsy and the authenticated branch (lines 44-68) can never run — it's dead code. Worse, isAuthenticated() triggers a second, independent getServerSession() call on every single landing-page view, and its use of cookies()/headers() is very likely why app/(landing)/layout.tsx needs export const dynamic = "force-dynamic" — which forces this otherwise-static marketing page to skip the Full Route Cache entirely.
Suggested fix: drop the isAuthenticated() check and always render the anonymous nav, then remove dynamic = "force-dynamic" from the layout so the page can be statically generated / ISR'd (the GitHub star fetch already revalidates hourly on its own).
2. GitHub star count fetched independently in two Server Components (features/landing/components/github-stars.tsx, features/landing/components/hero-section.tsx)
Both the navbar pill and the hero CTA call getRepoStars() independently. This only avoids a duplicate network call because Next.js's fetch request memoization happens to dedupe identical fetch() calls within one render pass — if either call site's options ever drift (a header added to one, not the other) they silently become two live calls to the GitHub API. Fetching once (e.g. in app/(landing)/page.tsx) and passing the value down as a prop removes the implicit dependency on that caching behavior.
If navigator.clipboard.writeText rejects (insecure context, permission denied, older browser), the button gives no feedback at all - the user just sees nothing happen. Worth at least a toast or a brief "failed to copy" state.
4. Two icon libraries mixed in the same components (features/landing/components/hero-section.tsx, cta-section.tsx, landing-footer.tsx, github-stars.tsx, mock-dashboard-ui.tsx, data.ts)
These files import GitHub/dashboard icons from @tabler/icons-react alongside lucide-react icons in the same file (e.g. hero-section.tsx imports IconBrandGithub/IconStar from tabler and ArrowRight from lucide). @tabler/icons-react was already a dependency used elsewhere in the repo (config/sidebar.ts, command-search.tsx), so this isn't a new dependency, but standardizing on one icon set per component (or repo-wide) would trim bundle weight and keep visual weight consistent.
5. Singular/plural mismatch in star count label (features/landing/components/github-stars.tsx:17)
`Star this project on GitHub - ${stars} stars` reads "1 stars" when the count is exactly 1. Minor copy nit only.
Strengths
lib/github.ts fails closed correctly: any non-OK response, network error, or unexpected payload shape returns null and the UI drops the star segment instead of breaking (github-stars.tsx, hero-section.tsx).
The hooks/use-circular-transitions.ts fix is correct and minimal - .catch(() => {}) before .finally() properly handles the InvalidStateError on an aborted View Transition without changing behavior otherwise.
Feature is cleanly isolated under features/landing/ with static content centralized in data.ts, following the repo's folder convention with no unnecessary abstraction.
No hardcoded colors or inline styles beyond one legitimate dynamic sparkline height (preview-section.tsx:53); theming is done entirely through existing CSS variables and the shared ThemeTab/useThemeManager, matching the "monochrome by default, presets tint everything" design goal described in the PR.
Copy accurately reflects the actual stack (next@16.2.2, react@19.2.0, Better Auth + Drizzle) rather than aspirational claims.
Verdict: No critical issues. Item 1 is worth fixing before merge since it's dead code plus an unnecessary per-request session lookup that also blocks static rendering of the page; items 2-5 can reasonably be iterated on later.
Note: I was not able to run pnpm exec tsc --noEmit or pnpm eslint in this environment (command execution required approval that wasn't available) - the PR description states both were run clean locally, but that could not be independently re-verified here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a public landing page at
/, replacing the previousredirect("/dashboard")stub.proxy.tsalready treats/as public and redirects authenticated users to/dashboard, so signed-in users still land on the dashboard while visitors now get the marketing page.What's included
app/(landing)/with its own layout (navbar + footer) and metadatalib/github.ts) fetched server-side, revalidated hourly, rendered in the header pill and hero CTA. Returnsnulland drops the count segment on any API failure rather than breaking the pageThemeTabso every Shadcn/Tweakcn preset, radius, brand colour and Import Theme option is available. Layout tab is omitted — there is no sidebar to configure hereDesign
Monochrome by default: the page uses
primary/foreground/mutedonly, and--primaryis already near-black (light) / near-white (dark) inglobals.css. Presets therefore tint the logo, buttons, eyebrows, icon tiles and demo dashboard together, while KPI numbers and table text stay high-contrast. The one colour accent is the gradient on "already finished".The customizer applies nothing on mount, so a fresh load renders exactly what
globals.cssdefines instead of stamping inline CSS variables.Drive-by fix
hooks/use-circular-transitions.ts—transition.finished.finally()did not handle rejection, so an aborted View Transition surfaced asInvalidStateError: Transition was aborted(an unhandled rejection). Added.catch(). This is shared code, so it also affects the dashboard customizer.Verification
tsc --noEmitcleaneslintcleannext buildcompilesKnown limitations
--font-sans, butglobals.csshas:root.font-inter body { font-family: var(--font-inter) }andFontProviderputsfont-interon<html>, which wins. Pre-existing behaviour shared with the dashboard; fixing it means deciding whether presets orFontProviderown typography.