Compare commits
11 Commits
f03aaf1927
...
dark-theme
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e53748d7a9 | ||
|
|
8aba6e31c8 | ||
|
|
be22888afb | ||
|
|
6d8056d483 | ||
|
|
d50555cbc4 | ||
|
|
51b81c40af | ||
|
|
7641675180 | ||
|
|
be63600ca3 | ||
|
|
a214192c41 | ||
|
|
c52777afa2 | ||
|
|
18ff5b9beb |
13
app/components/ShadcnButton.tsx
Normal file
13
app/components/ShadcnButton.tsx
Normal 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;
|
||||
16
app/components/TailwindButton.tsx
Normal file
16
app/components/TailwindButton.tsx
Normal 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;
|
||||
127
app/globals.css
127
app/globals.css
@@ -1,26 +1,125 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--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 {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
import { Header } from "@/components/ui/header";
|
||||
import { ThemeProvider } from "@/lib/theme-context";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
@@ -27,7 +30,10 @@ export default function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<ThemeProvider>
|
||||
<Header/>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
156
app/page.tsx
156
app/page.tsx
@@ -1,103 +1,69 @@
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Header } from "@/components/ui/header";
|
||||
|
||||
export default function Home() {
|
||||
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">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<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>
|
||||
<div>
|
||||
<main className="min-h-80" style={{ background: 'linear-gradient(180deg, #fff 10%, #eef0f7)' }}>
|
||||
<section className="max-w-4xl mx-auto px-4 py-24">
|
||||
<h1 className="text-5xl font-extrabold text-center text-gray-900">
|
||||
Enhance Your Food Photos with Dishpix AI
|
||||
</h1>
|
||||
<p className="mt-6 text-xl text-center text-gray-600 max-w-2xl mx-auto">
|
||||
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.
|
||||
</p>
|
||||
</section>
|
||||
</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"
|
||||
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"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
{/*
|
||||
Add a section here to display before/after photo and demonstrate how the application works
|
||||
*/}
|
||||
|
||||
<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
|
||||
src="/before-photo.png"
|
||||
alt="Before photo"
|
||||
width={150}
|
||||
height={113}
|
||||
className="w-full h-auto rounded"
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
After
|
||||
</h4>
|
||||
<Image
|
||||
src="/after-photo.png"
|
||||
alt="After photo"
|
||||
width={150}
|
||||
height={113}
|
||||
className="w-full h-auto rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto mt-8 text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-700 mb-2">
|
||||
What Happened?
|
||||
</h3>
|
||||
<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!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
123
app/pricing/page.tsx
Normal file
123
app/pricing/page.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PricingCard from '@/components/pricing-card';
|
||||
import { useTheme } from '@/lib/theme-context';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
|
||||
const PricingPage = () => {
|
||||
const [isYearly, setIsYearly] = useState(false);
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
document.body.classList.add('pricing-page');
|
||||
}, []);
|
||||
|
||||
const toggleBillingPeriod = () => {
|
||||
setIsYearly(!isYearly);
|
||||
};
|
||||
|
||||
const getPrice = (basePrice: number) => {
|
||||
return isYearly ? Math.round(basePrice * 10 * 0.8) : basePrice;
|
||||
};
|
||||
|
||||
const getBillingPeriod = () => {
|
||||
return isYearly ? 'year' : 'month';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`min-h-screen ${theme === 'dark' ? 'bg-gray-900 text-gray-100' : 'bg-gray-50 text-gray-900'}`}>
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-extrabold sm:text-5xl">
|
||||
Find a plan to power your apps.
|
||||
</h1>
|
||||
<p className="mt-4 text-xl">
|
||||
Dishpix supports teams of all sizes, with pricing that scales.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pricing Toggle (Monthly/Yearly) */}
|
||||
<div className="mt-12 flex justify-center">
|
||||
<div className="relative inline-flex items-center bg-gray-200 dark:bg-gray-700 rounded-full p-1">
|
||||
<button
|
||||
onClick={toggleBillingPeriod}
|
||||
className={`relative px-4 py-2 text-sm font-medium rounded-full focus:outline-none transition-colors ${
|
||||
!isYearly
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Monthly
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleBillingPeriod}
|
||||
className={`relative px-4 py-2 text-sm font-medium rounded-full focus:outline-none transition-colors ${
|
||||
isYearly
|
||||
? 'bg-white text-gray-900 shadow-sm'
|
||||
: 'text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Yearly
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Cards Grid */}
|
||||
<div className="mt-12 sm:mt-16 lg:mt-20 space-y-8 sm:space-y-12 md:space-y-0 md:grid md:grid-cols-1 lg:grid-cols-3 md:gap-6 lg:gap-8">
|
||||
<PricingCard
|
||||
title="Start"
|
||||
price={`$${getPrice(9)}`}
|
||||
billingPeriod={getBillingPeriod()}
|
||||
features={[
|
||||
'Import your repo, deploy in seconds',
|
||||
'Automatic CI/CD',
|
||||
'Web Application Firewall',
|
||||
'Global, automated CDN',
|
||||
'Fluid compute',
|
||||
'DDoS Mitigation',
|
||||
'Traffic & performance insights',
|
||||
]}
|
||||
ctaText="Start Deploying"
|
||||
ctaHref="/signup?plan=start"
|
||||
/>
|
||||
<PricingCard
|
||||
title="Pro"
|
||||
price={`$${getPrice(29)}`}
|
||||
billingPeriod={getBillingPeriod()}
|
||||
features={[
|
||||
'Everything in Start, plus:',
|
||||
'10x more included usage',
|
||||
'Observability tools',
|
||||
'Faster builds',
|
||||
'Cold start prevention',
|
||||
'Advanced WAF Protection',
|
||||
'Email support',
|
||||
]}
|
||||
ctaText="Start a free trial"
|
||||
ctaHref="/signup?plan=pro"
|
||||
isPopular={true}
|
||||
/>
|
||||
<PricingCard
|
||||
title="Premium"
|
||||
price={`$${getPrice(99)}`}
|
||||
billingPeriod={getBillingPeriod()}
|
||||
features={[
|
||||
'Everything in Pro, plus:',
|
||||
'Guest & Team access controls',
|
||||
'SCIM & Directory Sync',
|
||||
'Managed WAF Rulesets',
|
||||
'Multi-region compute & failover',
|
||||
'99.99% SLA',
|
||||
'Advanced Support',
|
||||
]}
|
||||
ctaText="Contact Sales"
|
||||
ctaHref="/contact-sales"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PricingPage;
|
||||
21
components.json
Normal file
21
components.json
Normal 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
100
components/pricing-card.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '@/lib/theme-context';
|
||||
|
||||
interface PricingCardProps {
|
||||
title: string;
|
||||
price: string;
|
||||
billingPeriod: string;
|
||||
features: string[];
|
||||
ctaText: string;
|
||||
ctaHref: string;
|
||||
isPopular?: boolean;
|
||||
}
|
||||
|
||||
const PricingCard: React.FC<PricingCardProps> = ({
|
||||
title,
|
||||
price,
|
||||
billingPeriod,
|
||||
features,
|
||||
ctaText,
|
||||
ctaHref,
|
||||
isPopular = false,
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
|
||||
// Add ARIA label for screen readers
|
||||
const cardAriaLabel = `${title} pricing plan - $${price}/${billingPeriod}`;
|
||||
|
||||
const cardBg = theme === 'dark' ? 'bg-gray-800' : 'bg-white';
|
||||
const cardText = theme === 'dark' ? 'text-gray-100' : 'text-gray-900';
|
||||
const cardBorder = theme === 'dark' ? 'border-gray-700' : 'border-gray-200';
|
||||
const cardShadow = theme === 'dark' ? 'shadow-lg' : 'shadow-sm';
|
||||
const cardHover = theme === 'dark' ? 'hover:bg-gray-700' : 'hover:bg-indigo-600';
|
||||
const cardFocus = theme === 'dark' ? 'focus:ring-indigo-500' : 'focus:ring-indigo-500';
|
||||
const cardPopular = theme === 'dark' ? 'bg-indigo-700' : 'bg-indigo-600';
|
||||
const cardFeatureText = theme === 'dark' ? 'text-gray-300' : 'text-gray-700';
|
||||
const cardFeatureIcon = theme === 'dark' ? 'text-green-400' : 'text-green-500';
|
||||
const cardHoverShadow = theme === 'dark' ? 'hover:shadow-xl' : 'hover:shadow-2xl';
|
||||
const cardTransition = 'transition-all duration-300';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col ${cardBg} border ${cardBorder} rounded-xl ${cardShadow} overflow-hidden ${cardTransition} ${cardHoverShadow}`}
|
||||
role="article"
|
||||
aria-label={cardAriaLabel}
|
||||
tabIndex={0} // Make the card focusable
|
||||
>
|
||||
{isPopular && (
|
||||
<div
|
||||
className={`${cardPopular} text-white text-xs font-semibold px-3 py-1.5 rounded-t-xl`}
|
||||
>
|
||||
Popular
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6">
|
||||
<h3 className={`text-2xl font-bold ${cardText}`}>{title}</h3>
|
||||
<p className="mt-2 text-sm text-gray-400">
|
||||
{title === 'Start' && 'The perfect starting place for your web app or personal project.'}
|
||||
{title === 'Pro' && 'Everything you need to build and scale your app.'}
|
||||
{title === 'Premium' && 'Critical security, performance, observability, platform SLAs, and support.'}
|
||||
</p>
|
||||
<p className="mt-6">
|
||||
<span className="text-4xl font-extrabold">{price}</span>
|
||||
<span className="text-base font-medium text-gray-400 ml-1">/{billingPeriod}</span>
|
||||
</p>
|
||||
<a
|
||||
href={ctaHref}
|
||||
className={`mt-6 block w-full py-3 px-4 border border-transparent rounded-md shadow text-center text-white bg-indigo-600 ${cardHover} focus:outline-none focus:ring-2 focus:ring-offset-2 ${cardFocus} ${cardTransition}`}
|
||||
aria-label={`${ctaText} for ${title} plan`}
|
||||
>
|
||||
{ctaText}
|
||||
</a>
|
||||
</div>
|
||||
<div className={`border-t ${cardBorder} p-6`}>
|
||||
<h4 className={`text-sm font-medium ${cardText}`}>What's included:</h4>
|
||||
<ul className="mt-4 space-y-3">
|
||||
{features.map((feature, index) => (
|
||||
<li key={index} className="flex items-start">
|
||||
<svg
|
||||
className={`h-5 w-5 ${cardFeatureIcon} flex-shrink-0`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span className={`ml-3 text-sm ${cardFeatureText}`}>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(PricingCard);
|
||||
59
components/ui/button.tsx
Normal file
59
components/ui/button.tsx
Normal 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
267
components/ui/header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
23
components/ui/theme-toggle.tsx
Normal file
23
components/ui/theme-toggle.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '@/lib/theme-context';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
|
||||
const ThemeToggle: React.FC = () => {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2"
|
||||
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
|
||||
>
|
||||
{theme === 'dark' ? (
|
||||
<Sun className="h-3 w-3" />
|
||||
) : (
|
||||
<Moon className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
49
lib/theme-context.tsx
Normal file
49
lib/theme-context.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
import React, { createContext, useState, useEffect, ReactNode, useContext } from 'react';
|
||||
|
||||
type Theme = 'light' | 'dark';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
||||
export const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [theme, setTheme] = useState<Theme>('dark'); // Default to dark mode
|
||||
|
||||
useEffect(() => {
|
||||
const savedTheme = localStorage.getItem('theme') as Theme | null;
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme((prevTheme) => (prevTheme === 'dark' ? 'light' : 'dark'));
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
6
lib/utils.ts
Normal file
6
lib/utils.ts
Normal 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
796
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
17
package.json
17
package.json
@@ -9,19 +9,26 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"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-dom": "19.1.0",
|
||||
"next": "15.4.6"
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"tailwindcss": "^4",
|
||||
"eslint": "^9",
|
||||
"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
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
BIN
public/before-photo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.0 MiB |
4
public/logo.svg
Normal file
4
public/logo.svg
Normal 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
34
tailwind.config.js
Normal 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',
|
||||
}
|
||||
Reference in New Issue
Block a user