diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts
index a13faf7..ca4ac82 100644
--- a/apps/api/src/auth/auth.controller.ts
+++ b/apps/api/src/auth/auth.controller.ts
@@ -7,6 +7,7 @@ import {
Post,
Req,
Res,
+ UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import {
@@ -111,7 +112,7 @@ export class AuthController {
| undefined) || dto?.refreshToken;
if (!refreshToken) {
- throw new Error('No refresh token provided');
+ throw new UnauthorizedException('No refresh token provided');
}
const result = await this.authService.refreshTokens(refreshToken);
diff --git a/apps/web/.env.example b/apps/web/.env.example
index 5143910..5f15c4b 100644
--- a/apps/web/.env.example
+++ b/apps/web/.env.example
@@ -9,3 +9,10 @@ NEXT_PUBLIC_DEMO_MODE_ENABLED=false
# Optional: Analytics, etc.
# NEXT_PUBLIC_GA_ID=""
+
+# GitHub Discussions integration for landing page feature wishlist
+# GITHUB_TOKEN must be a fine-grained PAT with Discussions: Read-only on the repo
+GITHUB_REPO_OWNER=
+GITHUB_REPO_NAME=
+GITHUB_DISCUSSIONS_CATEGORY_ID=
+GITHUB_TOKEN=
diff --git a/apps/web/app/(landing)/layout.tsx b/apps/web/app/(landing)/layout.tsx
new file mode 100644
index 0000000..891c846
--- /dev/null
+++ b/apps/web/app/(landing)/layout.tsx
@@ -0,0 +1,23 @@
+import type { Metadata } from 'next';
+import { Header } from '../../components/landing/Header';
+import { Footer } from '../../components/landing/Footer';
+
+export const metadata: Metadata = {
+ title: 'My Dev Deck — Your personal dev tool deck',
+ description:
+ 'An open-source, self-hostable collection of developer tools. Built in public, shaped by the community. Try the demo or explore on GitHub.',
+};
+
+export default function LandingLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
diff --git a/apps/web/app/(landing)/page.tsx b/apps/web/app/(landing)/page.tsx
new file mode 100644
index 0000000..105e36e
--- /dev/null
+++ b/apps/web/app/(landing)/page.tsx
@@ -0,0 +1,15 @@
+import { HeroSection } from '../../components/landing/HeroSection';
+import { ToolShowcaseSection } from '../../components/landing/ToolShowcaseSection';
+import { ValuePropsSection } from '../../components/landing/ValuePropsSection';
+import { FeatureWishlistSection } from '../../components/landing/FeatureWishlistSection';
+
+export default function LandingPage() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx
index c4329a5..eca25d1 100644
--- a/apps/web/app/login/page.tsx
+++ b/apps/web/app/login/page.tsx
@@ -1,34 +1,19 @@
'use client';
-import { useState, useEffect } from 'react';
+import { useState } from 'react';
import { useRouter } from 'next/navigation';
-import Cookies from 'js-cookie';
+import Link from 'next/link';
import { useAuth } from '../../contexts/AuthContext';
-import { tryDemo } from '../../lib/api';
-
-const DEMO_MODE_ENABLED = process.env.NEXT_PUBLIC_DEMO_MODE_ENABLED === 'true';
-const DEMO_UNAVAILABLE_KEY = 'demoUnavailable';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
- const [demoLoading, setDemoLoading] = useState(false);
- const [demoVisible, setDemoVisible] = useState(DEMO_MODE_ENABLED);
const router = useRouter();
const { login } = useAuth();
- // If a previous demo attempt returned 404, hide the button for this session
- useEffect(() => {
- if (!DEMO_MODE_ENABLED) return;
- if (typeof window === 'undefined') return;
- if (sessionStorage.getItem(DEMO_UNAVAILABLE_KEY) === '1') {
- setDemoVisible(false);
- }
- }, []);
-
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
@@ -44,56 +29,15 @@ export default function LoginPage() {
}
};
- const handleTryDemo = async () => {
- setError('');
- setDemoLoading(true);
-
- try {
- const res = await tryDemo();
-
- if (res.status === 201 || res.ok) {
- // Server set httpOnly auth cookies. Mirror the login flow by setting
- // the frontend `session` cookie so middleware recognizes the user,
- // then do a full navigation so AuthContext rehydrates with /api/auth/me.
- Cookies.set('session', 'active', { path: '/', expires: 1 });
- window.location.href = '/dashboard';
- return;
- }
-
- if (res.status === 429) {
- setError('Demo limit reached, try again later.');
- return;
- }
-
- if (res.status === 404) {
- if (typeof window !== 'undefined') {
- sessionStorage.setItem(DEMO_UNAVAILABLE_KEY, '1');
- }
- setDemoVisible(false);
- setError('Demo is not available right now.');
- return;
- }
-
- // Other error
- let message = 'Could not start demo. Please try again.';
- try {
- const body = await res.json();
- if (body?.message) message = body.message;
- } catch {
- // ignore JSON parse errors
- }
- setError(message);
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Could not start demo.');
- } finally {
- setDemoLoading(false);
- }
- };
-
return (
-
My Dev Deck
+
+
My Dev Deck
+
Sign in to access your developer tools
@@ -128,7 +72,7 @@ export default function LoginPage() {
onChange={(e) => setPassword(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-900"
required
- disabled={loading || demoLoading}
+ disabled={loading}
autoComplete="current-password"
/>
@@ -136,27 +80,11 @@ export default function LoginPage() {
{loading ? 'Signing in...' : 'Sign In'}
-
- {demoVisible && (
-
-
- No account? Explore with a demo.
-
-
- {demoLoading ? 'Starting demo...' : 'Try Demo'}
-
-
- )}
);
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
deleted file mode 100644
index 6399ed4..0000000
--- a/apps/web/app/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from 'next/navigation';
-
-export default function Page() {
- redirect('/dashboard');
-}
diff --git a/apps/web/components/landing/BrowserFrame.tsx b/apps/web/components/landing/BrowserFrame.tsx
new file mode 100644
index 0000000..41b05bc
--- /dev/null
+++ b/apps/web/components/landing/BrowserFrame.tsx
@@ -0,0 +1,24 @@
+interface Props {
+ children: React.ReactNode;
+ url?: string;
+}
+
+export function BrowserFrame({ children, url = 'devinbox.mydevdeck.com' }: Props) {
+ return (
+
+ );
+}
diff --git a/apps/web/components/landing/FeatureWishlistItem.tsx b/apps/web/components/landing/FeatureWishlistItem.tsx
new file mode 100644
index 0000000..ee6e406
--- /dev/null
+++ b/apps/web/components/landing/FeatureWishlistItem.tsx
@@ -0,0 +1,41 @@
+'use client';
+
+import type { FeatureIdea } from '../../lib/github';
+
+interface Props {
+ idea: FeatureIdea;
+}
+
+export function FeatureWishlistItem({ idea }: Props) {
+ return (
+
+
+ {idea.title}
+
+ {idea.bodyExcerpt && (
+
+ {idea.bodyExcerpt}
+
+ )}
+
+
+
+
+
+ {idea.upvotes}
+
+
+
+
+
+ {idea.commentCount}
+
+
+
+ );
+}
diff --git a/apps/web/components/landing/FeatureWishlistSection.tsx b/apps/web/components/landing/FeatureWishlistSection.tsx
new file mode 100644
index 0000000..5c0c9ca
--- /dev/null
+++ b/apps/web/components/landing/FeatureWishlistSection.tsx
@@ -0,0 +1,64 @@
+import { fetchFeatureIdeas } from '../../lib/github';
+import { FeatureWishlistItem } from './FeatureWishlistItem';
+
+function getRepoUrl(): string {
+ const owner = process.env.GITHUB_REPO_OWNER || 'Rowee13';
+ const name = process.env.GITHUB_REPO_NAME || 'my-dev-deck';
+ return `https://github.com/${owner}/${name}`;
+}
+
+export async function FeatureWishlistSection() {
+ const ideas = await fetchFeatureIdeas();
+ const repoUrl = getRepoUrl();
+ const submitUrl = `${repoUrl}/discussions/new?category=feature-ideas`;
+ const allUrl = `${repoUrl}/discussions`;
+
+ return (
+
+
+
+
+ What's next?
+
+
+ A community-driven wishlist. Upvote the ideas you want, or submit your own on GitHub.
+
+
+
+ {ideas.length > 0 ? (
+
+ {ideas.map((idea) => (
+
+ ))}
+
+ ) : (
+
+
No ideas yet.
+
+ Be the first to share what you'd like to see added to the deck.
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/web/components/landing/Footer.tsx b/apps/web/components/landing/Footer.tsx
new file mode 100644
index 0000000..87b7981
--- /dev/null
+++ b/apps/web/components/landing/Footer.tsx
@@ -0,0 +1,34 @@
+import Link from 'next/link';
+
+const GITHUB_URL = 'https://github.com/Rowee13/my-dev-deck';
+
+export function Footer() {
+ return (
+
+ );
+}
diff --git a/apps/web/components/landing/Header.tsx b/apps/web/components/landing/Header.tsx
new file mode 100644
index 0000000..911e8a1
--- /dev/null
+++ b/apps/web/components/landing/Header.tsx
@@ -0,0 +1,103 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { TryDemoButton } from './TryDemoButton';
+
+const GITHUB_URL = 'https://github.com/Rowee13/my-dev-deck';
+
+export function Header() {
+ const [scrolled, setScrolled] = useState(false);
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ const onScroll = () => setScrolled(window.scrollY > 50);
+ onScroll();
+ window.addEventListener('scroll', onScroll, { passive: true });
+ return () => window.removeEventListener('scroll', onScroll);
+ }, []);
+
+ return (
+
+
+
+ My Dev Deck
+
+
+
+
+
setMobileOpen((v) => !v)}
+ >
+
+ {mobileOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {mobileOpen && (
+
+ )}
+
+ );
+}
+
+function GitHubIcon() {
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/components/landing/HeroSection.tsx b/apps/web/components/landing/HeroSection.tsx
new file mode 100644
index 0000000..4adb62c
--- /dev/null
+++ b/apps/web/components/landing/HeroSection.tsx
@@ -0,0 +1,64 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import { TryDemoButton } from './TryDemoButton';
+
+const GITHUB_URL = 'https://github.com/Rowee13/my-dev-deck';
+
+export function HeroSection() {
+ return (
+
+ {/* Decorative gradient orbs */}
+
+
+
+
+
+ Your personal dev tool deck
+
+
+
+ An open-source, self-hostable collection of developer tools — built in public, shaped by the community.
+
+
+
+
+
+
+
+
+ View on GitHub
+
+
+
+
+ );
+}
diff --git a/apps/web/components/landing/ToolShowcaseSection.tsx b/apps/web/components/landing/ToolShowcaseSection.tsx
new file mode 100644
index 0000000..3fd8c75
--- /dev/null
+++ b/apps/web/components/landing/ToolShowcaseSection.tsx
@@ -0,0 +1,68 @@
+'use client';
+
+import { motion } from 'framer-motion';
+import Image from 'next/image';
+import Link from 'next/link';
+import { BrowserFrame } from './BrowserFrame';
+
+export function ToolShowcaseSection() {
+ return (
+
+
+
+
+ Tools in the deck
+
+
+ A growing collection of utilities for everyday development work.
+
+
+
+
+
+
+ Tool #1
+
+
+ DevInbox
+
+
+ A self-hosted email testing inbox. Route your app's transactional emails to DevInbox during development, inspect message content, headers, and attachments, and verify user signup flows without spamming real mailboxes.
+
+
+ Try DevInbox
+
→
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/components/landing/TryDemoButton.tsx b/apps/web/components/landing/TryDemoButton.tsx
new file mode 100644
index 0000000..592f79c
--- /dev/null
+++ b/apps/web/components/landing/TryDemoButton.tsx
@@ -0,0 +1,78 @@
+'use client';
+
+import { useState } from 'react';
+import Cookies from 'js-cookie';
+import { tryDemo } from '../../lib/api';
+
+const DEMO_MODE_ENABLED = process.env.NEXT_PUBLIC_DEMO_MODE_ENABLED === 'true';
+
+type Variant = 'primary' | 'secondary' | 'nav';
+
+interface Props {
+ variant?: Variant;
+ className?: string;
+}
+
+const VARIANT_CLASSES: Record = {
+ primary:
+ 'px-6 py-3 text-base bg-blue-600 text-white rounded-md hover:bg-blue-700 hover:scale-105 transition-all disabled:opacity-50',
+ secondary:
+ 'px-5 py-2.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50',
+ nav: 'px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50',
+};
+
+export function TryDemoButton({ variant = 'primary', className = '' }: Props) {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ if (!DEMO_MODE_ENABLED) return null;
+
+ const handleClick = async () => {
+ setError('');
+ setLoading(true);
+ try {
+ const res = await tryDemo();
+ if (res.status === 201 || res.ok) {
+ Cookies.set('session', 'active', { path: '/', expires: 1 });
+ window.location.href = '/dashboard';
+ return;
+ }
+ if (res.status === 429) {
+ setError('Demo limit reached, try again later.');
+ return;
+ }
+ if (res.status === 404) {
+ setError('Demo is not available right now.');
+ return;
+ }
+ let message = 'Could not start demo. Please try again.';
+ try {
+ const body = await res.json();
+ if (body?.message) message = body.message;
+ } catch {
+ // ignore
+ }
+ setError(message);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Could not start demo.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ {loading ? 'Starting demo...' : 'Try Demo'}
+
+ {error && (
+
{error}
+ )}
+
+ );
+}
diff --git a/apps/web/components/landing/ValuePropsSection.tsx b/apps/web/components/landing/ValuePropsSection.tsx
new file mode 100644
index 0000000..cb26f12
--- /dev/null
+++ b/apps/web/components/landing/ValuePropsSection.tsx
@@ -0,0 +1,72 @@
+'use client';
+
+import { motion } from 'framer-motion';
+
+const VALUE_PROPS = [
+ {
+ icon: '📜',
+ title: 'Open source',
+ description: 'MIT-licensed. Every line of code is on GitHub. Fork it, audit it, contribute back.',
+ },
+ {
+ icon: '🏠',
+ title: 'Self-hostable',
+ description: 'Docker Compose runs the whole stack locally or on your own server. Your data stays yours.',
+ },
+ {
+ icon: '🧰',
+ title: 'Growing toolset',
+ description: 'More tools are added over time. Suggest features, upvote what matters, shape the deck with us.',
+ },
+];
+
+export function ValuePropsSection() {
+ return (
+
+
+
+
+ Why My Dev Deck
+
+
+
+
+ {VALUE_PROPS.map((prop) => (
+
+
+ {prop.icon}
+
+
+ {prop.title}
+
+ {prop.description}
+
+ ))}
+
+
+
+ );
+}
diff --git a/apps/web/lib/github.ts b/apps/web/lib/github.ts
new file mode 100644
index 0000000..078fa44
--- /dev/null
+++ b/apps/web/lib/github.ts
@@ -0,0 +1,106 @@
+import 'server-only';
+
+export interface FeatureIdea {
+ id: string;
+ title: string;
+ bodyExcerpt: string;
+ url: string;
+ upvotes: number;
+ commentCount: number;
+}
+
+interface DiscussionNode {
+ id: string;
+ title: string;
+ bodyText: string;
+ url: string;
+ upvoteCount: number;
+ comments: { totalCount: number };
+}
+
+interface GraphQLResponse {
+ data?: {
+ repository?: {
+ discussions?: {
+ nodes?: DiscussionNode[];
+ };
+ };
+ };
+ errors?: unknown;
+}
+
+const GITHUB_GRAPHQL = 'https://api.github.com/graphql';
+const EXCERPT_LENGTH = 120;
+const REVALIDATE_SECONDS = 300; // 5 minutes
+
+const QUERY = `
+ query ($owner: String!, $name: String!, $category: ID!) {
+ repository(owner: $owner, name: $name) {
+ discussions(first: 5, categoryId: $category, orderBy: { field: CREATED_AT, direction: DESC }) {
+ nodes {
+ id
+ title
+ bodyText
+ url
+ upvoteCount
+ comments { totalCount }
+ }
+ }
+ }
+ }
+`;
+
+function excerpt(text: string): string {
+ const cleaned = text.trim().replace(/\s+/g, ' ');
+ if (cleaned.length <= EXCERPT_LENGTH) return cleaned;
+ return cleaned.slice(0, EXCERPT_LENGTH).trimEnd() + '…';
+}
+
+export async function fetchFeatureIdeas(): Promise {
+ const owner = process.env.GITHUB_REPO_OWNER;
+ const name = process.env.GITHUB_REPO_NAME;
+ const category = process.env.GITHUB_DISCUSSIONS_CATEGORY_ID;
+ const token = process.env.GITHUB_TOKEN;
+
+ if (!owner || !name || !category || !token) return [];
+
+ try {
+ const res = await fetch(GITHUB_GRAPHQL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({
+ query: QUERY,
+ variables: { owner, name, category },
+ }),
+ next: { revalidate: REVALIDATE_SECONDS },
+ });
+
+ if (!res.ok) {
+ console.error('[github] discussions fetch failed', res.status);
+ return [];
+ }
+
+ const json = (await res.json()) as GraphQLResponse;
+ if (json.errors) {
+ console.error('[github] discussions GraphQL errors', json.errors);
+ return [];
+ }
+
+ const nodes = json.data?.repository?.discussions?.nodes ?? [];
+ return nodes.map((n) => ({
+ id: n.id,
+ title: n.title,
+ bodyExcerpt: excerpt(n.bodyText ?? ''),
+ url: n.url,
+ upvotes: n.upvoteCount,
+ commentCount: n.comments.totalCount,
+ }));
+ } catch (err) {
+ console.error('[github] discussions fetch error', err);
+ return [];
+ }
+}
diff --git a/apps/web/package.json b/apps/web/package.json
index 85c0a35..05e2bd8 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -12,6 +12,7 @@
},
"dependencies": {
"@repo/ui": "workspace:*",
+ "framer-motion": "^12.38.0",
"js-cookie": "^3.0.5",
"next": "^16.1.6",
"react": "^19.2.0",
diff --git a/apps/web/proxy.ts b/apps/web/proxy.ts
index 3b45804..573ec74 100644
--- a/apps/web/proxy.ts
+++ b/apps/web/proxy.ts
@@ -1,7 +1,7 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
-const publicPaths = ['/login', '/setup'];
+const publicPaths = ['/', '/login', '/setup'];
const authPaths = ['/login', '/setup'];
export default function proxy(request: NextRequest) {
diff --git a/apps/web/public/landing/devinbox-preview.png b/apps/web/public/landing/devinbox-preview.png
new file mode 100644
index 0000000..6641c8a
Binary files /dev/null and b/apps/web/public/landing/devinbox-preview.png differ
diff --git a/docs/plans/2026-04-17-landing-page-design.md b/docs/plans/2026-04-17-landing-page-design.md
new file mode 100644
index 0000000..546b240
--- /dev/null
+++ b/docs/plans/2026-04-17-landing-page-design.md
@@ -0,0 +1,479 @@
+# Landing Page Design
+
+**Date:** 2026-04-17
+**Branch:** `feat/landing-page`
+**Status:** Design approved, pending implementation plan
+
+## Purpose
+
+Replace the current `/` route (which redirects to `/dashboard`) with a proper landing page that:
+
+- Showcases My Dev Deck as an open-source personal dev tool deck
+- Leads users to `Try Demo` as the primary conversion path
+- Highlights DevInbox as the first tool in the deck
+- Invites community feature suggestions via a live GitHub Discussions wishlist
+- Replaces the `Try Demo` button currently on the login page
+
+This is an OSS showcase with a hint of multi-tool pitch — not a roadmap (there are no concrete future features yet), but a wishlist where brainstorming can happen.
+
+## Scope
+
+In scope:
+
+- New landing page at `/`
+- Removal of `Try Demo` button from the login page
+- "My Dev Deck" wordmark on login page becomes a link back to `/`
+- Middleware update to treat `/` as public
+- New env vars for GitHub integration
+
+Out of scope:
+
+- Docs site link (docs app not yet deployed)
+- Full dark mode across the dashboard
+- Authenticated feature voting in-app (deferred; GitHub handles voting)
+- SEO beyond basic meta tags and OG image
+
+## Architecture overview
+
+- Next.js 16 App Router, Server Components as default
+- Client islands only where interactivity or animation requires it (`'use client'`)
+- Framer Motion for scroll-triggered reveals and entrance animations
+- GitHub GraphQL API for Discussions data, fetched server-side with 5-minute cache
+- No new backend endpoints — the NestJS API is untouched
+
+## File structure
+
+```
+apps/web/
+├── app/
+│ └── (landing)/ # Route group for layout scoping, no URL segment
+│ ├── layout.tsx # Header + {children} + Footer
+│ └── page.tsx # Composes section components
+├── components/
+│ └── landing/
+│ ├── Header.tsx # Sticky top bar
+│ ├── HeroSection.tsx # Dark gradient, headline, CTAs
+│ ├── ToolShowcaseSection.tsx # DevInbox card with screenshot
+│ ├── ValuePropsSection.tsx # 3 cards: OSS / self-hostable / growing deck
+│ ├── FeatureWishlistSection.tsx # Server component, fetches GitHub Discussions
+│ ├── FeatureWishlistItem.tsx # Per-item hover effects
+│ ├── Footer.tsx # Minimal footer
+│ ├── TryDemoButton.tsx # Extracted from login page
+│ └── BrowserFrame.tsx # Decorative frame for screenshot
+├── lib/
+│ └── github.ts # Server-only GraphQL client
+└── public/
+ └── landing/
+ └── devinbox-preview.png # Static screenshot (captured manually)
+```
+
+Modified files:
+
+- `apps/web/proxy.ts` — add `/` to `publicPaths`
+- `apps/web/app/login/page.tsx` — remove demo button, make wordmark a link
+- `apps/web/.env.example` — add GitHub env vars
+
+## Page composition
+
+**`app/(landing)/layout.tsx`:**
+
+```tsx
+import { Header } from '@/components/landing/Header';
+import { Footer } from '@/components/landing/Footer';
+
+export default function LandingLayout({ children }: { children: React.ReactNode }) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
+```
+
+**`app/(landing)/page.tsx`:**
+
+```tsx
+import { HeroSection } from '@/components/landing/HeroSection';
+import { ToolShowcaseSection } from '@/components/landing/ToolShowcaseSection';
+import { ValuePropsSection } from '@/components/landing/ValuePropsSection';
+import { FeatureWishlistSection } from '@/components/landing/FeatureWishlistSection';
+
+export default function LandingPage() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+```
+
+Each section component renders its own `` wrapper.
+
+## Component responsibilities
+
+### `Header.tsx`
+
+- Sticky top, transparent at page top, solid `bg-slate-900/80 backdrop-blur-md` after 50px scroll
+- Uses Framer Motion `useScroll` to track position
+- Left: "My Dev Deck" wordmark, links to `/`
+- Right: `Try Demo` button, `Login` link to `/login`, GitHub icon link to repo
+- Mobile: right-side items collapse into a hamburger dropdown
+
+### `HeroSection.tsx`
+
+- Background: `bg-gradient-to-br from-slate-900 via-slate-900 to-indigo-950`
+- Decorative blurred orbs and/or faint grid pattern overlay (CSS only)
+- Headline: **"Your personal dev tool deck"**
+- Subhead: one sentence, ~15 words — open source collection of tools built in public
+- CTAs: `Try Demo` (primary, blue filled) + `View on GitHub` (outlined, white border, GitHub icon)
+- Entrance animation: staggered fade-in-up (headline → subhead +100ms → buttons +200ms)
+
+### `ToolShowcaseSection.tsx`
+
+- Light background
+- Section heading: "Tools in the deck"
+- One card for DevInbox, 2-column grid on desktop (stacks on mobile):
+ - Left column: tool name, short description, "Try DevInbox" micro-CTA
+ - Right column: `` wrapping the static screenshot
+- Slides up from bottom on scroll into viewport
+- Hover lift effect on the card
+
+### `ValuePropsSection.tsx`
+
+- Alternating light background (e.g., `bg-slate-50`)
+- Heading: "Why My Dev Deck"
+- 3-column grid (1-column on mobile), each card:
+ - Icon + heading + 1-2 sentences
+ - Cards: **Open source** (MIT-licensed, code on GitHub) / **Self-hostable** (Docker Compose, runs anywhere) / **Growing toolset** (more tools being added; shape it with us)
+- Stagger animation — cards fade in 100ms apart as section enters viewport
+
+### `FeatureWishlistSection.tsx`
+
+- Light background (alternates back from value props)
+- Heading: "What's next?"
+- Intro paragraph explaining this is a community-driven wishlist
+- Grid of up to 5 `` components
+- Primary CTA: `Submit an idea` deep-links to `github.com///discussions/new?category=feature-ideas`
+- Secondary text link: "See all ideas on GitHub"
+- Empty state when no discussions exist or API fails: encouraging message with just the submit CTA
+
+### `FeatureWishlistItem.tsx`
+
+- Client component
+- Card layout: title, truncated description (~120 chars), upvote count with icon, comment count with icon
+- Entire card is clickable, opens discussion on GitHub in new tab
+- Hover: slight lift + border color change to blue-300
+
+### `Footer.tsx`
+
+- Single row, minimal
+- Left: © 2026 My Dev Deck • MIT Licensed
+- Right: GitHub icon link + Login link
+
+### `TryDemoButton.tsx`
+
+- Client component, extracted from current login page logic
+- Props: `variant` (primary / secondary / nav) to control size and color
+- Calls `tryDemo()` from `lib/api`
+- Handles 429 (rate limit), 404 (demo disabled), and generic errors with inline feedback or toast
+- On success: sets frontend `session` cookie, navigates to `/dashboard`
+
+### `BrowserFrame.tsx`
+
+- Purely decorative
+- Styled container: rounded corners, subtle shadow, mock browser top bar with traffic-light dots and fake URL
+- Children render inside the frame (in this case, the screenshot)
+
+## GitHub Discussions data flow
+
+### `lib/github.ts`
+
+Server-only module (`import 'server-only'`). Exports one function and a type:
+
+```ts
+export interface FeatureIdea {
+ id: string;
+ title: string;
+ bodyExcerpt: string;
+ url: string;
+ upvotes: number;
+ commentCount: number;
+}
+
+export async function fetchFeatureIdeas(): Promise;
+```
+
+Implementation:
+
+- Endpoint: `https://api.github.com/graphql`
+- GraphQL query fetches the first 5 discussions from the configured category, ordered by creation date descending
+- `fetch(..., { next: { revalidate: 300 } })` — Next.js caches for 5 minutes server-side
+- Unauthenticated — GitHub public API allows 60 requests per hour per IP; 5-minute cache means we use at most 12 requests per hour
+- Always returns an array; never throws (try/catch around fetch, returns `[]` on any failure)
+- Truncates body text to ~120 chars for excerpt
+
+### Env vars
+
+Added to `apps/web/.env.example` and `apps/web/.env.local`:
+
+```
+GITHUB_REPO_OWNER=Rowee13
+GITHUB_REPO_NAME=my-dev-deck
+GITHUB_DISCUSSIONS_CATEGORY_ID=DIC_kwDOxxxxxxxxxxxxxxxx
+```
+
+Category ID is a one-time lookup via GitHub's GraphQL explorer (see setup guide below).
+
+### Error handling
+
+- Network failure → empty array → empty state UI
+- Rate limit (HTTP 429) → empty array → empty state UI
+- Missing env vars → empty array → empty state UI
+- Malformed response → empty array → empty state UI
+
+No loading state needed because this runs at build/revalidate time, not per-request.
+
+## Styling
+
+### Palette
+
+- Hero background: `bg-gradient-to-br from-slate-900 via-slate-900 to-indigo-950`
+- Hero text: `text-white` / `text-slate-300` for subhead
+- Light sections: `bg-white` and `bg-slate-50` alternating
+- Primary CTA: `bg-blue-600 hover:bg-blue-700`
+- Secondary CTA (hero): `border border-white/20 hover:border-white/40 hover:bg-white/5`
+- Accent/links: `text-blue-600` on light, `text-indigo-400` on dark
+
+### Typography
+
+- Reuse existing `Geist` font loaded in root layout
+- Hero headline: `text-5xl md:text-6xl lg:text-7xl font-bold`
+- Section headings: `text-3xl md:text-4xl font-bold`
+- Body: default
+
+### Accessibility
+
+- All CTAs are real `` or `` — no div-click handlers
+- Images have descriptive `alt` text
+- Contrast meets WCAG AA (blue-600 on white 4.54:1, white on slate-900 16:1)
+- `aria-label` on icon-only links
+- Framer Motion auto-respects `prefers-reduced-motion`
+
+### Responsive
+
+- Mobile-first Tailwind
+- Hero CTAs stack vertically on mobile
+- Tool showcase stacks
+- Value props: 3-col → 1-col
+- Wishlist: always single-column
+- Header right-side collapses to hamburger
+
+## Animation system
+
+### Framer Motion install
+
+```bash
+pnpm --filter web add framer-motion
+```
+
+### Patterns
+
+1. **Hero entrance** — on mount, staggered fade-in-up:
+ ```tsx
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ transition={{ duration: 0.6, ease: 'easeOut' }}
+ ```
+
+2. **Scroll reveals** — section-level, on intersection:
+ ```tsx
+ initial={{ opacity: 0, y: 30 }}
+ whileInView={{ opacity: 1, y: 0 }}
+ viewport={{ once: true, amount: 0.2 }}
+ transition={{ duration: 0.5 }}
+ ```
+
+3. **Stagger children** — value props cards animate in 100ms apart using parent container with `staggerChildren: 0.1`.
+
+4. **Hover (CSS only)**:
+ - Tool card: `hover:shadow-xl hover:-translate-y-1 transition-all duration-300`
+ - Wishlist item: `hover:border-blue-300 hover:bg-blue-50/50 transition-colors`
+ - Primary CTA: `hover:scale-105 transition-transform`
+
+5. **Hero decoration** — CSS-only animated gradient orbs using `animate-pulse` with long duration, no JS.
+
+6. **Header scroll transition** — Framer Motion `useScroll` tracks `window.scrollY`, toggles transparent vs. solid background.
+
+## Middleware update
+
+`apps/web/proxy.ts`:
+
+```ts
+const publicPaths = ['/', '/login', '/setup']; // ADD '/'
+const authPaths = ['/login', '/setup']; // unchanged — '/' stays browseable when logged in
+```
+
+Behavior:
+
+- Logged out visiting `/` → landing page renders (public path)
+- Logged in visiting `/` → landing page renders (not in authPaths, so no redirect)
+- Logged in visiting `/login` → redirect to `/dashboard` (in authPaths)
+
+## Login page changes
+
+`apps/web/app/login/page.tsx`:
+
+Remove:
+
+- `DEMO_MODE_ENABLED` constant
+- `DEMO_UNAVAILABLE_KEY` constant
+- `demoLoading`, `demoVisible` state
+- `handleTryDemo` function
+- `tryDemo` import
+- sessionStorage `useEffect`
+- Entire demo button JSX block (bottom of form)
+- `|| demoLoading` from all `disabled` checks
+
+Add:
+
+- `import Link from 'next/link';`
+- Wrap the `My Dev Deck ` heading in ` ` so the wordmark links to the landing page
+
+## GitHub Discussions setup guide
+
+### Step 1 — Enable Discussions
+
+1. Visit `https://github.com/Rowee13/my-dev-deck/settings`
+2. Scroll to "Features"
+3. Check "Discussions" → click "Set up discussions"
+
+### Step 2 — Create the "Feature Ideas" category
+
+1. Visit `https://github.com/Rowee13/my-dev-deck/discussions`
+2. Click the gear icon next to "Categories"
+3. Click "New category"
+4. Fill in:
+ - **Name:** `Feature Ideas`
+ - **Description:** "Suggest new tools or features for My Dev Deck"
+ - **Discussion format:** `Open-ended discussion`
+ - **Emoji:** 💡 (or any)
+5. Click "Create"
+
+### Step 3 — Get the category node ID
+
+Visit `https://docs.github.com/en/graphql/overview/explorer`, sign in with GitHub, and run:
+
+```graphql
+query {
+ repository(owner: "Rowee13", name: "my-dev-deck") {
+ discussionCategories(first: 10) {
+ nodes {
+ id
+ name
+ slug
+ }
+ }
+ }
+}
+```
+
+Find the node where `name` is `"Feature Ideas"` and copy its `id` (format: `DIC_kwDO...`).
+
+### Step 4 — Add env vars
+
+Append to `apps/web/.env.local`:
+
+```
+GITHUB_REPO_OWNER=Rowee13
+GITHUB_REPO_NAME=my-dev-deck
+GITHUB_DISCUSSIONS_CATEGORY_ID=DIC_kwDOxxxxxxxxxxxxxxxx
+```
+
+Add placeholders to `apps/web/.env.example`:
+
+```
+GITHUB_REPO_OWNER=
+GITHUB_REPO_NAME=
+GITHUB_DISCUSSIONS_CATEGORY_ID=
+```
+
+Add the same vars to Fly.io secrets for production:
+
+```bash
+fly secrets set GITHUB_REPO_OWNER=Rowee13 GITHUB_REPO_NAME=my-dev-deck GITHUB_DISCUSSIONS_CATEGORY_ID=DIC_... --app my-dev-deck-web
+```
+
+### Step 5 — Seed starter ideas
+
+Create 3-5 discussions in the Feature Ideas category to avoid an empty landing page at launch. Suggested seeds:
+
+- "Snippet Vault — searchable personal code snippets"
+- "Regex Playground — test regex patterns with live match highlighting"
+- "JSON to TypeScript type converter"
+- "Markdown to HTML previewer"
+- "Webhook debugger (like DevInbox but for HTTP)"
+
+### Step 6 — Test
+
+```bash
+pnpm --filter web dev
+```
+
+Visit `http://localhost:4001/`, scroll to the wishlist section. Seed discussions should appear with upvote counts. If empty state shows, check env vars and API response in server logs.
+
+## Testing & quality gates
+
+### Automated
+
+- `pnpm --filter web lint` — must pass
+- `pnpm --filter web check-types` — must pass
+- No new unit tests planned (content-heavy page; manual QA is the right tool)
+
+### Manual QA checklist
+
+- [ ] `/` loads logged out → full landing page with wishlist
+- [ ] `/` loads logged in → full landing page (no redirect)
+- [ ] `Try Demo` in hero → `/dashboard` with demo banner + expiration countdown
+- [ ] `Try Demo` in header → same as above
+- [ ] `View on GitHub` → opens repo in new tab
+- [ ] `Login` in header when logged out → `/login`
+- [ ] `Login` in header when logged in → redirects to `/dashboard` via middleware
+- [ ] `/login` logged out → no `Try Demo` button visible
+- [ ] `/login` "My Dev Deck" wordmark → clicks through to `/`
+- [ ] Scroll past hero → header transitions transparent → solid with blur
+- [ ] Hover tool card → lifts with shadow
+- [ ] Hover value props → subtle effect
+- [ ] Hover wishlist items → border color change
+- [ ] Scroll each section into view → entrance animation plays
+- [ ] Resize to mobile width → sections stack, hamburger menu works
+- [ ] Disable JS → page content still renders (Server Components)
+- [ ] `prefers-reduced-motion: reduce` → animations minimized
+- [ ] Tab through page with keyboard → all CTAs focusable in logical order
+- [ ] Feature wishlist section shows real discussions from GitHub
+- [ ] Disconnect network / set invalid env var → empty state shows, page doesn't crash
+
+## SEO
+
+- Page title: "My Dev Deck — Your personal dev tool deck"
+- Meta description: ~150 chars describing the OSS dev tool collection
+- Open Graph image: placeholder for now (can be added in a follow-up PR)
+
+## Deployment considerations
+
+- Env vars `GITHUB_REPO_OWNER`, `GITHUB_REPO_NAME`, `GITHUB_DISCUSSIONS_CATEGORY_ID` must be set on Fly.io before landing page deploys, or the wishlist shows empty state permanently
+- Static screenshot must be committed to `public/landing/devinbox-preview.png` before deploy
+- No database migration needed
+- No API changes needed
+- Next.js ISR handles cache invalidation automatically via `revalidate: 300`
+
+## Open decisions (deferred)
+
+- OG image design and asset creation — follow-up PR
+- Dark mode for dashboard — not in this scope
+- In-app feature voting (vs. GitHub-hosted) — deferred, GitHub-hosted is sufficient for current scale
+- `sameSite` cookie strategy for cross-site fly.dev — unrelated issue being tracked separately
diff --git a/docs/plans/2026-04-17-landing-page.md b/docs/plans/2026-04-17-landing-page.md
new file mode 100644
index 0000000..c041ccd
--- /dev/null
+++ b/docs/plans/2026-04-17-landing-page.md
@@ -0,0 +1,1553 @@
+# Landing Page Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Build a landing page at `/` that showcases My Dev Deck as an OSS dev tool deck, drives the `Try Demo` conversion, highlights DevInbox, and surfaces a live GitHub Discussions feature wishlist.
+
+**Architecture:** Next.js 16 App Router Server Component page at `/`, wrapped in a `(landing)` route group layout with Header + Footer. Client islands (`'use client'`) for interactive and animated pieces. Server-side GitHub GraphQL fetch with 5-minute revalidation caches the feature wishlist. Dark hero + light sections with Framer Motion scroll-triggered animations.
+
+**Tech Stack:** Next.js 16, React 19, TypeScript, Tailwind CSS v4, Framer Motion, GitHub GraphQL API, js-cookie (existing).
+
+**Design doc:** `docs/plans/2026-04-17-landing-page-design.md`
+
+**Testing note:** This is a UI-heavy feature where the "test" is the rendered result. We rely on lint + typecheck after each task, and a full manual QA pass at the end. No unit tests are added for individual components (content-heavy, low logic complexity).
+
+---
+
+## Task 1: Install Framer Motion and wire env vars
+
+**Files:**
+- Modify: `apps/web/package.json`
+- Modify: `apps/web/.env.example`
+
+**Step 1: Install Framer Motion**
+
+```bash
+pnpm --filter web add framer-motion
+```
+
+Expected output: `framer-motion` added under `dependencies` in `apps/web/package.json`.
+
+**Step 2: Add env var placeholders**
+
+Append to `apps/web/.env.example`:
+
+```
+# GitHub Discussions integration for landing page feature wishlist
+GITHUB_REPO_OWNER=
+GITHUB_REPO_NAME=
+GITHUB_DISCUSSIONS_CATEGORY_ID=
+```
+
+**Step 3: Add env vars locally (user does this manually, outside this plan)**
+
+Tell the user to:
+1. Follow the setup guide in `docs/plans/2026-04-17-landing-page-design.md` Section "GitHub Discussions setup guide"
+2. Populate `apps/web/.env.local` with real values after creating the Discussions category
+3. The plan's implementation works even if env vars are empty (renders empty state), so don't block on this
+
+**Step 4: Run typecheck to confirm no regressions**
+
+```bash
+pnpm --filter web check-types
+```
+
+Expected: PASS.
+
+**Step 5: Commit**
+
+```bash
+git add apps/web/package.json apps/web/.env.example pnpm-lock.yaml
+git commit -m "chore(web): add framer-motion and github env vars for landing page"
+```
+
+---
+
+## Task 2: Create landing route group and layout
+
+**Files:**
+- Create: `apps/web/app/(landing)/layout.tsx`
+- Create: `apps/web/components/landing/Header.tsx` (minimal stub)
+- Create: `apps/web/components/landing/Footer.tsx` (minimal stub)
+
+**Step 1: Create stub Header**
+
+Create `apps/web/components/landing/Header.tsx`:
+
+```tsx
+export function Header() {
+ return (
+
+ );
+}
+```
+
+**Step 2: Create stub Footer**
+
+Create `apps/web/components/landing/Footer.tsx`:
+
+```tsx
+export function Footer() {
+ return (
+
+ © 2026 My Dev Deck • MIT Licensed
+
+ );
+}
+```
+
+**Step 3: Create landing layout**
+
+Create `apps/web/app/(landing)/layout.tsx`:
+
+```tsx
+import { Header } from '../../components/landing/Header';
+import { Footer } from '../../components/landing/Footer';
+
+export default function LandingLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
+```
+
+**Step 4: Run typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 5: Commit**
+
+```bash
+git add apps/web/app/\(landing\)/layout.tsx apps/web/components/landing/Header.tsx apps/web/components/landing/Footer.tsx
+git commit -m "feat(web): scaffold landing route group with header/footer stubs"
+```
+
+---
+
+## Task 3: Replace root page with landing page stub and update middleware
+
+**Files:**
+- Delete: `apps/web/app/page.tsx` (current redirect)
+- Create: `apps/web/app/(landing)/page.tsx`
+- Modify: `apps/web/proxy.ts` line 4
+
+**Step 1: Delete existing root page**
+
+```bash
+rm apps/web/app/page.tsx
+```
+
+This avoids a route conflict — `app/(landing)/page.tsx` will now serve `/`.
+
+**Step 2: Create new landing page stub**
+
+Create `apps/web/app/(landing)/page.tsx`:
+
+```tsx
+export default function LandingPage() {
+ return (
+
+
+ Landing page coming soon
+
+
+ );
+}
+```
+
+**Step 3: Update middleware to allow `/` publicly**
+
+Edit `apps/web/proxy.ts` line 4:
+
+Change:
+```ts
+const publicPaths = ['/login', '/setup'];
+```
+
+To:
+```ts
+const publicPaths = ['/', '/login', '/setup'];
+```
+
+The `authPaths` array stays unchanged. This means logged-in users can visit `/` without being redirected.
+
+**Step 4: Start dev server and manually verify**
+
+```bash
+pnpm --filter web dev
+```
+
+Visit `http://localhost:4001/` — should see the stub "Landing page coming soon" (both logged in and logged out). Stop the dev server.
+
+**Step 5: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 6: Commit**
+
+```bash
+git add apps/web/app/page.tsx apps/web/app/\(landing\)/page.tsx apps/web/proxy.ts
+git commit -m "feat(web): replace / redirect with landing page stub, make / public"
+```
+
+Note: `git add apps/web/app/page.tsx` stages the deletion.
+
+---
+
+## Task 4: Extract TryDemoButton component
+
+**Files:**
+- Create: `apps/web/components/landing/TryDemoButton.tsx`
+
+**Step 1: Create the component**
+
+Create `apps/web/components/landing/TryDemoButton.tsx`:
+
+```tsx
+'use client';
+
+import { useState } from 'react';
+import Cookies from 'js-cookie';
+import { tryDemo } from '../../lib/api';
+
+const DEMO_MODE_ENABLED = process.env.NEXT_PUBLIC_DEMO_MODE_ENABLED === 'true';
+
+type Variant = 'primary' | 'secondary' | 'nav';
+
+interface Props {
+ variant?: Variant;
+ className?: string;
+}
+
+const VARIANT_CLASSES: Record = {
+ primary:
+ 'px-6 py-3 text-base bg-blue-600 text-white rounded-md hover:bg-blue-700 hover:scale-105 transition-all disabled:opacity-50',
+ secondary:
+ 'px-5 py-2.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50',
+ nav: 'px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50',
+};
+
+export function TryDemoButton({ variant = 'primary', className = '' }: Props) {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ if (!DEMO_MODE_ENABLED) return null;
+
+ const handleClick = async () => {
+ setError('');
+ setLoading(true);
+ try {
+ const res = await tryDemo();
+ if (res.status === 201 || res.ok) {
+ Cookies.set('session', 'active', { path: '/', expires: 1 });
+ window.location.href = '/dashboard';
+ return;
+ }
+ if (res.status === 429) {
+ setError('Demo limit reached, try again later.');
+ return;
+ }
+ if (res.status === 404) {
+ setError('Demo is not available right now.');
+ return;
+ }
+ let message = 'Could not start demo. Please try again.';
+ try {
+ const body = await res.json();
+ if (body?.message) message = body.message;
+ } catch {
+ // ignore
+ }
+ setError(message);
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Could not start demo.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+ {loading ? 'Starting demo...' : 'Try Demo'}
+
+ {error && (
+
{error}
+ )}
+
+ );
+}
+```
+
+**Step 2: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 3: Commit**
+
+```bash
+git add apps/web/components/landing/TryDemoButton.tsx
+git commit -m "feat(web): add reusable TryDemoButton for landing page"
+```
+
+---
+
+## Task 5: Build Header with scroll transition and nav links
+
+**Files:**
+- Modify: `apps/web/components/landing/Header.tsx` (rewrite)
+
+**Step 1: Rewrite Header as a client component with scroll state and mobile menu**
+
+Replace contents of `apps/web/components/landing/Header.tsx`:
+
+```tsx
+'use client';
+
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { TryDemoButton } from './TryDemoButton';
+
+const GITHUB_URL = 'https://github.com/Rowee13/my-dev-deck';
+
+export function Header() {
+ const [scrolled, setScrolled] = useState(false);
+ const [mobileOpen, setMobileOpen] = useState(false);
+
+ useEffect(() => {
+ const onScroll = () => setScrolled(window.scrollY > 50);
+ onScroll();
+ window.addEventListener('scroll', onScroll, { passive: true });
+ return () => window.removeEventListener('scroll', onScroll);
+ }, []);
+
+ return (
+
+
+
+ My Dev Deck
+
+
+
+
+
setMobileOpen((v) => !v)}
+ >
+
+ {mobileOpen ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {mobileOpen && (
+
+ )}
+
+ );
+}
+
+function GitHubIcon() {
+ return (
+
+
+
+ );
+}
+```
+
+**Step 2: Run dev server and verify**
+
+```bash
+pnpm --filter web dev
+```
+
+Visit `/` — header renders transparent at top. Scroll down → transitions to blurred dark. Resize to narrow width → hamburger appears. Click hamburger → menu opens. Stop dev server.
+
+**Step 3: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 4: Commit**
+
+```bash
+git add apps/web/components/landing/Header.tsx
+git commit -m "feat(web): build landing header with scroll transition and mobile menu"
+```
+
+---
+
+## Task 6: Build HeroSection
+
+**Files:**
+- Create: `apps/web/components/landing/HeroSection.tsx`
+- Modify: `apps/web/app/(landing)/page.tsx`
+
+**Step 1: Create HeroSection**
+
+Create `apps/web/components/landing/HeroSection.tsx`:
+
+```tsx
+'use client';
+
+import { motion } from 'framer-motion';
+import { TryDemoButton } from './TryDemoButton';
+
+const GITHUB_URL = 'https://github.com/Rowee13/my-dev-deck';
+
+export function HeroSection() {
+ return (
+
+ {/* Decorative gradient orbs */}
+
+
+
+
+
+ Your personal dev tool deck
+
+
+
+ An open-source, self-hostable collection of developer tools — built in public, shaped by the community.
+
+
+
+
+
+
+
+
+ View on GitHub
+
+
+
+
+ );
+}
+```
+
+**Step 2: Wire into page**
+
+Replace `apps/web/app/(landing)/page.tsx` contents:
+
+```tsx
+import { HeroSection } from '../../components/landing/HeroSection';
+
+export default function LandingPage() {
+ return (
+ <>
+
+ >
+ );
+}
+```
+
+**Step 3: Start dev server and verify**
+
+```bash
+pnpm --filter web dev
+```
+
+Visit `/`. Expected: dark gradient hero fills viewport, headline + subhead + two CTAs fade in in sequence. Orbs pulse. Stop dev server.
+
+**Step 4: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 5: Commit**
+
+```bash
+git add apps/web/components/landing/HeroSection.tsx apps/web/app/\(landing\)/page.tsx
+git commit -m "feat(web): build landing hero section with CTAs and entrance animation"
+```
+
+---
+
+## Task 7: Build BrowserFrame and ToolShowcaseSection
+
+**Files:**
+- Create: `apps/web/components/landing/BrowserFrame.tsx`
+- Create: `apps/web/components/landing/ToolShowcaseSection.tsx`
+- Create: `apps/web/public/landing/devinbox-preview.png` (placeholder, user captures real screenshot later)
+- Modify: `apps/web/app/(landing)/page.tsx`
+
+**Step 1: Create BrowserFrame**
+
+Create `apps/web/components/landing/BrowserFrame.tsx`:
+
+```tsx
+interface Props {
+ children: React.ReactNode;
+ url?: string;
+}
+
+export function BrowserFrame({ children, url = 'devinbox.mydevdeck.com' }: Props) {
+ return (
+
+ );
+}
+```
+
+**Step 2: Add placeholder screenshot**
+
+Create `apps/web/public/landing/` directory and add a placeholder `devinbox-preview.png`. For now, commit a text placeholder or a generic preview image. The user will replace with a real screenshot during manual QA.
+
+```bash
+mkdir -p apps/web/public/landing
+```
+
+Then use a placeholder image (for example, copy an existing image from the project or generate a 1280x800 solid-color PNG as placeholder). Document this as a follow-up in the task commit message.
+
+**Step 3: Create ToolShowcaseSection**
+
+Create `apps/web/components/landing/ToolShowcaseSection.tsx`:
+
+```tsx
+'use client';
+
+import { motion } from 'framer-motion';
+import Image from 'next/image';
+import Link from 'next/link';
+import { BrowserFrame } from './BrowserFrame';
+
+export function ToolShowcaseSection() {
+ return (
+
+
+
+
+ Tools in the deck
+
+
+ A growing collection of utilities for everyday development work.
+
+
+
+
+
+
+ Tool #1
+
+
+ DevInbox
+
+
+ A self-hosted email testing inbox. Route your app's transactional emails to DevInbox during development, inspect message content, headers, and attachments, and verify user signup flows without spamming real mailboxes.
+
+
+ Try DevInbox
+
→
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+```
+
+**Step 4: Wire into page**
+
+Update `apps/web/app/(landing)/page.tsx`:
+
+```tsx
+import { HeroSection } from '../../components/landing/HeroSection';
+import { ToolShowcaseSection } from '../../components/landing/ToolShowcaseSection';
+
+export default function LandingPage() {
+ return (
+ <>
+
+
+ >
+ );
+}
+```
+
+**Step 5: Start dev server and verify**
+
+Visit `/`. Scroll past hero. Tool showcase section fades in. Card hover lifts. Image displays (or shows placeholder/broken icon until a real screenshot is added). Stop dev server.
+
+**Step 6: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 7: Commit**
+
+```bash
+git add apps/web/components/landing/BrowserFrame.tsx apps/web/components/landing/ToolShowcaseSection.tsx apps/web/app/\(landing\)/page.tsx apps/web/public/landing/
+git commit -m "feat(web): add tool showcase section with browser-framed devinbox preview"
+```
+
+---
+
+## Task 8: Build ValuePropsSection
+
+**Files:**
+- Create: `apps/web/components/landing/ValuePropsSection.tsx`
+- Modify: `apps/web/app/(landing)/page.tsx`
+
+**Step 1: Create ValuePropsSection**
+
+Create `apps/web/components/landing/ValuePropsSection.tsx`:
+
+```tsx
+'use client';
+
+import { motion } from 'framer-motion';
+
+const VALUE_PROPS = [
+ {
+ icon: '📜',
+ title: 'Open source',
+ description: 'MIT-licensed. Every line of code is on GitHub. Fork it, audit it, contribute back.',
+ },
+ {
+ icon: '🏠',
+ title: 'Self-hostable',
+ description: 'Docker Compose runs the whole stack locally or on your own server. Your data stays yours.',
+ },
+ {
+ icon: '🧰',
+ title: 'Growing toolset',
+ description: 'More tools are added over time. Suggest features, upvote what matters, shape the deck with us.',
+ },
+];
+
+export function ValuePropsSection() {
+ return (
+
+
+
+
+ Why My Dev Deck
+
+
+
+
+ {VALUE_PROPS.map((prop) => (
+
+
+ {prop.icon}
+
+
+ {prop.title}
+
+ {prop.description}
+
+ ))}
+
+
+
+ );
+}
+```
+
+**Step 2: Wire into page**
+
+Update `apps/web/app/(landing)/page.tsx`:
+
+```tsx
+import { HeroSection } from '../../components/landing/HeroSection';
+import { ToolShowcaseSection } from '../../components/landing/ToolShowcaseSection';
+import { ValuePropsSection } from '../../components/landing/ValuePropsSection';
+
+export default function LandingPage() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+```
+
+**Step 3: Start dev server and verify**
+
+Scroll to value props section. Three cards fade in with 100ms stagger. Hover borders turn blue with shadow. Stop dev server.
+
+**Step 4: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 5: Commit**
+
+```bash
+git add apps/web/components/landing/ValuePropsSection.tsx apps/web/app/\(landing\)/page.tsx
+git commit -m "feat(web): add value props section with staggered reveal animation"
+```
+
+---
+
+## Task 9: Build GitHub Discussions server client
+
+**Files:**
+- Create: `apps/web/lib/github.ts`
+
+**Step 1: Create the server-only module**
+
+Create `apps/web/lib/github.ts`:
+
+```ts
+import 'server-only';
+
+export interface FeatureIdea {
+ id: string;
+ title: string;
+ bodyExcerpt: string;
+ url: string;
+ upvotes: number;
+ commentCount: number;
+}
+
+interface DiscussionNode {
+ id: string;
+ title: string;
+ bodyText: string;
+ url: string;
+ upvoteCount: number;
+ comments: { totalCount: number };
+}
+
+interface GraphQLResponse {
+ data?: {
+ repository?: {
+ discussions?: {
+ nodes?: DiscussionNode[];
+ };
+ };
+ };
+ errors?: unknown;
+}
+
+const GITHUB_GRAPHQL = 'https://api.github.com/graphql';
+const EXCERPT_LENGTH = 120;
+const REVALIDATE_SECONDS = 300; // 5 minutes
+
+const QUERY = `
+ query ($owner: String!, $name: String!, $category: ID!) {
+ repository(owner: $owner, name: $name) {
+ discussions(first: 5, categoryId: $category, orderBy: { field: CREATED_AT, direction: DESC }) {
+ nodes {
+ id
+ title
+ bodyText
+ url
+ upvoteCount
+ comments { totalCount }
+ }
+ }
+ }
+ }
+`;
+
+function excerpt(text: string): string {
+ const cleaned = text.trim().replace(/\s+/g, ' ');
+ if (cleaned.length <= EXCERPT_LENGTH) return cleaned;
+ return cleaned.slice(0, EXCERPT_LENGTH).trimEnd() + '…';
+}
+
+export async function fetchFeatureIdeas(): Promise {
+ const owner = process.env.GITHUB_REPO_OWNER;
+ const name = process.env.GITHUB_REPO_NAME;
+ const category = process.env.GITHUB_DISCUSSIONS_CATEGORY_ID;
+
+ if (!owner || !name || !category) return [];
+
+ try {
+ const res = await fetch(GITHUB_GRAPHQL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ body: JSON.stringify({
+ query: QUERY,
+ variables: { owner, name, category },
+ }),
+ next: { revalidate: REVALIDATE_SECONDS },
+ });
+
+ if (!res.ok) {
+ console.error('[github] discussions fetch failed', res.status);
+ return [];
+ }
+
+ const json = (await res.json()) as GraphQLResponse;
+ if (json.errors) {
+ console.error('[github] discussions GraphQL errors', json.errors);
+ return [];
+ }
+
+ const nodes = json.data?.repository?.discussions?.nodes ?? [];
+ return nodes.map((n) => ({
+ id: n.id,
+ title: n.title,
+ bodyExcerpt: excerpt(n.bodyText ?? ''),
+ url: n.url,
+ upvotes: n.upvoteCount,
+ commentCount: n.comments.totalCount,
+ }));
+ } catch (err) {
+ console.error('[github] discussions fetch error', err);
+ return [];
+ }
+}
+```
+
+**Step 2: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 3: Commit**
+
+```bash
+git add apps/web/lib/github.ts
+git commit -m "feat(web): add github discussions graphql client for feature wishlist"
+```
+
+---
+
+## Task 10: Build FeatureWishlistItem
+
+**Files:**
+- Create: `apps/web/components/landing/FeatureWishlistItem.tsx`
+
+**Step 1: Create the component**
+
+Create `apps/web/components/landing/FeatureWishlistItem.tsx`:
+
+```tsx
+'use client';
+
+import type { FeatureIdea } from '../../lib/github';
+
+interface Props {
+ idea: FeatureIdea;
+}
+
+export function FeatureWishlistItem({ idea }: Props) {
+ return (
+
+
+ {idea.title}
+
+ {idea.bodyExcerpt && (
+
+ {idea.bodyExcerpt}
+
+ )}
+
+
+
+
+
+ {idea.upvotes}
+
+
+
+
+
+ {idea.commentCount}
+
+
+
+ );
+}
+```
+
+**Step 2: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 3: Commit**
+
+```bash
+git add apps/web/components/landing/FeatureWishlistItem.tsx
+git commit -m "feat(web): add feature wishlist item card component"
+```
+
+---
+
+## Task 11: Build FeatureWishlistSection
+
+**Files:**
+- Create: `apps/web/components/landing/FeatureWishlistSection.tsx`
+- Modify: `apps/web/app/(landing)/page.tsx`
+
+**Step 1: Create the server component**
+
+Create `apps/web/components/landing/FeatureWishlistSection.tsx`:
+
+```tsx
+import { fetchFeatureIdeas } from '../../lib/github';
+import { FeatureWishlistItem } from './FeatureWishlistItem';
+
+function getRepoUrl(): string {
+ const owner = process.env.GITHUB_REPO_OWNER || 'Rowee13';
+ const name = process.env.GITHUB_REPO_NAME || 'my-dev-deck';
+ return `https://github.com/${owner}/${name}`;
+}
+
+export async function FeatureWishlistSection() {
+ const ideas = await fetchFeatureIdeas();
+ const repoUrl = getRepoUrl();
+ const submitUrl = `${repoUrl}/discussions/new?category=feature-ideas`;
+ const allUrl = `${repoUrl}/discussions`;
+
+ return (
+
+
+
+
+ What's next?
+
+
+ A community-driven wishlist. Upvote the ideas you want, or submit your own on GitHub.
+
+
+
+ {ideas.length > 0 ? (
+
+ {ideas.map((idea) => (
+
+ ))}
+
+ ) : (
+
+
No ideas yet.
+
+ Be the first to share what you'd like to see added to the deck.
+
+
+ )}
+
+
+
+
+ );
+}
+```
+
+**Step 2: Wire into page**
+
+Update `apps/web/app/(landing)/page.tsx`:
+
+```tsx
+import { HeroSection } from '../../components/landing/HeroSection';
+import { ToolShowcaseSection } from '../../components/landing/ToolShowcaseSection';
+import { ValuePropsSection } from '../../components/landing/ValuePropsSection';
+import { FeatureWishlistSection } from '../../components/landing/FeatureWishlistSection';
+
+export default function LandingPage() {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
+```
+
+**Step 3: Start dev server and verify**
+
+Without GitHub env vars populated, visit `/` and scroll to the wishlist section. Should see the empty state + Submit/See all links. Stop dev server.
+
+**Step 4: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 5: Commit**
+
+```bash
+git add apps/web/components/landing/FeatureWishlistSection.tsx apps/web/app/\(landing\)/page.tsx
+git commit -m "feat(web): add feature wishlist section with github discussions fetch"
+```
+
+---
+
+## Task 12: Finalize Footer
+
+**Files:**
+- Modify: `apps/web/components/landing/Footer.tsx` (rewrite)
+
+**Step 1: Rewrite Footer with real links**
+
+Replace `apps/web/components/landing/Footer.tsx`:
+
+```tsx
+import Link from 'next/link';
+
+const GITHUB_URL = 'https://github.com/Rowee13/my-dev-deck';
+
+export function Footer() {
+ return (
+
+ );
+}
+```
+
+**Step 2: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 3: Commit**
+
+```bash
+git add apps/web/components/landing/Footer.tsx
+git commit -m "feat(web): finalize landing footer with github and login links"
+```
+
+---
+
+## Task 13: Remove demo button from login page and link wordmark to `/`
+
+**Files:**
+- Modify: `apps/web/app/login/page.tsx`
+
+**Step 1: Rewrite the login page**
+
+Replace `apps/web/app/login/page.tsx` contents:
+
+```tsx
+'use client';
+
+import { useState } from 'react';
+import { useRouter } from 'next/navigation';
+import Link from 'next/link';
+import { useAuth } from '../../contexts/AuthContext';
+
+export default function LoginPage() {
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+
+ const router = useRouter();
+ const { login } = useAuth();
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+ setLoading(true);
+
+ try {
+ await login(email, password);
+ router.push('/dashboard');
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Login failed');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
My Dev Deck
+
+
Sign in to access your developer tools
+
+
+
+
+ );
+}
+```
+
+**Step 2: Start dev server and verify**
+
+```bash
+pnpm --filter web dev
+```
+
+Visit `/login`. Expected:
+- No "Try Demo" button at bottom of card
+- "My Dev Deck" heading is a clickable link → takes you to `/`
+- Login form works normally
+
+Stop dev server.
+
+**Step 3: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 4: Commit**
+
+```bash
+git add apps/web/app/login/page.tsx
+git commit -m "feat(web): remove demo button from login, link wordmark to landing"
+```
+
+---
+
+## Task 14: Add page metadata
+
+**Files:**
+- Modify: `apps/web/app/(landing)/layout.tsx`
+
+**Step 1: Add metadata export**
+
+Update `apps/web/app/(landing)/layout.tsx`:
+
+```tsx
+import type { Metadata } from 'next';
+import { Header } from '../../components/landing/Header';
+import { Footer } from '../../components/landing/Footer';
+
+export const metadata: Metadata = {
+ title: 'My Dev Deck — Your personal dev tool deck',
+ description:
+ 'An open-source, self-hostable collection of developer tools. Built in public, shaped by the community. Try the demo or explore on GitHub.',
+};
+
+export default function LandingLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+ <>
+
+ {children}
+
+ >
+ );
+}
+```
+
+**Step 2: Typecheck and lint**
+
+```bash
+pnpm --filter web check-types && pnpm --filter web lint
+```
+
+Expected: PASS.
+
+**Step 3: Commit**
+
+```bash
+git add apps/web/app/\(landing\)/layout.tsx
+git commit -m "feat(web): add landing page metadata for seo"
+```
+
+---
+
+## Task 15: Full manual QA pass
+
+**Goal:** Walk through the QA checklist from the design doc before opening a PR.
+
+**Step 1: Ensure env vars are populated (if seed discussions exist)**
+
+If the user has completed the GitHub Discussions setup guide and seeded ideas, confirm `apps/web/.env.local` has all three `GITHUB_*` vars filled in. Otherwise the wishlist will show empty state — that's also a valid scenario to verify.
+
+**Step 2: Replace placeholder screenshot with a real one**
+
+1. Start dev server and navigate to DevInbox dashboard with sample data
+2. Take a screenshot of the inbox view (1280×800 recommended)
+3. Save as `apps/web/public/landing/devinbox-preview.png`, replacing the placeholder
+4. Commit separately:
+
+```bash
+git add apps/web/public/landing/devinbox-preview.png
+git commit -m "chore(web): add real devinbox preview screenshot for landing page"
+```
+
+**Step 3: Run the full QA checklist**
+
+Start dev server:
+
+```bash
+pnpm --filter web dev
+```
+
+Walk through every item from the design doc's QA checklist:
+
+- [ ] `/` logged out → full landing with wishlist
+- [ ] `/` logged in → full landing (no redirect)
+- [ ] `Try Demo` in hero → `/dashboard` with banner + expiration
+- [ ] `Try Demo` in header → same as above
+- [ ] `View on GitHub` → repo in new tab
+- [ ] `Login` link (logged out) → `/login`
+- [ ] `Login` link (logged in) → redirects to `/dashboard` via middleware
+- [ ] `/login` logged out → no Try Demo button
+- [ ] `/login` wordmark → clicks through to `/`
+- [ ] Scroll → header transitions transparent → solid blur
+- [ ] Hover tool card → lifts with shadow
+- [ ] Hover value props → border turns blue with shadow
+- [ ] Hover wishlist items → border turns blue, lifts slightly
+- [ ] Scroll each section into view → entrance animation plays
+- [ ] Resize to mobile → sections stack, hamburger works
+- [ ] Disable JS → page content still renders (Server Components)
+- [ ] System `prefers-reduced-motion: reduce` → animations minimized
+- [ ] Tab with keyboard → all CTAs focusable in logical order
+- [ ] Wishlist shows real discussions (if env vars set) or empty state
+- [ ] Temporarily set invalid `GITHUB_DISCUSSIONS_CATEGORY_ID` → empty state shows, page doesn't crash
+
+**Step 4: Fix any issues found**
+
+Address bugs surfaced by the QA pass in additional commits. Keep each fix focused (1-3 lines of change per commit where possible).
+
+**Step 5: Final lint, typecheck, and build**
+
+```bash
+pnpm --filter web lint
+pnpm --filter web check-types
+pnpm --filter web build
+```
+
+All must PASS. Build confirms production bundle works (this catches issues Framer Motion might have with RSC boundaries).
+
+**Step 6: Push branch and open PR**
+
+```bash
+git push -u origin feat/landing-page
+```
+
+Then open a PR from `feat/landing-page` → `main` with this description:
+
+```
+## Summary
+- Replace / redirect with a landing page (dark hero, light sections)
+- Move Try Demo CTA from login page to landing hero + nav
+- Add GitHub Discussions-backed feature wishlist (5-min server cache)
+- Make login page wordmark a link back to /
+- Make / publicly accessible via middleware update
+
+## Test plan
+- [ ] Landing page renders logged out and logged in
+- [ ] Try Demo button still works from hero and header
+- [ ] Login page has no Try Demo button
+- [ ] Login page wordmark links to /
+- [ ] Feature wishlist fetches real GitHub Discussions (or shows empty state)
+- [ ] All animations and hover effects work
+- [ ] Mobile layout stacks correctly
+```
+
+---
+
+## Summary
+
+15 tasks, each ~2-10 minutes of implementation:
+
+1. Install Framer Motion + env vars
+2. Landing route group + layout + stubs
+3. Replace root page + middleware update
+4. TryDemoButton component
+5. Header with scroll transition + mobile menu
+6. HeroSection
+7. BrowserFrame + ToolShowcaseSection
+8. ValuePropsSection
+9. GitHub GraphQL client
+10. FeatureWishlistItem
+11. FeatureWishlistSection
+12. Finalize Footer
+13. Clean up login page
+14. Page metadata
+15. Manual QA + screenshot + PR
+
+Total estimated commits: ~16 (one per task + extra for screenshot + any QA fixes).
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 21ffdeb..8fb75b5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -259,6 +259,9 @@ importers:
'@repo/ui':
specifier: workspace:*
version: link:../../packages/ui
+ framer-motion:
+ specifier: ^12.38.0
+ version: 12.38.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
js-cookie:
specifier: ^3.0.5
version: 3.0.5
@@ -3027,6 +3030,20 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
+ framer-motion@12.38.0:
+ resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0 || ^19.0.0
+ react-dom: ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
@@ -3914,6 +3931,12 @@ packages:
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
engines: {node: '>=16 || 14 >=14.17'}
+ motion-dom@12.38.0:
+ resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
+
+ motion-utils@12.36.0:
+ resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
+
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
@@ -8158,6 +8181,15 @@ snapshots:
fraction.js@4.3.7: {}
+ framer-motion@12.38.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ motion-dom: 12.38.0
+ motion-utils: 12.36.0
+ tslib: 2.8.1
+ optionalDependencies:
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+
fresh@2.0.0: {}
fs-extra@10.1.0:
@@ -9218,6 +9250,12 @@ snapshots:
minipass@7.1.3: {}
+ motion-dom@12.38.0:
+ dependencies:
+ motion-utils: 12.36.0
+
+ motion-utils@12.36.0: {}
+
mri@1.2.0: {}
ms@2.1.3: {}
diff --git a/turbo.json b/turbo.json
index 13745da..3bfb8e2 100644
--- a/turbo.json
+++ b/turbo.json
@@ -5,7 +5,13 @@
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
- "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
+ "outputs": ["dist/**", ".next/**", "!.next/cache/**"],
+ "env": [
+ "GITHUB_REPO_OWNER",
+ "GITHUB_REPO_NAME",
+ "GITHUB_DISCUSSIONS_CATEGORY_ID",
+ "GITHUB_TOKEN"
+ ]
},
"lint": {
"dependsOn": ["^lint"]