Add dark modle
This commit is contained in:
parent
a58b9222c3
commit
6a6f1e0454
@ -1,65 +1,146 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Light theme variables */
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #0f172a;
|
||||
--muted: #f8fafc;
|
||||
--muted-foreground: #64748b;
|
||||
--border: #e2e8f0;
|
||||
--input: #ffffff;
|
||||
--primary: #0f172a;
|
||||
--primary-foreground: #f8fafc;
|
||||
--secondary: #f1f5f9;
|
||||
--secondary-foreground: #0f172a;
|
||||
--accent: #f1f5f9;
|
||||
--accent-foreground: #0f172a;
|
||||
--destructive: #ef4444;
|
||||
--destructive-foreground: #fef2f2;
|
||||
--ring: #0f172a;
|
||||
--background: 255 255 255;
|
||||
--foreground: 15 23 42;
|
||||
--card: 255 255 255;
|
||||
--card-foreground: 15 23 42;
|
||||
--popover: 255 255 255;
|
||||
--popover-foreground: 15 23 42;
|
||||
--primary: 15 23 42;
|
||||
--primary-foreground: 248 250 252;
|
||||
--secondary: 241 245 249;
|
||||
--secondary-foreground: 15 23 42;
|
||||
--muted: 248 250 252;
|
||||
--muted-foreground: 100 116 139;
|
||||
--accent: 241 245 249;
|
||||
--accent-foreground: 15 23 42;
|
||||
--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 {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-ring: var(--ring);
|
||||
--color-background: rgb(var(--background));
|
||||
--color-foreground: rgb(var(--foreground));
|
||||
--color-card: rgb(var(--card));
|
||||
--color-card-foreground: rgb(var(--card-foreground));
|
||||
--color-popover: rgb(var(--popover));
|
||||
--color-popover-foreground: rgb(var(--popover-foreground));
|
||||
--color-primary: rgb(var(--primary));
|
||||
--color-primary-foreground: rgb(var(--primary-foreground));
|
||||
--color-secondary: rgb(var(--secondary));
|
||||
--color-secondary-foreground: rgb(var(--secondary-foreground));
|
||||
--color-muted: rgb(var(--muted));
|
||||
--color-muted-foreground: rgb(var(--muted-foreground));
|
||||
--color-accent: rgb(var(--accent));
|
||||
--color-accent-foreground: rgb(var(--accent-foreground));
|
||||
--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-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #020617;
|
||||
--foreground: #f8fafc;
|
||||
--muted: #0f172a;
|
||||
--muted-foreground: #94a3b8;
|
||||
--border: #1e293b;
|
||||
--input: #0f172a;
|
||||
--primary: #f8fafc;
|
||||
--primary-foreground: #0f172a;
|
||||
--secondary: #1e293b;
|
||||
--secondary-foreground: #f8fafc;
|
||||
--accent: #1e293b;
|
||||
--accent-foreground: #f8fafc;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #fef2f2;
|
||||
--ring: #f8fafc;
|
||||
}
|
||||
/* Base styles */
|
||||
* {
|
||||
border-color: rgb(var(--border));
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
|
||||
background-color: rgb(var(--background));
|
||||
color: rgb(var(--foreground));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,57 @@
|
||||
import type { Metadata } from "next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { ThemeProvider } from "@/contexts/ThemeContext";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
preload: true,
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
preload: true,
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Prmbr - AI Prompt Studio",
|
||||
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({
|
||||
@ -23,11 +60,32 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
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
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen bg-background text-foreground`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen`}
|
||||
>
|
||||
<ThemeProvider defaultTheme="system" storageKey="prmbr-theme">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
104
src/app/page.tsx
104
src/app/page.tsx
@ -18,13 +18,13 @@ export default function Home() {
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
<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!
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-8">
|
||||
<p className="text-muted-foreground mb-8">
|
||||
Start building, testing, and managing your AI prompts.
|
||||
</p>
|
||||
<Button size="lg">
|
||||
@ -37,17 +37,17 @@ export default function Home() {
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white">
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
{/* 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="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
|
||||
</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.
|
||||
Version control, collaboration tools, and analytics in one professional platform.
|
||||
</p>
|
||||
@ -64,54 +64,54 @@ export default function Home() {
|
||||
</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="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
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Professional tools for prompt engineering and management
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Target className="h-12 w-12 text-blue-600 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||
<Target className="h-12 w-12 text-primary mb-4" />
|
||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||
Prompt Builder
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
Intuitive interface for crafting and refining AI prompts with real-time preview.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<Layers className="h-12 w-12 text-blue-600 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||
<Layers className="h-12 w-12 text-primary mb-4" />
|
||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||
Version Control
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
Track changes, compare versions, and rollback to previous iterations seamlessly.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<BarChart3 className="h-12 w-12 text-blue-600 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||
<BarChart3 className="h-12 w-12 text-primary mb-4" />
|
||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||
Test & Analytics
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
Run tests, analyze performance, and optimize your prompts with detailed metrics.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
|
||||
<Zap className="h-12 w-12 text-blue-600 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
||||
<div className="bg-card p-6 rounded-lg shadow-sm border">
|
||||
<Zap className="h-12 w-12 text-primary mb-4" />
|
||||
<h3 className="text-lg font-semibold text-card-foreground mb-2">
|
||||
Team Collaboration
|
||||
</h3>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
Share prompts, collaborate with team members, and maintain quality standards.
|
||||
</p>
|
||||
</div>
|
||||
@ -120,36 +120,36 @@ export default function Home() {
|
||||
</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="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
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600">
|
||||
<p className="text-xl text-muted-foreground">
|
||||
Choose the plan that fits your needs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-2">Free</h3>
|
||||
<div className="text-4xl font-bold text-gray-900 mb-2">$0</div>
|
||||
<p className="text-gray-600">Perfect for getting started</p>
|
||||
<h3 className="text-2xl font-bold text-card-foreground mb-2">Free</h3>
|
||||
<div className="text-4xl font-bold text-card-foreground mb-2">$0</div>
|
||||
<p className="text-muted-foreground">Perfect for getting started</p>
|
||||
</div>
|
||||
<ul className="space-y-3 mb-8">
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-green-500 mr-3" />
|
||||
20 prompts
|
||||
<span className="text-card-foreground">20 prompts</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<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 className="flex items-center">
|
||||
<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>
|
||||
</ul>
|
||||
<Button className="w-full" onClick={() => window.location.href = '/signup'}>
|
||||
@ -157,34 +157,34 @@ export default function Home() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-600 p-8 rounded-lg shadow-sm text-white 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="bg-primary p-8 rounded-lg shadow-sm text-primary-foreground relative">
|
||||
<div className="absolute top-4 right-4 bg-primary-foreground text-primary px-3 py-1 rounded-full text-xs font-semibold">
|
||||
Popular
|
||||
</div>
|
||||
<div className="text-center mb-6">
|
||||
<h3 className="text-2xl font-bold mb-2">Pro</h3>
|
||||
<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>
|
||||
<ul className="space-y-3 mb-8">
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-blue-200 mr-3" />
|
||||
500 prompts
|
||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||
<span>500 prompts</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-blue-200 mr-3" />
|
||||
10 versions per prompt
|
||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||
<span>10 versions per prompt</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-blue-200 mr-3" />
|
||||
$20 AI credits monthly
|
||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||
<span>$20 AI credits monthly</span>
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<Check className="h-5 w-5 text-blue-200 mr-3" />
|
||||
Priority support
|
||||
<Check className="h-5 w-5 text-primary-foreground/80 mr-3" />
|
||||
<span>Priority support</span>
|
||||
</li>
|
||||
</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
|
||||
</Button>
|
||||
</div>
|
||||
@ -193,14 +193,14 @@ export default function Home() {
|
||||
</section>
|
||||
|
||||
{/* 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="flex flex-col md:flex-row justify-between items-center">
|
||||
<div className="flex items-center mb-4 md:mb-0">
|
||||
<Zap className="h-8 w-8 text-blue-400" />
|
||||
<span className="ml-2 text-xl font-bold">Prmbr</span>
|
||||
<Zap className="h-8 w-8 text-primary" />
|
||||
<span className="ml-2 text-xl font-bold text-foreground">Prmbr</span>
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
© 2024 Prmbr. All rights reserved.
|
||||
</div>
|
||||
</div>
|
||||
|
@ -234,7 +234,7 @@ export default function ProfilePage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
<div className="max-w-4xl mx-auto px-4 py-8">
|
||||
|
@ -3,6 +3,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ThemeToggle, MobileThemeToggle } from '@/components/ui/theme-toggle'
|
||||
import { Menu, X, Zap } from 'lucide-react'
|
||||
|
||||
export function Header() {
|
||||
@ -10,26 +11,26 @@ export function Header() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
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="flex justify-between items-center h-16">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<Zap className="h-8 w-8 text-blue-600" />
|
||||
<span className="ml-2 text-xl font-bold text-gray-900">Prmbr</span>
|
||||
<Zap className="h-8 w-8 text-primary" />
|
||||
<span className="ml-2 text-xl font-bold text-foreground">Prmbr</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:block">
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</a>
|
||||
</div>
|
||||
@ -37,8 +38,10 @@ export function Header() {
|
||||
|
||||
{/* Desktop Auth */}
|
||||
<div className="hidden md:block">
|
||||
<div className="flex items-center space-x-2">
|
||||
<ThemeToggle variant="dropdown" showLabel={false} />
|
||||
{user ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => window.location.href = '/profile'}>
|
||||
Profile
|
||||
</Button>
|
||||
@ -57,12 +60,15 @@ export function Header() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden">
|
||||
{/* Mobile controls */}
|
||||
<div className="md:hidden flex items-center space-x-2">
|
||||
<ThemeToggle variant="button" />
|
||||
<button
|
||||
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" />}
|
||||
</button>
|
||||
@ -72,20 +78,26 @@ export function Header() {
|
||||
{/* Mobile menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="md:hidden">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 border-t border-gray-200">
|
||||
<a href="#features" className="block text-gray-600 hover:text-gray-900 px-3 py-2 text-base font-medium">
|
||||
<div className="px-2 pt-2 pb-3 space-y-1 border-t">
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</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 ? (
|
||||
<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
|
||||
</Button>
|
||||
<Button variant="outline" className="w-full" onClick={signOut}>
|
||||
|
214
src/components/ui/theme-toggle.tsx
Normal file
214
src/components/ui/theme-toggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
113
src/contexts/ThemeContext.tsx
Normal file
113
src/contexts/ThemeContext.tsx
Normal 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>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user