Add dark modle

This commit is contained in:
songtianlun 2025-07-28 23:26:50 +08:00
parent a58b9222c3
commit 6a6f1e0454
7 changed files with 620 additions and 142 deletions

View File

@ -1,65 +1,146 @@
@import "tailwindcss"; @import "tailwindcss";
/* Light theme variables */
:root { :root {
--background: #ffffff; --background: 255 255 255;
--foreground: #0f172a; --foreground: 15 23 42;
--muted: #f8fafc; --card: 255 255 255;
--muted-foreground: #64748b; --card-foreground: 15 23 42;
--border: #e2e8f0; --popover: 255 255 255;
--input: #ffffff; --popover-foreground: 15 23 42;
--primary: #0f172a; --primary: 15 23 42;
--primary-foreground: #f8fafc; --primary-foreground: 248 250 252;
--secondary: #f1f5f9; --secondary: 241 245 249;
--secondary-foreground: #0f172a; --secondary-foreground: 15 23 42;
--accent: #f1f5f9; --muted: 248 250 252;
--accent-foreground: #0f172a; --muted-foreground: 100 116 139;
--destructive: #ef4444; --accent: 241 245 249;
--destructive-foreground: #fef2f2; --accent-foreground: 15 23 42;
--ring: #0f172a; --destructive: 239 68 68;
--destructive-foreground: 254 242 242;
--border: 226 232 240;
--input: 255 255 255;
--ring: 15 23 42;
--radius: 0.5rem;
}
/* Dark theme variables */
.dark {
--background: 2 6 23;
--foreground: 248 250 252;
--card: 15 23 42;
--card-foreground: 248 250 252;
--popover: 15 23 42;
--popover-foreground: 248 250 252;
--primary: 248 250 252;
--primary-foreground: 15 23 42;
--secondary: 30 41 59;
--secondary-foreground: 248 250 252;
--muted: 15 23 42;
--muted-foreground: 148 163 184;
--accent: 30 41 59;
--accent-foreground: 248 250 252;
--destructive: 220 38 38;
--destructive-foreground: 254 242 242;
--border: 30 41 59;
--input: 15 23 42;
--ring: 248 250 252;
} }
@theme inline { @theme inline {
--color-background: var(--background); --color-background: rgb(var(--background));
--color-foreground: var(--foreground); --color-foreground: rgb(var(--foreground));
--color-muted: var(--muted); --color-card: rgb(var(--card));
--color-muted-foreground: var(--muted-foreground); --color-card-foreground: rgb(var(--card-foreground));
--color-border: var(--border); --color-popover: rgb(var(--popover));
--color-input: var(--input); --color-popover-foreground: rgb(var(--popover-foreground));
--color-primary: var(--primary); --color-primary: rgb(var(--primary));
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: rgb(var(--primary-foreground));
--color-secondary: var(--secondary); --color-secondary: rgb(var(--secondary));
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: rgb(var(--secondary-foreground));
--color-accent: var(--accent); --color-muted: rgb(var(--muted));
--color-accent-foreground: var(--accent-foreground); --color-muted-foreground: rgb(var(--muted-foreground));
--color-destructive: var(--destructive); --color-accent: rgb(var(--accent));
--color-destructive-foreground: var(--destructive-foreground); --color-accent-foreground: rgb(var(--accent-foreground));
--color-ring: var(--ring); --color-destructive: rgb(var(--destructive));
--color-destructive-foreground: rgb(var(--destructive-foreground));
--color-border: rgb(var(--border));
--color-input: rgb(var(--input));
--color-ring: rgb(var(--ring));
--radius: var(--radius);
--font-sans: var(--font-geist-sans); --font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
} }
@media (prefers-color-scheme: dark) { /* Base styles */
:root { * {
--background: #020617; border-color: rgb(var(--border));
--foreground: #f8fafc; }
--muted: #0f172a;
--muted-foreground: #94a3b8; html {
--border: #1e293b; scroll-behavior: smooth;
--input: #0f172a;
--primary: #f8fafc;
--primary-foreground: #0f172a;
--secondary: #1e293b;
--secondary-foreground: #f8fafc;
--accent: #1e293b;
--accent-foreground: #f8fafc;
--destructive: #dc2626;
--destructive-foreground: #fef2f2;
--ring: #f8fafc;
}
} }
body { body {
background: var(--background); background-color: rgb(var(--background));
color: var(--foreground); color: rgb(var(--foreground));
font-family: var(--font-sans), Arial, Helvetica, sans-serif; font-family: var(--font-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-feature-settings: "rlig" 1, "calt" 1;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Selection styles */
::selection {
background-color: rgb(var(--primary) / 0.1);
color: rgb(var(--primary));
}
/* Focus styles */
:focus-visible {
outline: 2px solid rgb(var(--ring));
outline-offset: 2px;
}
/* Scrollbar styles */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgb(var(--muted));
}
::-webkit-scrollbar-thumb {
background: rgb(var(--muted-foreground) / 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgb(var(--muted-foreground) / 0.5);
}
/* Dark mode scrollbar */
.dark ::-webkit-scrollbar-track {
background: rgb(var(--muted));
}
.dark ::-webkit-scrollbar-thumb {
background: rgb(var(--muted-foreground) / 0.3);
}
.dark ::-webkit-scrollbar-thumb:hover {
background: rgb(var(--muted-foreground) / 0.5);
}
/* Print styles */
@media print {
* {
background: transparent !important;
color: black !important;
box-shadow: none !important;
text-shadow: none !important;
}
} }

View File

@ -1,20 +1,57 @@
import type { Metadata } from "next"; import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { ThemeProvider } from "@/contexts/ThemeContext";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
subsets: ["latin"], subsets: ["latin"],
preload: true,
display: "swap",
}); });
const geistMono = Geist_Mono({ const geistMono = Geist_Mono({
variable: "--font-geist-mono", variable: "--font-geist-mono",
subsets: ["latin"], subsets: ["latin"],
preload: true,
display: "swap",
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Prmbr - AI Prompt Studio", title: "Prmbr - AI Prompt Studio",
description: "Build, manage and optimize your AI prompts with Prmbr", description: "Build, manage and optimize your AI prompts with Prmbr",
keywords: ["AI", "prompt", "builder", "studio", "GPT", "ChatGPT", "Claude"],
authors: [{ name: "Prmbr Team" }],
creator: "Prmbr",
publisher: "Prmbr",
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
manifest: "/manifest.json",
icons: {
icon: "/favicon.ico",
shortcut: "/favicon-16x16.png",
apple: "/apple-touch-icon.png",
},
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#020617" },
],
}; };
export default function RootLayout({ export default function RootLayout({
@ -23,11 +60,32 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en" className="dark:dark"> <html lang="en" suppressHydrationWarning>
<head>
<meta name="theme-color" content="#ffffff" />
<script
dangerouslySetInnerHTML={{
__html: `
try {
if (localStorage.getItem('prmbr-theme') === 'dark' ||
(!localStorage.getItem('prmbr-theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
document.querySelector('meta[name="theme-color"]').setAttribute('content', '#020617')
} else {
document.documentElement.classList.remove('dark')
document.querySelector('meta[name="theme-color"]').setAttribute('content', '#ffffff')
}
} catch (e) {}
`,
}}
/>
</head>
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen bg-background text-foreground`} className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen`}
> >
{children} <ThemeProvider defaultTheme="system" storageKey="prmbr-theme">
{children}
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@ -18,13 +18,13 @@ export default function Home() {
if (user) { if (user) {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen">
<Header /> <Header />
<div className="max-w-4xl mx-auto px-4 py-12 text-center"> <div className="max-w-4xl mx-auto px-4 py-12 text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-4"> <h1 className="text-3xl font-bold text-foreground mb-4">
Welcome to your Prompt Studio! Welcome to your Prompt Studio!
</h1> </h1>
<p className="text-gray-600 mb-8"> <p className="text-muted-foreground mb-8">
Start building, testing, and managing your AI prompts. Start building, testing, and managing your AI prompts.
</p> </p>
<Button size="lg"> <Button size="lg">
@ -37,17 +37,17 @@ export default function Home() {
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen">
<Header /> <Header />
{/* Hero Section */} {/* Hero Section */}
<section className="bg-gradient-to-b from-blue-50 to-white"> <section className="bg-gradient-to-b from-muted/50 to-background">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 pt-20 pb-24">
<div className="text-center"> <div className="text-center">
<h1 className="text-4xl md:text-6xl font-bold text-gray-900 mb-6"> <h1 className="text-4xl md:text-6xl font-bold text-foreground mb-6">
AI Prompt Studio AI Prompt Studio
</h1> </h1>
<p className="text-xl text-gray-600 mb-8 max-w-3xl mx-auto"> <p className="text-xl text-muted-foreground mb-8 max-w-3xl mx-auto">
Build, test, and manage your AI prompts with precision. Build, test, and manage your AI prompts with precision.
Version control, collaboration tools, and analytics in one professional platform. Version control, collaboration tools, and analytics in one professional platform.
</p> </p>
@ -64,54 +64,54 @@ export default function Home() {
</section> </section>
{/* Features Section */} {/* Features Section */}
<section id="features" className="py-24 bg-gray-50"> <section id="features" className="py-24 bg-muted/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
Everything you need to master prompts Everything you need to master prompts
</h2> </h2>
<p className="text-xl text-gray-600"> <p className="text-xl text-muted-foreground">
Professional tools for prompt engineering and management Professional tools for prompt engineering and management
</p> </p>
</div> </div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8"> <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-card p-6 rounded-lg shadow-sm border">
<Target className="h-12 w-12 text-blue-600 mb-4" /> <Target className="h-12 w-12 text-primary mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-card-foreground mb-2">
Prompt Builder Prompt Builder
</h3> </h3>
<p className="text-gray-600"> <p className="text-muted-foreground">
Intuitive interface for crafting and refining AI prompts with real-time preview. Intuitive interface for crafting and refining AI prompts with real-time preview.
</p> </p>
</div> </div>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-card p-6 rounded-lg shadow-sm border">
<Layers className="h-12 w-12 text-blue-600 mb-4" /> <Layers className="h-12 w-12 text-primary mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-card-foreground mb-2">
Version Control Version Control
</h3> </h3>
<p className="text-gray-600"> <p className="text-muted-foreground">
Track changes, compare versions, and rollback to previous iterations seamlessly. Track changes, compare versions, and rollback to previous iterations seamlessly.
</p> </p>
</div> </div>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-card p-6 rounded-lg shadow-sm border">
<BarChart3 className="h-12 w-12 text-blue-600 mb-4" /> <BarChart3 className="h-12 w-12 text-primary mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-card-foreground mb-2">
Test & Analytics Test & Analytics
</h3> </h3>
<p className="text-gray-600"> <p className="text-muted-foreground">
Run tests, analyze performance, and optimize your prompts with detailed metrics. Run tests, analyze performance, and optimize your prompts with detailed metrics.
</p> </p>
</div> </div>
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200"> <div className="bg-card p-6 rounded-lg shadow-sm border">
<Zap className="h-12 w-12 text-blue-600 mb-4" /> <Zap className="h-12 w-12 text-primary mb-4" />
<h3 className="text-lg font-semibold text-gray-900 mb-2"> <h3 className="text-lg font-semibold text-card-foreground mb-2">
Team Collaboration Team Collaboration
</h3> </h3>
<p className="text-gray-600"> <p className="text-muted-foreground">
Share prompts, collaborate with team members, and maintain quality standards. Share prompts, collaborate with team members, and maintain quality standards.
</p> </p>
</div> </div>
@ -120,36 +120,36 @@ export default function Home() {
</section> </section>
{/* Pricing Section */} {/* Pricing Section */}
<section id="pricing" className="py-24 bg-white"> <section id="pricing" className="py-24">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-3xl font-bold text-gray-900 mb-4"> <h2 className="text-3xl font-bold text-foreground mb-4">
Simple, transparent pricing Simple, transparent pricing
</h2> </h2>
<p className="text-xl text-gray-600"> <p className="text-xl text-muted-foreground">
Choose the plan that fits your needs Choose the plan that fits your needs
</p> </p>
</div> </div>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto"> <div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
<div className="bg-white p-8 rounded-lg shadow-sm border border-gray-200"> <div className="bg-card p-8 rounded-lg shadow-sm border">
<div className="text-center mb-6"> <div className="text-center mb-6">
<h3 className="text-2xl font-bold text-gray-900 mb-2">Free</h3> <h3 className="text-2xl font-bold text-card-foreground mb-2">Free</h3>
<div className="text-4xl font-bold text-gray-900 mb-2">$0</div> <div className="text-4xl font-bold text-card-foreground mb-2">$0</div>
<p className="text-gray-600">Perfect for getting started</p> <p className="text-muted-foreground">Perfect for getting started</p>
</div> </div>
<ul className="space-y-3 mb-8"> <ul className="space-y-3 mb-8">
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" /> <Check className="h-5 w-5 text-green-500 mr-3" />
20 prompts <span className="text-card-foreground">20 prompts</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" /> <Check className="h-5 w-5 text-green-500 mr-3" />
3 versions per prompt <span className="text-card-foreground">3 versions per prompt</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-green-500 mr-3" /> <Check className="h-5 w-5 text-green-500 mr-3" />
$5 AI credits monthly <span className="text-card-foreground">$5 AI credits monthly</span>
</li> </li>
</ul> </ul>
<Button className="w-full" onClick={() => window.location.href = '/signup'}> <Button className="w-full" onClick={() => window.location.href = '/signup'}>
@ -157,34 +157,34 @@ export default function Home() {
</Button> </Button>
</div> </div>
<div className="bg-blue-600 p-8 rounded-lg shadow-sm text-white relative"> <div className="bg-primary p-8 rounded-lg shadow-sm text-primary-foreground relative">
<div className="absolute top-4 right-4 bg-white text-blue-600 px-3 py-1 rounded-full text-xs font-semibold"> <div className="absolute top-4 right-4 bg-primary-foreground text-primary px-3 py-1 rounded-full text-xs font-semibold">
Popular Popular
</div> </div>
<div className="text-center mb-6"> <div className="text-center mb-6">
<h3 className="text-2xl font-bold mb-2">Pro</h3> <h3 className="text-2xl font-bold mb-2">Pro</h3>
<div className="text-4xl font-bold mb-2">$19.9</div> <div className="text-4xl font-bold mb-2">$19.9</div>
<p className="text-blue-100">per month</p> <p className="text-primary-foreground/80">per month</p>
</div> </div>
<ul className="space-y-3 mb-8"> <ul className="space-y-3 mb-8">
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-blue-200 mr-3" /> <Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
500 prompts <span>500 prompts</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-blue-200 mr-3" /> <Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
10 versions per prompt <span>10 versions per prompt</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-blue-200 mr-3" /> <Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
$20 AI credits monthly <span>$20 AI credits monthly</span>
</li> </li>
<li className="flex items-center"> <li className="flex items-center">
<Check className="h-5 w-5 text-blue-200 mr-3" /> <Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
Priority support <span>Priority support</span>
</li> </li>
</ul> </ul>
<Button variant="outline" className="w-full bg-white text-blue-600 hover:bg-gray-50" onClick={() => window.location.href = '/signup'}> <Button variant="outline" className="w-full bg-primary-foreground text-primary hover:bg-primary-foreground/90" onClick={() => window.location.href = '/signup'}>
Start Pro Trial Start Pro Trial
</Button> </Button>
</div> </div>
@ -193,14 +193,14 @@ export default function Home() {
</section> </section>
{/* Footer */} {/* Footer */}
<footer className="bg-gray-900 text-white py-12"> <footer className="border-t py-12">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex flex-col md:flex-row justify-between items-center"> <div className="flex flex-col md:flex-row justify-between items-center">
<div className="flex items-center mb-4 md:mb-0"> <div className="flex items-center mb-4 md:mb-0">
<Zap className="h-8 w-8 text-blue-400" /> <Zap className="h-8 w-8 text-primary" />
<span className="ml-2 text-xl font-bold">Prmbr</span> <span className="ml-2 text-xl font-bold text-foreground">Prmbr</span>
</div> </div>
<div className="text-gray-400 text-sm"> <div className="text-muted-foreground text-sm">
© 2024 Prmbr. All rights reserved. © 2024 Prmbr. All rights reserved.
</div> </div>
</div> </div>

View File

@ -234,7 +234,7 @@ export default function ProfilePage() {
} }
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen">
<Header /> <Header />
<div className="max-w-4xl mx-auto px-4 py-8"> <div className="max-w-4xl mx-auto px-4 py-8">

View File

@ -3,6 +3,7 @@
import { useState } from 'react' import { useState } from 'react'
import { useAuth } from '@/hooks/useAuth' import { useAuth } from '@/hooks/useAuth'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ThemeToggle, MobileThemeToggle } from '@/components/ui/theme-toggle'
import { Menu, X, Zap } from 'lucide-react' import { Menu, X, Zap } from 'lucide-react'
export function Header() { export function Header() {
@ -10,26 +11,26 @@ export function Header() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return ( return (
<header className="bg-white border-b border-gray-200"> <header className="sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-16">
<div className="flex items-center"> <div className="flex items-center">
<div className="flex-shrink-0 flex items-center"> <div className="flex-shrink-0 flex items-center">
<Zap className="h-8 w-8 text-blue-600" /> <Zap className="h-8 w-8 text-primary" />
<span className="ml-2 text-xl font-bold text-gray-900">Prmbr</span> <span className="ml-2 text-xl font-bold text-foreground">Prmbr</span>
</div> </div>
</div> </div>
{/* Desktop Navigation */} {/* Desktop Navigation */}
<div className="hidden md:block"> <div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4"> <div className="ml-10 flex items-baseline space-x-4">
<a href="#features" className="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm font-medium"> <a href="#features" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
Features Features
</a> </a>
<a href="#pricing" className="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm font-medium"> <a href="#pricing" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
Pricing Pricing
</a> </a>
<a href="#showcase" className="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm font-medium"> <a href="#showcase" className="text-muted-foreground hover:text-foreground px-3 py-2 text-sm font-medium transition-colors">
Showcase Showcase
</a> </a>
</div> </div>
@ -37,32 +38,37 @@ export function Header() {
{/* Desktop Auth */} {/* Desktop Auth */}
<div className="hidden md:block"> <div className="hidden md:block">
{user ? ( <div className="flex items-center space-x-2">
<div className="flex items-center space-x-4"> <ThemeToggle variant="dropdown" showLabel={false} />
<Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}> {user ? (
Profile <div className="flex items-center space-x-2">
</Button> <Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}>
<Button variant="outline" onClick={signOut}> Profile
Sign Out </Button>
</Button> <Button variant="outline" onClick={signOut}>
</div> Sign Out
) : ( </Button>
<div className="flex items-center space-x-2"> </div>
<Button variant="ghost" onClick={() => window.location.href = '/signin'}> ) : (
Sign In <div className="flex items-center space-x-2">
</Button> <Button variant="ghost" onClick={() => window.location.href = '/signin'}>
<Button onClick={() => window.location.href = '/signup'}> Sign In
Sign Up </Button>
</Button> <Button onClick={() => window.location.href = '/signup'}>
</div> Sign Up
)} </Button>
</div>
)}
</div>
</div> </div>
{/* Mobile menu button */} {/* Mobile controls */}
<div className="md:hidden"> <div className="md:hidden flex items-center space-x-2">
<ThemeToggle variant="button" />
<button <button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)} onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="text-gray-600 hover:text-gray-900 p-2" className="text-muted-foreground hover:text-foreground p-2 transition-colors"
aria-label="Toggle menu"
> >
{mobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />} {mobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button> </button>
@ -72,20 +78,26 @@ export function Header() {
{/* Mobile menu */} {/* Mobile menu */}
{mobileMenuOpen && ( {mobileMenuOpen && (
<div className="md:hidden"> <div className="md:hidden">
<div className="px-2 pt-2 pb-3 space-y-1 border-t border-gray-200"> <div className="px-2 pt-2 pb-3 space-y-1 border-t">
<a href="#features" className="block text-gray-600 hover:text-gray-900 px-3 py-2 text-base font-medium"> <a href="#features" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
Features Features
</a> </a>
<a href="#pricing" className="block text-gray-600 hover:text-gray-900 px-3 py-2 text-base font-medium"> <a href="#pricing" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
Pricing Pricing
</a> </a>
<a href="#showcase" className="block text-gray-600 hover:text-gray-900 px-3 py-2 text-base font-medium"> <a href="#showcase" className="block text-muted-foreground hover:text-foreground px-3 py-2 text-base font-medium transition-colors rounded-md hover:bg-accent">
Showcase Showcase
</a> </a>
<div className="pt-4 pb-2">
{/* Mobile Theme Toggle */}
<div className="pt-4 pb-2 border-t">
<MobileThemeToggle />
</div>
<div className="pt-2 pb-2 border-t">
{user ? ( {user ? (
<div className="space-y-2"> <div className="space-y-2">
<Button variant="ghost" className="w-full" onClick={() => window.location.href = '/profile'}> <Button variant="ghost" className="w-full justify-start" onClick={() => window.location.href = '/profile'}>
Profile Profile
</Button> </Button>
<Button variant="outline" className="w-full" onClick={signOut}> <Button variant="outline" className="w-full" onClick={signOut}>

View File

@ -0,0 +1,214 @@
'use client'
import { useState, useEffect } from 'react'
import { useTheme } from '@/contexts/ThemeContext'
import { Button } from '@/components/ui/button'
import { Sun, Moon, Monitor, ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
interface ThemeToggleProps {
className?: string
showLabel?: boolean
variant?: 'button' | 'dropdown'
}
const themeOptions = [
{
value: 'light' as const,
label: { en: 'Light', zh: '浅色' },
icon: Sun,
},
{
value: 'dark' as const,
label: { en: 'Dark', zh: '深色' },
icon: Moon,
},
{
value: 'system' as const,
label: { en: 'System', zh: '跟随系统' },
icon: Monitor,
},
]
export function ThemeToggle({
className,
showLabel = false,
variant = 'button'
}: ThemeToggleProps) {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const [currentLanguage, setCurrentLanguage] = useState<'en' | 'zh'>('en')
// Prevent hydration mismatch
useEffect(() => {
setMounted(true)
// Get language from user metadata or browser
const getUserLanguage = async () => {
// Try to get from user profile first (if implemented)
const browserLanguage = navigator.language.startsWith('zh') ? 'zh' : 'en'
setCurrentLanguage(browserLanguage)
}
getUserLanguage()
}, [])
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Element
if (!target.closest('[data-theme-toggle]')) {
setIsOpen(false)
}
}
if (isOpen) {
document.addEventListener('click', handleClickOutside)
return () => document.removeEventListener('click', handleClickOutside)
}
}, [isOpen])
if (!mounted) {
return (
<Button
variant="ghost"
size="sm"
className={cn("w-9 h-9 p-0", className)}
disabled
>
<Monitor className="h-4 w-4" />
<span className="sr-only">Loading theme...</span>
</Button>
)
}
const currentThemeOption = themeOptions.find(option => option.value === theme)
const CurrentIcon = currentThemeOption?.icon || Monitor
if (variant === 'button') {
const nextTheme = theme === 'light' ? 'dark' : theme === 'dark' ? 'system' : 'light'
return (
<Button
variant="ghost"
size="sm"
className={cn("w-9 h-9 p-0", className)}
onClick={() => setTheme(nextTheme)}
title={`Current: ${currentThemeOption?.label[currentLanguage] || 'System'}`}
>
<CurrentIcon className="h-4 w-4 transition-transform duration-200" />
<span className="sr-only">
Toggle theme (current: {currentThemeOption?.label[currentLanguage] || 'System'})
</span>
</Button>
)
}
return (
<div className="relative" data-theme-toggle>
<Button
variant="ghost"
size="sm"
className={cn(
"h-9 gap-2 px-3",
showLabel ? "w-auto" : "w-9 px-0",
className
)}
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-haspopup="menu"
>
<CurrentIcon className="h-4 w-4" />
{showLabel && (
<>
<span className="text-sm">
{currentThemeOption?.label[currentLanguage] || 'System'}
</span>
<ChevronDown className={cn(
"h-3 w-3 transition-transform",
isOpen && "rotate-180"
)} />
</>
)}
<span className="sr-only">Theme options</span>
</Button>
{isOpen && (
<div className="absolute right-0 top-full mt-1 w-36 rounded-md border border-border bg-popover p-1 shadow-md z-50">
{themeOptions.map((option) => {
const Icon = option.icon
const isSelected = theme === option.value
return (
<button
key={option.value}
className={cn(
"w-full flex items-center gap-2 px-2 py-1.5 text-sm rounded-sm transition-colors",
"hover:bg-accent hover:text-accent-foreground",
"focus:bg-accent focus:text-accent-foreground focus:outline-none",
isSelected && "bg-accent text-accent-foreground"
)}
onClick={() => {
setTheme(option.value)
setIsOpen(false)
}}
role="menuitem"
>
<Icon className="h-4 w-4" />
<span>{option.label[currentLanguage]}</span>
{isSelected && (
<div className="ml-auto w-1 h-1 rounded-full bg-current" />
)}
</button>
)
})}
</div>
)}
</div>
)
}
// Simplified version for mobile menu
export function MobileThemeToggle({ className }: { className?: string }) {
const { theme, setTheme } = useTheme()
const [mounted, setMounted] = useState(false)
const [currentLanguage, setCurrentLanguage] = useState<'en' | 'zh'>('en')
useEffect(() => {
setMounted(true)
const browserLanguage = navigator.language.startsWith('zh') ? 'zh' : 'en'
setCurrentLanguage(browserLanguage)
}, [])
if (!mounted) {
return null
}
return (
<div className={cn("space-y-1", className)}>
<div className="text-sm font-medium text-muted-foreground px-3 py-2">
{currentLanguage === 'zh' ? '主题' : 'Theme'}
</div>
{themeOptions.map((option) => {
const Icon = option.icon
const isSelected = theme === option.value
return (
<button
key={option.value}
className={cn(
"w-full flex items-center gap-3 px-3 py-2 text-sm rounded-md transition-colors",
"hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-accent text-accent-foreground"
)}
onClick={() => setTheme(option.value)}
>
<Icon className="h-4 w-4" />
<span>{option.label[currentLanguage]}</span>
</button>
)
})}
</div>
)
}

View File

@ -0,0 +1,113 @@
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
type Theme = 'light' | 'dark' | 'system'
interface ThemeContextType {
theme: Theme
setTheme: (theme: Theme) => void
resolvedTheme: 'light' | 'dark'
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
export function useTheme() {
const context = useContext(ThemeContext)
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider')
}
return context
}
interface ThemeProviderProps {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'prmbr-theme',
}: ThemeProviderProps) {
const [theme, setThemeState] = useState<Theme>(defaultTheme)
const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light')
// Initialize theme from localStorage or default
useEffect(() => {
try {
const stored = localStorage.getItem(storageKey) as Theme
if (stored && ['light', 'dark', 'system'].includes(stored)) {
setThemeState(stored)
}
} catch (error) {
console.warn('Failed to load theme from localStorage:', error)
}
}, [storageKey])
// Update resolved theme based on current theme and system preference
useEffect(() => {
const updateResolvedTheme = () => {
if (theme === 'system') {
const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
setResolvedTheme(systemTheme)
} else {
setResolvedTheme(theme)
}
}
updateResolvedTheme()
// Listen for system theme changes
if (theme === 'system') {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleChange = () => updateResolvedTheme()
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
}
}, [theme])
// Apply theme to document
useEffect(() => {
const root = document.documentElement
// Remove existing theme classes
root.classList.remove('light', 'dark')
// Add resolved theme class
root.classList.add(resolvedTheme)
// Set color-scheme for native browser elements
root.style.colorScheme = resolvedTheme
// Update meta theme-color for mobile browsers
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
if (metaThemeColor) {
metaThemeColor.setAttribute('content', resolvedTheme === 'dark' ? '#020617' : '#ffffff')
}
}, [resolvedTheme])
const setTheme = (newTheme: Theme) => {
try {
localStorage.setItem(storageKey, newTheme)
setThemeState(newTheme)
} catch (error) {
console.warn('Failed to save theme to localStorage:', error)
setThemeState(newTheme)
}
}
const value = {
theme,
setTheme,
resolvedTheme,
}
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
)
}