Compare commits

...

11 Commits

Author SHA1 Message Date
kbe
e53748d7a9 Wip on dark theme 2025-08-14 18:24:43 +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
19 changed files with 1694 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>
); );
} }

123
app/pricing/page.tsx Normal file
View 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;

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"
}

100
components/pricing-card.tsx Normal file
View 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);

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 }

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

@@ -0,0 +1,267 @@
"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,
});
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) {
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>
<ThemeToggleWrapper />
</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,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>('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;
};

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))
}

796
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,19 +9,26 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@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',
}