Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cdfe175
docs(landing): add landing page design doc
Rowee13 Apr 17, 2026
c71dae5
docs(landing): add landing page implementation plan
Rowee13 Apr 17, 2026
ed93fbb
chore(web): add framer-motion and github env vars for landing page
Rowee13 Apr 17, 2026
758bdfd
feat(web): scaffold landing route group with header/footer stubs
Rowee13 Apr 17, 2026
a7e4de9
feat(web): replace / redirect with landing page stub, make / public
Rowee13 Apr 17, 2026
0c2aa1b
feat(web): add reusable TryDemoButton for landing page
Rowee13 Apr 17, 2026
3832877
feat(web): build landing header with scroll transition and mobile menu
Rowee13 Apr 17, 2026
6fe56cc
feat(web): build landing hero section with CTAs and entrance animation
Rowee13 Apr 17, 2026
2df778a
feat(web): add tool showcase section with browser-framed devinbox pre…
Rowee13 Apr 17, 2026
2785f4b
feat(web): add value props section with staggered reveal animation
Rowee13 Apr 17, 2026
caeb4a8
feat(web): add github discussions graphql client for feature wishlist
Rowee13 Apr 17, 2026
9716e09
feat(web): add feature wishlist item card component
Rowee13 Apr 17, 2026
7fd93c6
feat(web): add feature wishlist section with github discussions fetch
Rowee13 Apr 17, 2026
8623102
feat(web): finalize landing footer with github and login links
Rowee13 Apr 17, 2026
0610162
feat(web): remove demo button from login, link wordmark to landing
Rowee13 Apr 17, 2026
58a846b
feat(web): add landing page metadata for seo
Rowee13 Apr 17, 2026
9efac67
fix(api): throw UnauthorizedException for missing refresh token
Rowee13 Apr 17, 2026
1ac2788
fix(web): authenticate github discussions fetch with bearer token
Rowee13 Apr 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Post,
Req,
Res,
UnauthorizedException,
UseGuards,
} from '@nestjs/common';
import {
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
23 changes: 23 additions & 0 deletions apps/web/app/(landing)/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Header />
<main>{children}</main>
<Footer />
</>
);
}
15 changes: 15 additions & 0 deletions apps/web/app/(landing)/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<HeroSection />
<ToolShowcaseSection />
<ValuePropsSection />
<FeatureWishlistSection />
</>
);
}
94 changes: 11 additions & 83 deletions apps/web/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -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('');
Expand All @@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">My Dev Deck</h1>
<Link
href="/"
className="inline-block mb-2 hover:opacity-80 transition-opacity"
>
<h1 className="text-3xl font-bold text-gray-900">My Dev Deck</h1>
</Link>
<p className="text-gray-600 mb-6">Sign in to access your developer tools</p>

<form onSubmit={handleSubmit}>
Expand All @@ -113,7 +57,7 @@ export default function LoginPage() {
onChange={(e) => setEmail(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="email"
/>
</div>
Expand All @@ -128,35 +72,19 @@ 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"
/>
</div>

<button
type="submit"
className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors disabled:opacity-50"
disabled={loading || demoLoading}
disabled={loading}
>
{loading ? 'Signing in...' : 'Sign In'}
</button>
</form>

{demoVisible && (
<div className="mt-6 pt-6 border-t border-gray-200">
<p className="text-center text-sm text-gray-500 mb-3">
No account? Explore with a demo.
</p>
<button
type="button"
onClick={handleTryDemo}
disabled={loading || demoLoading}
className="w-full px-4 py-2 bg-white text-blue-600 border border-blue-600 rounded-md hover:bg-blue-50 transition-colors disabled:opacity-50"
>
{demoLoading ? 'Starting demo...' : 'Try Demo'}
</button>
</div>
)}
</div>
</div>
);
Expand Down
5 changes: 0 additions & 5 deletions apps/web/app/page.tsx

This file was deleted.

24 changes: 24 additions & 0 deletions apps/web/components/landing/BrowserFrame.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
interface Props {
children: React.ReactNode;
url?: string;
}

export function BrowserFrame({ children, url = 'devinbox.mydevdeck.com' }: Props) {
return (
<div className="rounded-xl overflow-hidden shadow-2xl ring-1 ring-black/5 bg-white">
<div className="flex items-center gap-2 px-4 py-3 bg-gray-100 border-b border-gray-200">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-400" />
<div className="w-3 h-3 rounded-full bg-yellow-400" />
<div className="w-3 h-3 rounded-full bg-green-400" />
</div>
<div className="flex-1 mx-4">
<div className="px-3 py-1 bg-white rounded text-xs text-gray-500 text-center max-w-sm mx-auto">
{url}
</div>
</div>
</div>
<div>{children}</div>
</div>
);
}
41 changes: 41 additions & 0 deletions apps/web/components/landing/FeatureWishlistItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import type { FeatureIdea } from '../../lib/github';

interface Props {
idea: FeatureIdea;
}

export function FeatureWishlistItem({ idea }: Props) {
return (
<a
href={idea.url}
target="_blank"
rel="noopener noreferrer"
className="block p-6 bg-white rounded-xl border border-gray-200 hover:border-blue-300 hover:bg-blue-50/50 hover:-translate-y-0.5 transition-all duration-200"
>
<h3 className="text-lg font-semibold text-gray-900 mb-2 line-clamp-1">
{idea.title}
</h3>
{idea.bodyExcerpt && (
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{idea.bodyExcerpt}
</p>
)}
<div className="flex items-center gap-4 text-sm text-gray-500">
<span className="flex items-center gap-1">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
</svg>
{idea.upvotes}
</span>
<span className="flex items-center gap-1">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
{idea.commentCount}
</span>
</div>
</a>
);
}
64 changes: 64 additions & 0 deletions apps/web/components/landing/FeatureWishlistSection.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<section className="py-24 px-6 bg-white">
<div className="max-w-5xl mx-auto">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
What&apos;s next?
</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
A community-driven wishlist. Upvote the ideas you want, or submit your own on GitHub.
</p>
</div>

{ideas.length > 0 ? (
<div className="grid md:grid-cols-2 gap-4 mb-10">
{ideas.map((idea) => (
<FeatureWishlistItem key={idea.id} idea={idea} />
))}
</div>
) : (
<div className="text-center py-12 mb-10 bg-slate-50 rounded-xl border border-gray-200">
<p className="text-gray-600 mb-2">No ideas yet.</p>
<p className="text-sm text-gray-500">
Be the first to share what you&apos;d like to see added to the deck.
</p>
</div>
)}

<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<a
href={submitUrl}
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 hover:scale-105 transition-all"
>
Submit an idea
</a>
<a
href={allUrl}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-gray-600 hover:text-blue-600 transition-colors"
>
See all ideas on GitHub →
</a>
</div>
</div>
</section>
);
}
34 changes: 34 additions & 0 deletions apps/web/components/landing/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Link from 'next/link';

const GITHUB_URL = 'https://github.com/Rowee13/my-dev-deck';

export function Footer() {
return (
<footer className="py-8 px-6 border-t border-gray-200 bg-white">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<p className="text-sm text-gray-500">
© 2026 My Dev Deck • MIT Licensed
</p>
<div className="flex items-center gap-6">
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
aria-label="View on GitHub"
className="text-gray-500 hover:text-gray-900 transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.111.82-.261.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
</a>
<Link
href="/login"
className="text-sm text-gray-500 hover:text-gray-900 transition-colors"
>
Login
</Link>
</div>
</div>
</footer>
);
}
Loading
Loading