From 8aba6e31c8db7667b97f63c9826e822e45ca946d Mon Sep 17 00:00:00 2001 From: kbe Date: Thu, 14 Aug 2025 18:14:59 +0200 Subject: [PATCH] Prepare dark theme, add pricing page --- app/globals.css | 3 + app/layout.tsx | 9 ++- app/pricing/page.tsx | 134 +++++++++++++++++++++++++++++++++ components/pricing-card.tsx | 87 +++++++++++++++++++++ components/ui/header.tsx | 14 +++- components/ui/theme-toggle.tsx | 23 ++++++ lib/theme-context.tsx | 49 ++++++++++++ tailwind.config.js | 11 ++- 8 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 app/pricing/page.tsx create mode 100644 components/pricing-card.tsx create mode 100644 components/ui/theme-toggle.tsx create mode 100644 lib/theme-context.tsx diff --git a/app/globals.css b/app/globals.css index dc98be7..838d5e9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -119,4 +119,7 @@ body { @apply bg-background text-foreground; } + .pricing-page { + @apply bg-background text-foreground; + } } diff --git a/app/layout.tsx b/app/layout.tsx index 576fea5..0dba9b7 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { Header } from "@/components/ui/header"; +import { ThemeProvider } from "@/lib/theme-context"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -29,10 +30,10 @@ export default function RootLayout({ -
- - {children} - + +
+ {children} + ); diff --git a/app/pricing/page.tsx b/app/pricing/page.tsx new file mode 100644 index 0000000..6e21f9f --- /dev/null +++ b/app/pricing/page.tsx @@ -0,0 +1,134 @@ +"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 ? basePrice * 10 : basePrice; + }; + + const getBillingPeriod = () => { + return isYearly ? 'year' : 'month'; + }; + + return ( +
+
+
+

+ Find a plan to power your apps. +

+

+ Dishpix supports teams of all sizes, with pricing that scales. +

+
+ + {/* Pricing Toggle (Monthly/Yearly) */} +
+
+ + + + + +
+
+ + {/* Pricing Cards Grid */} +
+ + + +
+
+
+ ); +}; + +export default PricingPage; \ No newline at end of file diff --git a/components/pricing-card.tsx b/components/pricing-card.tsx new file mode 100644 index 0000000..0b9b821 --- /dev/null +++ b/components/pricing-card.tsx @@ -0,0 +1,87 @@ +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 = ({ + title, + price, + billingPeriod, + features, + ctaText, + ctaHref, + isPopular = false, +}) => { + const { theme } = useTheme(); + + 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-700'; + 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'; + + return ( +
+ {isPopular && ( +
+ Popular +
+ )} +
+

{title}

+

+ {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.'} +

+

+ {price} + /{billingPeriod} +

+ + {ctaText} + +
+
+

What's included:

+
    + {features.map((feature, index) => ( +
  • + + {feature} +
  • + ))} +
+
+
+ ); +}; + +export default PricingCard; \ No newline at end of file diff --git a/components/ui/header.tsx b/components/ui/header.tsx index 3a02938..8cc446d 100644 --- a/components/ui/header.tsx +++ b/components/ui/header.tsx @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils" import Link from "next/link" import { useState, useEffect } from "react" import { usePathname } from "next/navigation" +import dynamic from 'next/dynamic' interface HeaderProps { className?: string @@ -25,7 +26,11 @@ const leftNavigation = [ 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, +}); export function Header({ className, isLoggedIn = false, isAdmin = false }: HeaderProps) { const [mobileMenuOpen, setMobileMenuOpen] = useState(false) @@ -103,6 +108,9 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade ))} +
+ +
)} @@ -206,7 +214,7 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade })} {isLoggedIn && ( -
+
@@ -243,7 +251,7 @@ export function Header({ className, isLoggedIn = false, isAdmin = false }: Heade
-
+
)}
diff --git a/components/ui/theme-toggle.tsx b/components/ui/theme-toggle.tsx new file mode 100644 index 0000000..fa96da3 --- /dev/null +++ b/components/ui/theme-toggle.tsx @@ -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 ( + + ); +}; + +export default ThemeToggle; \ No newline at end of file diff --git a/lib/theme-context.tsx b/lib/theme-context.tsx new file mode 100644 index 0000000..b1ab659 --- /dev/null +++ b/lib/theme-context.tsx @@ -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(undefined); + +export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [theme, setTheme] = useState('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 ( + + {children} + + ); +}; + +export const useTheme = (): ThemeContextType => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index a447605..de8d250 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -8,7 +8,12 @@ module.exports = { './components/**/*.{js,ts,jsx,tsx}', ], theme: { - extend: {}, + extend: { + transitionProperty: { + 'width': 'width', + 'spacing': 'margin, padding', + }, + }, }, plugins: [ createThemes({ @@ -21,5 +26,9 @@ module.exports = { foreground: '#ededed', }, }), + require('tailwindcss/plugin')({ + darkMode: 'class', + }), ], + darkMode: 'class', } \ No newline at end of file