Compare commits

..

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 {
--background: #0a0a0a; --radius: 0.625rem;
--foreground: #ededed; --background: oklch(1 0 0);
} --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);
} }
body { .dark {
background: var(--background); --background: oklch(0.145 0 0);
color: var(--foreground); --foreground: oklch(0.985 0 0);
font-family: Arial, Helvetica, sans-serif; --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;
}
} }

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`}
> >
<ThemeProvider>
<Header/>
{children} {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">
<h2 className="text-3xl font-bold text-center text-gray-800 mb-4">See how Dishpix can help you improve your content online.</h2>
<div className="flex flex-col items-center">
<h3 className="text-lg font-semibold text-center text-gray-700 mb-2">
Photo Comparison
</h3>
<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">
<h4 className="text-sm font-semibold text-center text-gray-700 mb-1">
Before
</h4>
<Image <Image
aria-hidden src="/before-photo.png"
src="/file.svg" alt="Before photo"
alt="File icon" width={150}
width={16} height={113}
height={16} className="w-full h-auto rounded"
/> />
Learn </div>
</a> <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">
<a <h4 className="text-sm font-semibold text-center text-gray-700 mb-1">
className="flex items-center gap-2 hover:underline hover:underline-offset-4" After
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" </h4>
target="_blank"
rel="noopener noreferrer"
>
<Image <Image
aria-hidden src="/after-photo.png"
src="/window.svg" alt="After photo"
alt="Window icon" width={150}
width={16} height={113}
height={16} className="w-full h-auto rounded"
/> />
Examples </div>
</a> </div>
<a <div className="max-w-4xl mx-auto mt-8 text-center">
className="flex items-center gap-2 hover:underline hover:underline-offset-4" <h3 className="text-lg font-semibold text-gray-700 mb-2">
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app" What Happened?
target="_blank" </h3>
rel="noopener noreferrer" <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!
<Image </p>
aria-hidden </div>
src="/globe.svg" </div>
alt="Globe icon" </div>
width={16} </section>
height={16}
/>
Go to nextjs.org
</a>
</footer>
</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',
}