Compare commits
4 Commits
d50555cbc4
...
dark-theme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e53748d7a9 | ||
|
|
8aba6e31c8 | ||
|
|
be22888afb | ||
|
|
6d8056d483 |
@@ -119,4 +119,7 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
.pricing-page {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google";
|
|||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
import { Header } from "@/components/ui/header";
|
import { Header } from "@/components/ui/header";
|
||||||
|
import { ThemeProvider } from "@/lib/theme-context";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -29,10 +30,10 @@ export default function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
<ThemeProvider>
|
||||||
<Header/>
|
<Header/>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
123
app/pricing/page.tsx
Normal file
123
app/pricing/page.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import PricingCard from '@/components/pricing-card';
|
||||||
|
import { useTheme } from '@/lib/theme-context';
|
||||||
|
import { Sun, Moon } from 'lucide-react';
|
||||||
|
|
||||||
|
const PricingPage = () => {
|
||||||
|
const [isYearly, setIsYearly] = useState(false);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.classList.add('pricing-page');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleBillingPeriod = () => {
|
||||||
|
setIsYearly(!isYearly);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPrice = (basePrice: number) => {
|
||||||
|
return isYearly ? Math.round(basePrice * 10 * 0.8) : basePrice;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBillingPeriod = () => {
|
||||||
|
return isYearly ? 'year' : 'month';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`min-h-screen ${theme === 'dark' ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}>
|
||||||
|
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-4xl font-extrabold sm:text-5xl">
|
||||||
|
Find a plan to power your apps.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 text-xl">
|
||||||
|
Dishpix supports teams of all sizes, with pricing that scales.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pricing Toggle (Monthly/Yearly) */}
|
||||||
|
<div className="mt-12 flex justify-center">
|
||||||
|
<div className="relative inline-flex items-center bg-gray-200 dark:bg-gray-700 rounded-full p-1">
|
||||||
|
<button
|
||||||
|
onClick={toggleBillingPeriod}
|
||||||
|
className={`relative px-4 py-2 text-sm font-medium rounded-full focus:outline-none transition-colors ${
|
||||||
|
!isYearly
|
||||||
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Monthly
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={toggleBillingPeriod}
|
||||||
|
className={`relative px-4 py-2 text-sm font-medium rounded-full focus:outline-none transition-colors ${
|
||||||
|
isYearly
|
||||||
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
|
: 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Yearly
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pricing Cards Grid */}
|
||||||
|
<div className="mt-12 sm:mt-16 lg:mt-20 space-y-8 sm:space-y-12 md:space-y-0 md:grid md:grid-cols-1 lg:grid-cols-3 md:gap-6 lg:gap-8">
|
||||||
|
<PricingCard
|
||||||
|
title="Start"
|
||||||
|
price={`$${getPrice(9)}`}
|
||||||
|
billingPeriod={getBillingPeriod()}
|
||||||
|
features={[
|
||||||
|
'Import your repo, deploy in seconds',
|
||||||
|
'Automatic CI/CD',
|
||||||
|
'Web Application Firewall',
|
||||||
|
'Global, automated CDN',
|
||||||
|
'Fluid compute',
|
||||||
|
'DDoS Mitigation',
|
||||||
|
'Traffic & performance insights',
|
||||||
|
]}
|
||||||
|
ctaText="Start Deploying"
|
||||||
|
ctaHref="/signup?plan=start"
|
||||||
|
/>
|
||||||
|
<PricingCard
|
||||||
|
title="Pro"
|
||||||
|
price={`$${getPrice(29)}`}
|
||||||
|
billingPeriod={getBillingPeriod()}
|
||||||
|
features={[
|
||||||
|
'Everything in Start, plus:',
|
||||||
|
'10x more included usage',
|
||||||
|
'Observability tools',
|
||||||
|
'Faster builds',
|
||||||
|
'Cold start prevention',
|
||||||
|
'Advanced WAF Protection',
|
||||||
|
'Email support',
|
||||||
|
]}
|
||||||
|
ctaText="Start a free trial"
|
||||||
|
ctaHref="/signup?plan=pro"
|
||||||
|
isPopular={true}
|
||||||
|
/>
|
||||||
|
<PricingCard
|
||||||
|
title="Premium"
|
||||||
|
price={`$${getPrice(99)}`}
|
||||||
|
billingPeriod={getBillingPeriod()}
|
||||||
|
features={[
|
||||||
|
'Everything in Pro, plus:',
|
||||||
|
'Guest & Team access controls',
|
||||||
|
'SCIM & Directory Sync',
|
||||||
|
'Managed WAF Rulesets',
|
||||||
|
'Multi-region compute & failover',
|
||||||
|
'99.99% SLA',
|
||||||
|
'Advanced Support',
|
||||||
|
]}
|
||||||
|
ctaText="Contact Sales"
|
||||||
|
ctaHref="/contact-sales"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PricingPage;
|
||||||
100
components/pricing-card.tsx
Normal file
100
components/pricing-card.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTheme } from '@/lib/theme-context';
|
||||||
|
|
||||||
|
interface PricingCardProps {
|
||||||
|
title: string;
|
||||||
|
price: string;
|
||||||
|
billingPeriod: string;
|
||||||
|
features: string[];
|
||||||
|
ctaText: string;
|
||||||
|
ctaHref: string;
|
||||||
|
isPopular?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PricingCard: React.FC<PricingCardProps> = ({
|
||||||
|
title,
|
||||||
|
price,
|
||||||
|
billingPeriod,
|
||||||
|
features,
|
||||||
|
ctaText,
|
||||||
|
ctaHref,
|
||||||
|
isPopular = false,
|
||||||
|
}) => {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
|
||||||
|
// Add ARIA label for screen readers
|
||||||
|
const cardAriaLabel = `${title} pricing plan - $${price}/${billingPeriod}`;
|
||||||
|
|
||||||
|
const cardBg = theme === 'dark' ? 'bg-gray-800' : 'bg-white';
|
||||||
|
const cardText = theme === 'dark' ? 'text-gray-100' : 'text-gray-900';
|
||||||
|
const cardBorder = theme === 'dark' ? 'border-gray-700' : 'border-gray-200';
|
||||||
|
const cardShadow = theme === 'dark' ? 'shadow-lg' : 'shadow-sm';
|
||||||
|
const cardHover = theme === 'dark' ? 'hover:bg-gray-700' : 'hover:bg-indigo-600';
|
||||||
|
const cardFocus = theme === 'dark' ? 'focus:ring-indigo-500' : 'focus:ring-indigo-500';
|
||||||
|
const cardPopular = theme === 'dark' ? 'bg-indigo-700' : 'bg-indigo-600';
|
||||||
|
const cardFeatureText = theme === 'dark' ? 'text-gray-300' : 'text-gray-700';
|
||||||
|
const cardFeatureIcon = theme === 'dark' ? 'text-green-400' : 'text-green-500';
|
||||||
|
const cardHoverShadow = theme === 'dark' ? 'hover:shadow-xl' : 'hover:shadow-2xl';
|
||||||
|
const cardTransition = 'transition-all duration-300';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`flex flex-col ${cardBg} border ${cardBorder} rounded-xl ${cardShadow} overflow-hidden ${cardTransition} ${cardHoverShadow}`}
|
||||||
|
role="article"
|
||||||
|
aria-label={cardAriaLabel}
|
||||||
|
tabIndex={0} // Make the card focusable
|
||||||
|
>
|
||||||
|
{isPopular && (
|
||||||
|
<div
|
||||||
|
className={`${cardPopular} text-white text-xs font-semibold px-3 py-1.5 rounded-t-xl`}
|
||||||
|
>
|
||||||
|
Popular
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className={`text-2xl font-bold ${cardText}`}>{title}</h3>
|
||||||
|
<p className="mt-2 text-sm text-gray-400">
|
||||||
|
{title === 'Start' && 'The perfect starting place for your web app or personal project.'}
|
||||||
|
{title === 'Pro' && 'Everything you need to build and scale your app.'}
|
||||||
|
{title === 'Premium' && 'Critical security, performance, observability, platform SLAs, and support.'}
|
||||||
|
</p>
|
||||||
|
<p className="mt-6">
|
||||||
|
<span className="text-4xl font-extrabold">{price}</span>
|
||||||
|
<span className="text-base font-medium text-gray-400 ml-1">/{billingPeriod}</span>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={ctaHref}
|
||||||
|
className={`mt-6 block w-full py-3 px-4 border border-transparent rounded-md shadow text-center text-white bg-indigo-600 ${cardHover} focus:outline-none focus:ring-2 focus:ring-offset-2 ${cardFocus} ${cardTransition}`}
|
||||||
|
aria-label={`${ctaText} for ${title} plan`}
|
||||||
|
>
|
||||||
|
{ctaText}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className={`border-t ${cardBorder} p-6`}>
|
||||||
|
<h4 className={`text-sm font-medium ${cardText}`}>What's included:</h4>
|
||||||
|
<ul className="mt-4 space-y-3">
|
||||||
|
{features.map((feature, index) => (
|
||||||
|
<li key={index} className="flex items-start">
|
||||||
|
<svg
|
||||||
|
className={`h-5 w-5 ${cardFeatureIcon} flex-shrink-0`}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className={`ml-3 text-sm ${cardFeatureText}`}>{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(PricingCard);
|
||||||
@@ -5,7 +5,7 @@ import { cn } from "@/lib/utils"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useState, useEffect } from "react"
|
import { useState, useEffect } from "react"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
|
import dynamic from 'next/dynamic'
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
className?: string
|
className?: string
|
||||||
@@ -13,16 +13,33 @@ interface HeaderProps {
|
|||||||
isAdmin?: boolean
|
isAdmin?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const navigation = [
|
const leftNavigation = [
|
||||||
{ name: 'Home', href: '/', current: false, requiresAuth: false },
|
{ name: 'Home', href: '/', current: false, requiresAuth: false },
|
||||||
{ name: 'Pricing', href: '/pricing', current: false, requiresAuth: false },
|
{ name: 'Pricing', href: '/pricing', current: false, requiresAuth: false },
|
||||||
{ name: 'FAQ', href: '/faq', current: false, requiresAuth: false },
|
{ name: 'FAQ', href: '/faq', current: false, requiresAuth: false },
|
||||||
{ name: 'Dashboard', href: '/dashboard', current: false, requiresAuth: false },
|
{ name: 'Dashboard', href: '/dashboard', current: false, requiresAuth: true },
|
||||||
{ name: 'Projects', href: '/projects', current: false, requiresAuth: true },
|
{ name: 'Projects', href: '/projects', current: false, requiresAuth: true },
|
||||||
{ name: 'Calendar', href: '/calendar', current: false, requiresAuth: false },
|
{ name: 'Calendar', href: '/calendar', current: false, requiresAuth: true },
|
||||||
{ name: 'Reports', href: '/reports', current: false, requiresAuth: true },
|
{ name: 'Reports', href: '/reports', current: false, requiresAuth: true },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const rightNavigation = [
|
||||||
|
{ name: 'Sign-up', href: '/sign-up', current: false, requiresAuth: false },
|
||||||
|
{ name: 'Login', href: '/login', current: false, requiresAuth: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const ThemeToggle = dynamic(() => import('./theme-toggle').then(mod => mod.default), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ThemeToggleWrapper = () => {
|
||||||
|
return (
|
||||||
|
<div className="rounded-md px-3 py-2 text-sm font-medium transition-colors text-gray-300 hover:bg-gray-700 hover:text-white">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export function Header({ className, isLoggedIn = false, isAdmin = false }: HeaderProps) {
|
export function Header({ className, isLoggedIn = false, isAdmin = false }: HeaderProps) {
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
const [profileOpen, setProfileOpen] = useState(false)
|
const [profileOpen, setProfileOpen] = useState(false)
|
||||||
@@ -59,7 +76,7 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade
|
|||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
{navigation.map((item) => {
|
{leftNavigation.map((item) => {
|
||||||
const isCurrent = activeNavItem === item.href
|
const isCurrent = activeNavItem === item.href
|
||||||
return (
|
return (
|
||||||
(item.requiresAuth ? (isLoggedIn || isAdmin) : true) && (
|
(item.requiresAuth ? (isLoggedIn || isAdmin) : true) && (
|
||||||
@@ -83,12 +100,32 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoggedIn && (
|
|
||||||
<div className="hidden md:block">
|
<div className="hidden md:block">
|
||||||
<div className="ml-4 flex items-center md:ml-6">
|
<div className="ml-4 flex items-center md:ml-6">
|
||||||
|
{!isLoggedIn && (
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<div className="ml-10 flex items-baseline space-x-4">
|
||||||
|
<div>
|
||||||
|
{rightNavigation.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
href={item.href}
|
||||||
|
className='rounded-md px-3 py-2 text-sm font-medium transition-colors text-gray-300 hover:bg-gray-700 hover:text-white'
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<ThemeToggleWrapper />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoggedIn && (
|
||||||
|
<div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="relative rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
|
className="mx-4 relative rounded-full bg-gray-800 p-1 text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
|
||||||
>
|
>
|
||||||
<span className="absolute -inset-1.5" />
|
<span className="absolute -inset-1.5" />
|
||||||
<span className="sr-only">View notifications</span>
|
<span className="sr-only">View notifications</span>
|
||||||
@@ -130,8 +167,9 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="-mr-2 flex md:hidden">
|
<div className="-mr-2 flex md:hidden">
|
||||||
{/* Mobile menu button */}
|
{/* Mobile menu button */}
|
||||||
@@ -160,7 +198,7 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade
|
|||||||
{mobileMenuOpen && (
|
{mobileMenuOpen && (
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<div className="space-y-1 px-2 pb-3 pt-2 sm:px-3">
|
<div className="space-y-1 px-2 pb-3 pt-2 sm:px-3">
|
||||||
{navigation.map((item) => {
|
{leftNavigation.map((item) => {
|
||||||
const isCurrent = activeNavItem === item.href
|
const isCurrent = activeNavItem === item.href
|
||||||
return (
|
return (
|
||||||
(item.requiresAuth ? (isLoggedIn || isAdmin) : true) && (
|
(item.requiresAuth ? (isLoggedIn || isAdmin) : true) && (
|
||||||
@@ -181,8 +219,8 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-700 pb-3 pt-4">
|
|
||||||
{isLoggedIn && (
|
{isLoggedIn && (
|
||||||
|
<div className="border-t border-gray-700 pb-3 pt-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center px-5">
|
<div className="flex items-center px-5">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
@@ -219,8 +257,9 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
23
components/ui/theme-toggle.tsx
Normal file
23
components/ui/theme-toggle.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTheme } from '@/lib/theme-context';
|
||||||
|
import { Sun, Moon } from 'lucide-react';
|
||||||
|
|
||||||
|
const ThemeToggle: React.FC = () => {
|
||||||
|
const { theme, toggleTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={toggleTheme}
|
||||||
|
className="text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2"
|
||||||
|
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
||||||
|
>
|
||||||
|
{theme === 'dark' ? (
|
||||||
|
<Sun className="h-3 w-3" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-3 w-3" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeToggle;
|
||||||
49
lib/theme-context.tsx
Normal file
49
lib/theme-context.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { createContext, useState, useEffect, ReactNode, useContext } from 'react';
|
||||||
|
|
||||||
|
type Theme = 'light' | 'dark';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [theme, setTheme] = useState<Theme>('dark'); // Default to dark mode
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedTheme = localStorage.getItem('theme') as Theme | null;
|
||||||
|
if (savedTheme) {
|
||||||
|
setTheme(savedTheme);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
localStorage.setItem('theme', theme);
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme((prevTheme) => (prevTheme === 'dark' ? 'light' : 'dark'));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useTheme = (): ThemeContextType => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useTheme must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -8,7 +8,12 @@ module.exports = {
|
|||||||
'./components/**/*.{js,ts,jsx,tsx}',
|
'./components/**/*.{js,ts,jsx,tsx}',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
transitionProperty: {
|
||||||
|
'width': 'width',
|
||||||
|
'spacing': 'margin, padding',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
createThemes({
|
createThemes({
|
||||||
@@ -21,5 +26,9 @@ module.exports = {
|
|||||||
foreground: '#ededed',
|
foreground: '#ededed',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
require('tailwindcss/plugin')({
|
||||||
|
darkMode: 'class',
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
|
darkMode: 'class',
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user