12 Commits

Author SHA1 Message Date
kbe
a1e49a1e1a Implemented pricing page 2025-08-14 18:38:57 +02:00
kbe
9d5f57c13e Shiny pricing page 2025-08-14 18:30:29 +02:00
kbe
8aba6e31c8 Prepare dark theme, add pricing page 2025-08-14 18:15:08 +02:00
kbe
be22888afb Fix gray bar on mobile navbar bottom 2025-08-14 17:43:12 +02:00
kbe
6d8056d483 Add a sign-up and login button in the navbar 2025-08-14 17:42:02 +02:00
kbe
d50555cbc4 In the header make use of next/navigation to get the path name and display current link 2025-08-14 17:08:56 +02:00
kbe
51b81c40af Add conditional link for logged in user and administrator 2025-08-14 16:51:20 +02:00
kbe
7641675180 Add tailwind navbar 2025-08-14 16:36:58 +02:00
kbe
be63600ca3 Improve default page 2025-08-13 16:43:18 +02:00
kbe
a214192c41 Photo comparison 2025-08-13 16:18:15 +02:00
kbe
c52777afa2 Moving Header to layout.tsx 2025-08-13 15:17:31 +02:00
kbe
18ff5b9beb prepare layout 2025-08-13 15:13:57 +02:00
20 changed files with 1930 additions and 125 deletions

View File

@@ -0,0 +1,13 @@
"use client";
import React from 'react';
import { Button } from '@shadcn/ui';
const ShadcnButton: React.FC = () => {
return (
<Button onClick={() => alert('Button clicked!')}>
Click me
</Button>
);
};
export default ShadcnButton;

View File

@@ -0,0 +1,16 @@
"use client";
import React from 'react';
const TailwindButton: React.FC = () => {
return (
<button
onClick={() => alert('Button clicked!')}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
Click me
</button>
);
};
export default TailwindButton;

View File

@@ -1,26 +1,125 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css";
:root { @custom-variant dark (&:is(.dark *));
--background: #ffffff;
--foreground: #171717;
}
@theme inline { @theme inline {
--color-background: var(--background); --color-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans); --font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono); --font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
} }
@media (prefers-color-scheme: dark) { :root {
:root { --radius: 0.625rem;
--background: #0a0a0a; --background: oklch(1 0 0);
--foreground: #ededed; --foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
.pricing-page {
@apply bg-background text-foreground;
} }
} }
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -2,6 +2,9 @@ import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css"; import "./globals.css";
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",
subsets: ["latin"], subsets: ["latin"],
@@ -27,7 +30,10 @@ export default function RootLayout({
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
{children} <ThemeProvider>
<Header/>
{children}
</ThemeProvider>
</body> </body>
</html> </html>
); );

View File

@@ -1,103 +1,69 @@
import Image from "next/image"; import Image from "next/image";
import { Button } from "@/components/ui/button";
import { Header } from "@/components/ui/header";
export default function Home() { export default function Home() {
return ( return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"> <div>
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start"> <main className="min-h-80" style={{ background: 'linear-gradient(180deg, #fff 10%, #eef0f7)' }}>
<Image <section className="max-w-4xl mx-auto px-4 py-24">
className="dark:invert" <h1 className="text-5xl font-extrabold text-center text-gray-900">
src="/next.svg" Enhance Your Food Photos with Dishpix AI
alt="Next.js logo" </h1>
width={180} <p className="mt-6 text-xl text-center text-gray-600 max-w-2xl mx-auto">
height={38} Dishpix is the perfect website for people who want to take their food photography to the next level. Whether you're a professional chef, a food blogger, or just someone who loves capturing delicious moments, Dishpix offers tools and features to help you create stunning food photos.
priority </p>
/> </section>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main> </main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a {/*
className="flex items-center gap-2 hover:underline hover:underline-offset-4" Add a section here to display before/after photo and demonstrate how the application works
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" */}
target="_blank"
rel="noopener noreferrer" <section className="w-full" style={{ backgroundColor: 'rgb(248, 249, 254)' }}>
> <div className="max-w-screen-xl mx-auto py-24">
<Image <h2 className="text-3xl font-bold text-center text-gray-800 mb-4">See how Dishpix can help you improve your content online.</h2>
aria-hidden <div className="flex flex-col items-center">
src="/file.svg" <h3 className="text-lg font-semibold text-center text-gray-700 mb-2">
alt="File icon" Photo Comparison
width={16} </h3>
height={16} <div className="flex flex-col md:flex-row justify-center space-y-4 md:space-y-0 md:space-x-4 w-full">
/> <div className="w-full md:w-1/2 p-4 bg-gray-100 rounded-lg shadow-md border-2 border-solid border-color-indigo-500">
Learn <h4 className="text-sm font-semibold text-center text-gray-700 mb-1">
</a> Before
<a </h4>
className="flex items-center gap-2 hover:underline hover:underline-offset-4" <Image
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" src="/before-photo.png"
target="_blank" alt="Before photo"
rel="noopener noreferrer" width={150}
> height={113}
<Image className="w-full h-auto rounded"
aria-hidden />
src="/window.svg" </div>
alt="Window icon" <div className="w-full md:w-1/2 p-4 bg-gray-100 rounded-lg shadow-md border-2 border-solid border-color-indigo-500">
width={16} <h4 className="text-sm font-semibold text-center text-gray-700 mb-1">
height={16} After
/> </h4>
Examples <Image
</a> src="/after-photo.png"
<a alt="After photo"
className="flex items-center gap-2 hover:underline hover:underline-offset-4" width={150}
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" height={113}
target="_blank" className="w-full h-auto rounded"
rel="noopener noreferrer" />
> </div>
<Image </div>
aria-hidden <div className="max-w-4xl mx-auto mt-8 text-center">
src="/globe.svg" <h3 className="text-lg font-semibold text-gray-700 mb-2">
alt="Globe icon" What Happened?
width={16} </h3>
height={16} <p className="text-gray-600">
/> Dishpix AI transforms ordinary food photos into stunning, professional-quality images. Our advanced algorithms enhance colors, adjust lighting, and add artistic touches to make your food photos look incredible. Simply upload your photo and let the magic happen!
Go to nextjs.org </p>
</a> </div>
</footer> </div>
</div>
</section>
</div> </div>
); );
} }

115
app/pricing/page.tsx Normal file
View File

@@ -0,0 +1,115 @@
"use client";
import React, { useState, useEffect } from 'react';
import PricingCard from '@/components/pricing-card';
import { useTheme } from '@/lib/theme-context';
import PricingToggle from '@/components/ui/pricing-toggle';
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';
};
const getDiscountBadge = () => {
if (isYearly) {
return { text: 'Save 20%', style: 'text-green-500 dark:text-green-400' };
}
return { text: '', style: '' };
};
return (
<div className={`min-h-screen ${theme === 'dark' ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'} transition-colors duration-300`}>
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="text-center">
<h1 className="text-3xl font-extrabold sm:text-4xl md:text-5xl transition-all duration-300">
Transform your food photos with AI
</h1>
<p className="mt-3 text-lg sm:text-xl transition-opacity duration-300">
Choose the perfect plan to enhance your culinary photography with cutting-edge AI technology.
</p>
<p className="mt-2 text-sm font-medium text-indigo-600 dark:text-indigo-400 transition-colors duration-300">
{getDiscountBadge().text}
</p>
</div>
{/* Pricing Toggle (Monthly/Yearly) */}
<PricingToggle isYearly={isYearly} onToggle={toggleBillingPeriod} />
{/* Pricing Cards Grid */}
<div className="mt-12 sm:mt-16 lg:mt-20 space-y-8 sm:space-y-12 md:space-y-16 lg:space-y-0 sm:grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 sm:gap-6 lg:gap-8 transition-all duration-300">
<PricingCard
title="Basic"
price={`$${getPrice(9)}`}
billingPeriod={getBillingPeriod()}
features={[
'Enhance up to 50 food photos/month',
'Basic AI food recognition',
'Standard lighting correction',
'Color enhancement',
'Remove minor blemishes',
'Basic background cleanup',
'Standard resolution exports',
]}
ctaText="Start Enhancing"
ctaHref="#"
/>
<PricingCard
title="Pro"
price={`$${getPrice(29)}`}
billingPeriod={getBillingPeriod()}
features={[
'Everything in Basic, plus:',
'Enhance up to 500 food photos/month',
'Advanced AI food styling',
'Professional lighting simulation',
'Gourmet color grading',
'Remove unwanted objects',
'AI-powered ingredient recognition',
'High-resolution exports',
'Priority processing',
]}
ctaText="Start free trial"
ctaHref="#"
isPopular={true}
/>
<PricingCard
title="MasterChef"
price={`$${getPrice(99)}`}
billingPeriod={getBillingPeriod()}
features={[
'Everything in Pro, plus:',
'Unlimited food photo enhancements',
'AI-generated food styling suggestions',
'Restaurant-quality presentation',
'Advanced background replacement',
'Multi-dish composition',
'Professional food photography presets',
'4K resolution exports',
'Batch processing capabilities',
'Priority support',
]}
ctaText="Get Started"
ctaHref="#"
/>
</div>
</div>
</div>
);
};
export default PricingPage;

21
components.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,90 @@
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();
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-xl' : 'shadow-2xl';
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 (
<div className={`flex flex-col ${cardBg} border ${cardBorder} rounded-xl ${cardShadow} overflow-hidden transition-all duration-300 transform hover:scale-105`}>
{isPopular && (
<div className={`${cardPopular} px-3 py-1.5 text-center text-white text-xs font-semibold rounded-t-xl`}>
Most Popular
</div>
)}
{!isPopular && (
<div className="py-3.5"></div>
)}
<div className="p-6">
<h3 className={`text-2xl font-bold ${cardText}`}>{title}</h3>
<p className="mt-4 text-sm text-gray-400">
{title === 'Basic' && 'Perfect for food bloggers and home cooks looking to enhance their culinary photography.'}
{title === 'Pro' && 'Everything you need to showcase restaurant dishes professionally and attract more customers.'}
{title === 'MasterChef' && 'Complete professional suite for food photographers and culinary brands demanding perfection.'}
</p>
<p className="mt-8">
<span className="text-5xl font-extrabold">{price}</span>
<span className="text-base font-medium text-gray-400">/{billingPeriod}</span>
</p>
<a
href={ctaHref}
className={`mt-8 block w-full py-3 px-6 border border-transparent rounded-md shadow-lg text-center text-white bg-indigo-600 ${cardHover} focus:outline-none focus:ring-2 focus:ring-offset-2 ${cardFocus} transition-all duration-200`}
>
{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 PricingCard;

59
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

263
components/ui/header.tsx Normal file
View File

@@ -0,0 +1,263 @@
"use client";
import * as React from "react"
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
isLoggedIn?: boolean
isAdmin?: boolean
}
const leftNavigation = [
{ name: 'Home', href: '/', current: false, requiresAuth: false },
{ name: 'Pricing', href: '/pricing', current: false, requiresAuth: false },
{ name: 'FAQ', href: '/faq', current: false, requiresAuth: false },
{ name: 'Dashboard', href: '/dashboard', current: false, requiresAuth: true },
{ name: 'Projects', href: '/projects', current: false, requiresAuth: true },
{ name: 'Calendar', href: '/calendar', 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,
});
export function Header({ className, isLoggedIn = false, isAdmin = false }: HeaderProps) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [profileOpen, setProfileOpen] = useState(false)
const [activeNavItem, setActiveNavItem] = useState<string | null>(null)
// Use usePathname for client-side navigation
let pathname = usePathname()
// Update activeNavItem when pathname changes
useEffect(() => {
if (pathname) {
setActiveNavItem(pathname)
}
}, [pathname])
// Only render the header after the pathname is available
if (!pathname) {
return null
}
return (
<header className={cn("bg-gray-900 shadow-sm", className)}>
<nav className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
<div className="flex items-center">
<div className="flex-shrink-0">
<Link href="/">
<img
className="h-8 w-auto"
src="/logo.svg"
alt="Your Company"
/>
</Link>
</div>
<div className="hidden md:block">
<div className="ml-10 flex items-baseline space-x-4">
{leftNavigation.map((item) => {
const isCurrent = activeNavItem === item.href
return (
(item.requiresAuth ? (isLoggedIn || isAdmin) : true) && (
<Link
key={item.name}
href={item.href}
className={cn(
isCurrent
? 'bg-gray-800 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
'rounded-md px-3 py-2 text-sm font-medium transition-colors'
)}
aria-current={isCurrent ? 'page' : undefined}
>
{item.name}
</Link>
)
)
})}
</div>
</div>
</div>
<div className="hidden md:block">
<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>
{/*
<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>
*/}
</div>
</div>
)}
{isLoggedIn && (
<div>
<button
type="button"
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="sr-only">View notifications</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>
</button>
{/* Profile dropdown */}
<div className="relative ml-3">
<div>
<button
type="button"
className="relative flex max-w-xs items-center rounded-full bg-gray-800 text-sm focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-800"
onClick={() => setProfileOpen(!profileOpen)}
>
<span className="absolute -inset-1.5" />
<span className="sr-only">Open user menu</span>
<img
className="h-8 w-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</button>
</div>
{profileOpen && (
<div className="absolute right-0 z-10 mt-2 w-48 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<Link href="/profile" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Your Profile
</Link>
<Link href="/settings" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Settings
</Link>
<Link href="/logout" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
Sign out
</Link>
</div>
)}
</div>
</div>
)}
</div>
</div>
<div className="-mr-2 flex md:hidden">
{/* Mobile menu button */}
<button
type="button"
className="relative inline-flex items-center justify-center rounded-md bg-gray-800 p-2 text-gray-400 hover:bg-gray-700 hover:text-white focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
<span className="absolute -inset-0.5" />
<span className="sr-only">Open main menu</span>
{mobileMenuOpen ? (
<svg className="block h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="block h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
)}
</button>
</div>
</div>
</nav>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="md:hidden">
<div className="space-y-1 px-2 pb-3 pt-2 sm:px-3">
{leftNavigation.map((item) => {
const isCurrent = activeNavItem === item.href
return (
(item.requiresAuth ? (isLoggedIn || isAdmin) : true) && (
<Link
key={item.name}
href={item.href}
className={cn(
isCurrent
? 'bg-gray-900 text-white'
: 'text-gray-300 hover:bg-gray-700 hover:text-white',
'block rounded-md px-3 py-2 text-base font-medium transition-colors'
)}
aria-current={isCurrent ? 'page' : undefined}
>
{item.name}
</Link>
)
)
})}
</div>
{isLoggedIn && (
<div className="border-t border-gray-700 pb-3 pt-4">
<div>
<div className="flex items-center px-5">
<div className="flex-shrink-0">
<img
className="h-10 w-10 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</div>
<div className="ml-3">
<div className="text-base font-medium leading-none text-white">User Name</div>
<div className="text-sm font-medium leading-none text-gray-400">user@example.com</div>
</div>
<button
type="button"
className="relative ml-auto flex-shrink-0 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="sr-only">View notifications</span>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
</svg>
</button>
</div>
<div className="mt-3 space-y-1 px-2">
<Link href="/profile" className="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-700 hover:text-white">
Your Profile
</Link>
<Link href="/settings" className="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-700 hover:text-white">
Settings
</Link>
<Link href="/logout" className="block rounded-md px-3 py-2 text-base font-medium text-gray-400 hover:bg-gray-700 hover:text-white">
Sign out
</Link>
</div>
</div>
</div>
)}
</div>
)}
</header>
)
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import { Switch } from '@headlessui/react';
import { useTheme } from '@/lib/theme-context';
interface PricingToggleProps {
isYearly: boolean;
onToggle: () => void;
}
const PricingToggle: React.FC<PricingToggleProps> = ({ isYearly, onToggle }) => {
const { theme } = useTheme();
const thumbColor = theme === 'dark' ? 'bg-white' : 'bg-gray-900';
const trackColor = theme === 'dark' ? 'bg-gray-700' : 'bg-gray-200';
const activeTrackColor = theme === 'dark' ? 'bg-indigo-600' : 'bg-indigo-500';
return (
<div className="flex items-center justify-center mt-12 space-x-4">
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}`}>
Monthly
</span>
<div className="relative inline-flex items-center">
<Switch
checked={isYearly}
onChange={onToggle}
className={`relative inline-flex items-center h-6 rounded-full w-16 transition-colors duration-200 ease-in-out ${
isYearly ? activeTrackColor : trackColor
}`}
>
<span
className={`inline-block w-7 h-5 transform rounded-full shadow transition-transform duration-200 ease-in-out ${
isYearly ? 'translate-x-8' : 'translate-x-0'
} ${thumbColor}`}
/>
</Switch>
</div>
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-gray-300' : 'text-gray-700'}`}>
Yearly
</span>
</div>
);
};
export default PricingToggle;

View 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
View 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>('light'); // Default to light mode (white backgrounds)
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;
};

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

1009
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,19 +9,27 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.7",
"@radix-ui/react-slot": "^1.2.3",
"@shadcn/ui": "^0.0.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.539.0",
"next": "15.4.6",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"next": "15.4.6" "tailwind-merge": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.4.6", "eslint-config-next": "15.4.6",
"@eslint/eslintrc": "^3" "tailwindcss": "^4",
"tw-animate-css": "^1.3.6",
"typescript": "^5"
} }
} }

BIN
public/after-photo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

BIN
public/before-photo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

4
public/logo.svg Normal file
View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="#007bff" />
<text x="50" y="55" font-size="20" text-anchor="middle" fill="white">Logo</text>
</svg>

After

Width:  |  Height:  |  Size: 203 B

34
tailwind.config.js Normal file
View File

@@ -0,0 +1,34 @@
/** @type {import('tailwindcss').Config} */
const { createThemes } = require('@shadcn/ui');
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}',
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
transitionProperty: {
'width': 'width',
'spacing': 'margin, padding',
},
},
},
plugins: [
createThemes({
light: {
background: '#ffffff',
foreground: '#171717',
},
dark: {
background: '#0a0a0a',
foreground: '#ededed',
},
}),
require('tailwindcss/plugin')({
darkMode: 'class',
}),
],
darkMode: 'class',
}