Initial ScaleSite Next.js implementation

Complete frontend implementation with:
- Next.js 16 with App Router and TypeScript
- Tailwind CSS v4 with custom violet theme
- shadcn/ui components with Lucide React icons
- Landing page with hero, services, pricing, testimonials, FAQ
- Service selection page with toggle
- Login/Register pages with social auth UI
- Multi-step checkout flow
- Client dashboard with stats, projects, support tickets
- Billing page with subscription, payment methods, invoices
- All mock data and TypeScript types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
bast1qn 2026-02-01 21:42:48 +01:00
parent aa211a53d3
commit 98552163a8
63 changed files with 6604 additions and 93 deletions

192
CLAUDE.md Normal file
View File

@ -0,0 +1,192 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
ScaleSite is a Next.js 16 frontend-only application for a digital agency service platform. It provides web design and AI automation services with a complete client portal. The application uses mock data only—no backend or database.
**Tech Stack:**
- Next.js 16.1.6 with App Router and Turbopack
- React 19.2.3
- TypeScript 5
- Tailwind CSS v4 (CSS-based theme configuration)
- shadcn/ui components (Radix UI primitives)
- Lucide React icons
- React Hook Form + Zod validation
## Development Commands
```bash
# Start development server (runs on port 3000 by default)
npm run dev
# Production build
npm run build
# Start production server
npm start
# Lint
npm run lint
```
## Architecture
### Page Structure
**Public Pages (Marketing Layout)**
- `/` - Landing page with hero, services, pricing, testimonials, FAQ
- `/services` - Service selection with toggle between Web Design and AI Automation
- `/login` and `/register` - Authentication pages
**Protected Pages (Dashboard Layout)**
- `/dashboard` - Client dashboard with stats, projects, tickets
- `/billing` - Subscription management, payment methods, invoice history
**Checkout Flow**
- `/checkout` - Multi-step checkout (Account → Billing → Payment)
### Layouts
Pages use one of two layout wrappers:
**MarketingLayout** (`components/layouts/marketing-layout.tsx`)
- Used by: `/`, `/services`
- Includes: SiteHeader, SiteFooter
- Sticky header with glass morphism effect
**Dashboard Layout** (sidebar layout)
- Used by: `/dashboard`, `/billing`
- Note: Dashboard pages currently render inline without a wrapper component
- Components reference: `DashboardSidebar` in `components/layouts/dashboard-sidebar.tsx`
- Navigation items: Dashboard, Projects, AI Automations, Support, Billing, Settings
**Auth Layout** (`components/auth/auth-layout.tsx`)
- Used by: `/login`, `/register`
- Split screen: hero image (left) + form (right)
- No header/footer
**Minimal Layout**
- Used by: `/checkout`
- Logo header only, no navigation
### Data & Types
**TypeScript Types** (`lib/types/index.ts`)
- `User`, `ServiceType`, `PricingTier`, `PricingPlan`
- `Project`, `ProjectStatus`, `ProjectStage`
- `SupportTicket`, `TicketStatus`
- `Invoice`, `InvoiceStatus`, `PaymentMethod`, `Subscription`
- `CheckoutSession`, `CheckoutStep`, `Address`
- `Testimonial`, `FAQ`, `DashboardStats`
**Mock Data** (`lib/mock-data/`)
- All data is mocked—no API calls
- Exports: `mockProjects`, `mockSupportTickets`, `mockInvoices`, `mockTestimonials`, `mockPricingPlans`, etc.
- Helper functions: `getPricingPlanById()`, `getPricingPlansByServiceType()`, `getAllFAQs()`
### Component Organization
```
components/
├── auth/ # Auth-related components (social login, password input)
├── billing/ # Subscription, payment methods, invoice table
├── checkout/ # Checkout steps, summary, payment forms
├── dashboard/ # Stats cards, project cards, tickets
├── layouts/ # Marketing layout, header, footer, sidebar
├── marketing/ # Hero, service cards, pricing, testimonials, FAQ
├── mobile/ # Mobile-specific components (if implementing separate views)
└── ui/ # shadcn/ui base components (button, card, input, etc.)
```
### Icon System
**Lucide React Only**
- All icons use Lucide React (`lucide-react` package)
- Icons passed as components: `icon={Rocket}` not `icon="rocket"`
- Do NOT use Material Symbols (previously caused display issues)
- Import pattern: `import { Rocket, Bot, ArrowRight } from 'lucide-react'`
**Component Props Pattern**
For components that accept icons:
```tsx
import { LucideIcon } from 'lucide-react'
interface Props {
icon: LucideIcon // Pass component, not string
}
export function Component({ icon: Icon }: Props) {
return <Icon className="w-6 h-6" />
}
```
### Styling & Theme
**Tailwind CSS v4 Configuration** (app/globals.css)
- Theme defined inline with `@theme` directive
- Custom colors:
- `primary`: #8B5CF6 (Electric Violet)
- `background`: #0B0B0B (Deep Obsidian)
- `surface`: #1E1E1E (Slate Gray)
- Dark mode by default: `<html lang="en" className="dark">` in root layout
**Utility Classes**
- `.glow-button` - Primary button with purple glow effect
- `.glass-card` - Glass morphism card background
- `.glass-nav` - Glass navigation header
### Special Patterns
**Checkout Suspense Boundary**
Checkout page uses `useSearchParams` which requires a Suspense boundary:
```tsx
// app/checkout/page.tsx
import { Suspense } from 'react'
import { CheckoutContent } from './checkout-content'
export default function CheckoutPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<CheckoutContent />
</Suspense>
)
}
```
The actual client component with `useSearchParams` is in `checkout-content.tsx`.
**Status Color Mapping**
Several components use Record types for status-based styling:
```tsx
const statusColors: Record<Project['status'], string> = {
discovery: 'bg-blue-500/10 text-blue-400',
design: 'bg-purple-500/10 text-purple-400',
// ...
}
```
## Common Tasks
**Adding a New Page**
1. Create `app/route-name/page.tsx`
2. For marketing pages: wrap with `MarketingLayout`
3. For dashboard pages: render content inline (layout wrapper not yet implemented)
**Adding Mock Data**
1. Create file in `lib/mock-data/`
2. Export data and any helper functions
3. Re-export in `lib/mock-data/index.ts`
**Updating Icons**
- Always use Lucide React components
- Search available icons: https://lucide.dev/icons/
- Import and pass as component props
## Known Limitations
- No backend—all data is mocked
- Dashboard layout wrapper not implemented (pages render sidebar content inline)
- No authentication flow (login/register pages are UI only)
- No form validation connected (React Hook Form + Zod installed but not implemented)

16
app/billing/layout.tsx Normal file
View File

@ -0,0 +1,16 @@
import { DashboardSidebar } from '@/components/layouts/dashboard-sidebar'
export default function BillingLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex min-h-screen w-full flex-col lg:flex-row overflow-hidden">
<DashboardSidebar />
<main className="flex-1 flex flex-col h-full lg:h-screen overflow-y-auto bg-background custom-scrollbar">
{children}
</main>
</div>
)
}

34
app/billing/page.tsx Normal file
View File

@ -0,0 +1,34 @@
import { SubscriptionCard } from '@/components/billing/subscription-card'
import { PaymentMethodList } from '@/components/billing/payment-method-list'
import { InvoiceTable } from '@/components/billing/invoice-table'
import { mockSubscription, mockPaymentMethods, mockInvoices } from '@/lib/mock-data'
export default function BillingPage() {
return (
<div className="flex-1 w-full max-w-7xl mx-auto p-4 lg:p-8 flex flex-col gap-8">
{/* Header */}
<div className="flex flex-col gap-1">
<h1 className="text-3xl lg:text-4xl font-black tracking-tight text-foreground">
Billing & Invoices
</h1>
<p className="text-muted-foreground text-base font-medium">
Manage your subscription and payment methods
</p>
</div>
{/* Billing Content */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left Column - Subscription & Payment Methods */}
<div className="xl:col-span-1 flex flex-col gap-6">
<SubscriptionCard subscription={mockSubscription} />
<PaymentMethodList paymentMethods={mockPaymentMethods} />
</div>
{/* Right Column - Invoice History */}
<div className="xl:col-span-2">
<InvoiceTable invoices={mockInvoices} />
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,285 @@
'use client'
import { useState } from 'react'
import { useSearchParams } from 'next/navigation'
import { Card } from '@/components/ui/card'
import { CheckoutSteps } from '@/components/checkout/checkout-steps'
import { CheckoutSummary } from '@/components/checkout/checkout-summary'
import { PaymentMethodCard } from '@/components/checkout/payment-method-card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { getPricingPlanById } from '@/lib/mock-data'
import Link from 'next/link'
import { Rocket, ChevronRight } from 'lucide-react'
export function CheckoutContent() {
const searchParams = useSearchParams()
const planId = searchParams.get('plan') || 'growth-web'
const step = (searchParams.get('step') as 'account' | 'billing' | 'payment') || 'account'
const [currentStep, setCurrentStep] = useState<'account' | 'billing' | 'payment'>(step)
const [selectedPaymentMethod, setSelectedPaymentMethod] = useState('card')
const plan = getPricingPlanById(planId)
if (!plan) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">Plan not found</h1>
<Button asChild>
<Link href="/services">Back to Services</Link>
</Button>
</div>
</div>
)
}
const serviceName = plan.serviceType === 'web-design' ? 'Web Design' : 'AI Automation'
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b border-border py-4">
<div className="max-w-[1280px] mx-auto px-4 flex items-center gap-3">
<Link href="/" className="flex items-center gap-3">
<div className="size-8 flex items-center justify-center rounded bg-gradient-to-br from-primary to-primary-glow text-white">
<Rocket className="w-5 h-5" />
</div>
<h2 className="text-foreground text-lg font-bold">ScaleSite</h2>
</Link>
</div>
</header>
<div className="max-w-6xl mx-auto px-4 py-12">
{/* Steps */}
<CheckoutSteps currentStep={currentStep} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left side - Forms */}
<div className="lg:col-span-2">
{/* Account Step */}
{currentStep === 'account' && (
<Card className="bg-card border-border p-6">
<h2 className="text-xl font-bold text-foreground mb-6">Account Information</h2>
<form className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="email">Email Address</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
className="bg-background border-border"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="confirm-email">Confirm Email</Label>
<Input
id="confirm-email"
type="email"
placeholder="you@example.com"
className="bg-background border-border"
/>
</div>
<div className="flex items-start gap-2">
<input type="checkbox" id="newsletter" className="mt-1" />
<Label htmlFor="newsletter" className="text-sm text-muted-foreground">
I want to receive updates about my order and promotional offers
</Label>
</div>
<Button
type="button"
onClick={() => setCurrentStep('billing')}
className="glow-button bg-primary hover:bg-primary-glow text-white w-full"
>
Continue to Billing
</Button>
</form>
</Card>
)}
{/* Billing Step */}
{currentStep === 'billing' && (
<Card className="bg-card border-border p-6">
<h2 className="text-xl font-bold text-foreground mb-6">Billing Address</h2>
<form className="flex flex-col gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="firstName">First Name</Label>
<Input
id="firstName"
type="text"
placeholder="John"
className="bg-background border-border"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="lastName">Last Name</Label>
<Input
id="lastName"
type="text"
placeholder="Doe"
className="bg-background border-border"
/>
</div>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="address">Street Address</Label>
<Input
id="address"
type="text"
placeholder="123 Main St"
className="bg-background border-border"
/>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="city">City</Label>
<Input
id="city"
type="text"
placeholder="Berlin"
className="bg-background border-border"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="postalCode">Postal Code</Label>
<Input
id="postalCode"
type="text"
placeholder="10115"
className="bg-background border-border"
/>
</div>
<div className="flex flex-col gap-2 col-span-2 md:col-span-1">
<Label htmlFor="country">Country</Label>
<Input
id="country"
type="text"
placeholder="Germany"
className="bg-background border-border"
/>
</div>
</div>
<div className="flex gap-3">
<Button
type="button"
variant="outline"
onClick={() => setCurrentStep('account')}
className="bg-card border-border"
>
Back
</Button>
<Button
type="button"
onClick={() => setCurrentStep('payment')}
className="glow-button bg-primary hover:bg-primary-glow text-white flex-1"
>
Continue to Payment
</Button>
</div>
</form>
</Card>
)}
{/* Payment Step */}
{currentStep === 'payment' && (
<Card className="bg-card border-border p-6">
<h2 className="text-xl font-bold text-foreground mb-6">Payment Method</h2>
<div className="flex flex-col gap-3 mb-6">
<PaymentMethodCard
id="card"
icon="card"
label="Credit Card"
description="Pay with Visa, Mastercard, or Amex"
selected={selectedPaymentMethod === 'card'}
onSelect={() => setSelectedPaymentMethod('card')}
/>
<PaymentMethodCard
id="paypal"
icon="paypal"
label="PayPal"
description="Pay with your PayPal account"
selected={selectedPaymentMethod === 'paypal'}
onSelect={() => setSelectedPaymentMethod('paypal')}
/>
<PaymentMethodCard
id="stripe-link"
icon="link"
label="Stripe Link"
description="Quick payment with saved cards"
selected={selectedPaymentMethod === 'stripe-link'}
onSelect={() => setSelectedPaymentMethod('stripe-link')}
/>
</div>
{selectedPaymentMethod === 'card' && (
<form className="flex flex-col gap-4 border-t border-border pt-6">
<div className="flex flex-col gap-2">
<Label htmlFor="cardNumber">Card Number</Label>
<Input
id="cardNumber"
type="text"
placeholder="4242 4242 4242 4242"
className="bg-background border-border"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="expiry">Expiry Date</Label>
<Input
id="expiry"
type="text"
placeholder="MM/YY"
className="bg-background border-border"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="cvc">CVC</Label>
<Input
id="cvc"
type="text"
placeholder="123"
className="bg-background border-border"
/>
</div>
</div>
<div className="flex gap-3">
<Button
type="button"
variant="outline"
onClick={() => setCurrentStep('billing')}
className="bg-card border-border"
>
Back
</Button>
<Button
type="button"
className="glow-button bg-primary hover:bg-primary-glow text-white flex-1"
>
Pay Now
</Button>
</div>
</form>
)}
</Card>
)}
</div>
{/* Right side - Summary */}
<div className="lg:col-span-1">
<div className="sticky top-24">
<CheckoutSummary
serviceName={serviceName}
tier={plan.tier}
price={plan.price}
currency={plan.currency}
/>
</div>
</div>
</div>
</div>
</div>
)
}

10
app/checkout/page.tsx Normal file
View File

@ -0,0 +1,10 @@
import { Suspense } from 'react'
import { CheckoutContent } from './checkout-content'
export default function CheckoutPage() {
return (
<Suspense fallback={<div className="min-h-screen flex items-center justify-center">Loading...</div>}>
<CheckoutContent />
</Suspense>
)
}

16
app/dashboard/layout.tsx Normal file
View File

@ -0,0 +1,16 @@
import { DashboardSidebar } from '@/components/layouts/dashboard-sidebar'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex min-h-screen w-full flex-col lg:flex-row overflow-hidden">
<DashboardSidebar />
<main className="flex-1 flex flex-col h-full lg:h-screen overflow-y-auto bg-background custom-scrollbar">
{children}
</main>
</div>
)
}

106
app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,106 @@
import { StatsCard } from '@/components/dashboard/stats-card'
import { ProjectCard } from '@/components/dashboard/project-card'
import { SupportTicketItem } from '@/components/dashboard/support-ticket-item'
import { UpgradeCard } from '@/components/dashboard/upgrade-card'
import { mockProjects, mockSupportTickets, mockDashboardStats, mockCurrentUser } from '@/lib/mock-data'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
import { Rocket, Bot, Ticket, Calendar, Plus } from 'lucide-react'
export default function DashboardPage() {
const userProjects = mockProjects
const userTickets = mockSupportTickets.slice(0, 3)
const stats = mockDashboardStats
return (
<div className="flex-1 w-full max-w-7xl mx-auto p-4 lg:p-8 flex flex-col gap-8">
{/* Header */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-end gap-4 pb-2">
<div className="flex flex-col gap-1">
<h1 className="text-3xl lg:text-4xl font-black tracking-tight text-foreground">
Welcome back, {mockCurrentUser.name.split(' ')[0]}
</h1>
<p className="text-muted-foreground text-base font-medium">
Here&apos;s what&apos;s happening with your digital products today.
</p>
</div>
<Button className="glow-button bg-primary hover:bg-primary-glow text-white">
<Plus className="w-5 h-5 mr-2" />
Request New Service
</Button>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatsCard
value={stats.totalProjects}
label="Active Projects"
icon={Rocket}
trend="+1 this month"
/>
<StatsCard
value={stats.activeProjects}
label="Active Automations"
icon={Bot}
trend="All Systems Go"
trendColor="bg-slate-500/10 text-slate-400 border-slate-500/10"
/>
<StatsCard
value={stats.pendingTickets}
label="Pending Tickets"
icon={Ticket}
trend="Action Required"
trendColor="bg-orange-500/10 text-orange-400 border-orange-500/10"
iconColor="bg-orange-500/10 text-orange-400"
/>
<StatsCard
value="5 Days"
label="Next Launch"
icon={Calendar}
iconColor="bg-emerald-500/10 text-emerald-400"
/>
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Left Column - Projects */}
<div className="xl:col-span-2 flex flex-col gap-6">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold text-foreground">Your Projects</h2>
<Button variant="link" asChild className="text-primary">
<Link href="/dashboard/projects">View All </Link>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{userProjects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
</div>
{/* Right Column - Support & Upgrade */}
<div className="flex flex-col gap-6">
{/* Upgrade Card */}
<UpgradeCard />
{/* Support Tickets */}
<div className="bg-card border border-border rounded-2xl p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-foreground">Support Tickets</h2>
<Button variant="link" asChild className="text-primary">
<Link href="/dashboard/support">View All </Link>
</Button>
</div>
<div className="flex flex-col gap-2">
{userTickets.map((ticket) => (
<SupportTicketItem key={ticket.id} ticket={ticket} />
))}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -1,26 +1,206 @@
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap");
@import "tailwindcss";
: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-sans: "Inter", sans-serif;
--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);
/* ScaleSite Custom Colors */
--color-primary-default: #8B5CF6;
--color-primary-hover: #7c3aed;
--color-primary-glow: #A78BFA;
--color-background-light: #f6f6f8;
--color-background-dark: #0B0B0B;
--color-surface: #1E1E1E;
--color-surface-border: #2E2E2E;
--color-surface-darker: #0f172a;
--color-text-muted: #9ca3af;
--color-text-dim: #6b7280;
--color-off-white: #F3F4F6;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--radius: 0.625rem;
/* Light mode defaults */
--background: #f6f6f8;
--foreground: #1a1a1a;
--card: #ffffff;
--card-foreground: #1a1a1a;
--popover: #ffffff;
--popover-foreground: #1a1a1a;
--primary: #8B5CF6;
--primary-foreground: #ffffff;
--secondary: #f3f4f6;
--secondary-foreground: #1a1a1a;
--muted: #f3f4f6;
--muted-foreground: #6b7280;
--accent: #f3f4f6;
--accent-foreground: #1a1a1a;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #e5e7eb;
--input: #e5e7eb;
--ring: #8B5CF6;
}
.dark {
/* ScaleSite Dark Theme */
--background: #0B0B0B;
--foreground: #F3F4F6;
--card: #1E1E1E;
--card-foreground: #F3F4F6;
--popover: #1E1E1E;
--popover-foreground: #F3F4F6;
--primary: #8B5CF6;
--primary-foreground: #ffffff;
--secondary: #2E2E2E;
--secondary-foreground: #F3F4F6;
--muted: #1E1E1E;
--muted-foreground: #9ca3af;
--accent: #2E2E2E;
--accent-foreground: #F3F4F6;
--destructive: #ef4444;
--destructive-foreground: #ffffff;
--border: #2E2E2E;
--input: #2E2E2E;
--ring: #8B5CF6;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
@layer utilities {
/* Glass morphism effects */
.glass-nav {
background: rgba(11, 11, 11, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.glass-card {
background: rgba(30, 30, 30, 0.6);
backdrop-filter: blur(8px);
border: 1px solid rgba(139, 92, 246, 0.1);
}
/* Glow effects */
.glow-button {
box-shadow: 0 0 15px rgba(139, 92, 246, 0.4);
transition: all 0.3s ease;
}
.glow-button:hover {
box-shadow: 0 0 25px rgba(139, 92, 246, 0.6);
transform: translateY(-1px);
}
.glow-card {
transition: all 0.3s ease;
}
.glow-card:hover {
border-color: #8B5CF6;
box-shadow: 0 0 20px rgba(139, 92, 246, 0.15);
}
/* Text gradient */
.text-gradient {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary-glow;
}
/* Custom scrollbar for webkit browsers */
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #0B0B0B;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #2E2E2E;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #8B5CF6;
}
}
/* Global scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #0B0B0B;
}
::-webkit-scrollbar-thumb {
background: #2E2E2E;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #8B5CF6;
}
/* Accordion / Details summary styling */
details > summary {
list-style: none;
}
details > summary::-webkit-details-marker {
display: none;
}
details[open] .toggle-icon {
transform: rotate(45deg);
}

View File

@ -1,20 +1,16 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Inter } from "next/font/google";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "ScaleSite - AI & Web Solutions",
description: "Premium websites and intelligent automations starting at just 50€. Built for growth, designed for the future.",
};
export default function RootLayout({
@ -23,11 +19,10 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<html lang="en" className="dark">
<body className={`${inter.variable} font-sans antialiased`}>
{children}
<Toaster />
</body>
</html>
);

90
app/login/page.tsx Normal file
View File

@ -0,0 +1,90 @@
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
import { AuthLayout } from '@/components/auth/auth-layout'
import { SocialLoginButtons } from '@/components/auth/social-login-buttons'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
export default function LoginPage() {
return (
<AuthLayout
title="Welcome Back"
subtitle="Log in to access your dashboard and manage your projects."
>
<div className="flex flex-col gap-6">
{/* Social Login */}
<SocialLoginButtons />
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-background text-muted-foreground">
Or continue with email
</span>
</div>
</div>
{/* Login Form */}
<form className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
className="bg-card border-border"
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Label htmlFor="password">Password</Label>
<Link
href="#"
className="text-sm text-primary hover:underline"
>
Forgot password?
</Link>
</div>
<Input
id="password"
type="password"
placeholder="••••••••"
className="bg-card border-border"
/>
</div>
<Button
type="submit"
className="glow-button bg-primary hover:bg-primary-glow text-white w-full"
>
Log In
</Button>
</form>
{/* Sign up link */}
<p className="text-center text-sm text-muted-foreground">
Don&apos;t have an account?{' '}
<Link href="/register" className="text-primary hover:underline font-medium">
Sign up
</Link>
</p>
{/* Back to home */}
<div className="text-center">
<Link
href="/"
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
>
<ArrowLeft className="w-4 h-4" />
Back to home
</Link>
</div>
</div>
</AuthLayout>
)
}

View File

@ -1,65 +1,158 @@
import Image from "next/image";
import { MarketingLayout } from '@/components/layouts/marketing-layout'
import { HeroSection } from '@/components/marketing/hero-section'
import { ServiceCard } from '@/components/marketing/service-card'
import { PricingCard } from '@/components/marketing/pricing-card'
import { TestimonialCard } from '@/components/marketing/testimonial-card'
import { FAQItem } from '@/components/marketing/faq-item'
import { mockTestimonials, getAllFAQs, getPricingPlansByServiceType } from '@/lib/mock-data'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
import { Globe, Bot, Rocket, ArrowRight } from 'lucide-react'
const services = [
{
icon: Globe,
title: 'Web Design',
description: 'Modern, responsive websites built with the latest technologies. From landing pages to e-commerce platforms.',
},
{
icon: Bot,
title: 'AI Automation',
description: 'Intelligent chatbots and automation workflows that save time and enhance customer experience.',
},
{
icon: Rocket,
title: 'Growth Solutions',
description: 'Complete digital transformation packages combining web, AI, and marketing strategies.',
},
]
export default function Home() {
const webDesignPlans = getPricingPlansByServiceType('web-design')
const testimonials = mockTestimonials.slice(0, 4)
const faqs = getAllFAQs()
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
<MarketingLayout>
<HeroSection />
{/* Services Section */}
<section id="services" className="w-full py-20 bg-card/50">
<div className="w-full max-w-[1280px] mx-auto px-4 md:px-10">
<div className="flex flex-col gap-12">
<div className="text-center max-w-2xl mx-auto">
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-4">
Our Services
</h2>
<p className="text-muted-foreground text-lg">
Everything you need to scale your business in the digital age.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
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={16}
height={16}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{services.map((service, index) => (
<ServiceCard key={index} {...service} />
))}
</div>
</div>
</div>
</section>
{/* Pricing Preview Section */}
<section id="pricing" className="w-full py-20">
<div className="w-full max-w-[1280px] mx-auto px-4 md:px-10">
<div className="flex flex-col gap-12">
<div className="text-center max-w-2xl mx-auto">
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-4">
Simple, Transparent Pricing
</h2>
<p className="text-muted-foreground text-lg">
Choose the perfect plan for your needs. All plans include our core features.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-5xl mx-auto">
{webDesignPlans.map((plan) => (
<PricingCard
key={plan.id}
tier={plan.tier}
price={plan.price}
currency={plan.currency}
features={plan.features}
popular={plan.popular}
planId={plan.id}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] 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"
>
Documentation
</a>
))}
</div>
</main>
<div className="text-center">
<Button asChild size="lg" className="glow-button bg-primary hover:bg-primary-glow text-white">
<Link href="/services">View All Pricing</Link>
</Button>
</div>
);
</div>
</div>
</section>
{/* Testimonials Section */}
<section className="w-full py-20 bg-card/50">
<div className="w-full max-w-[1280px] mx-auto px-4 md:px-10">
<div className="flex flex-col gap-12">
<div className="text-center max-w-2xl mx-auto">
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-4">
Trusted by Growing Businesses
</h2>
<p className="text-muted-foreground text-lg">
See what our clients have to say about working with us.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{testimonials.map((testimonial) => (
<TestimonialCard key={testimonial.id} {...testimonial} />
))}
</div>
</div>
</div>
</section>
{/* FAQ Section */}
<section className="w-full py-20">
<div className="w-full max-w-[1280px] mx-auto px-4 md:px-10">
<div className="flex flex-col gap-8 max-w-3xl mx-auto">
<div className="text-center">
<h2 className="text-3xl md:text-4xl font-bold text-foreground mb-4">
Frequently Asked Questions
</h2>
<p className="text-muted-foreground text-lg">
Common questions about our services and process.
</p>
</div>
<div className="flex flex-col gap-4">
{faqs.map((faq) => (
<FAQItem key={faq.id} faq={faq} />
))}
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="w-full py-20 bg-gradient-to-b from-background to-card border-t border-border">
<div className="w-full max-w-[1280px] mx-auto px-4 md:px-10 text-center">
<h2 className="text-3xl md:text-5xl font-bold text-foreground mb-6">
Ready to upgrade your business?
</h2>
<p className="text-muted-foreground mb-8 max-w-2xl mx-auto">
Join hundreds of other businesses saving time and making money with ScaleSite's affordable tech solutions.
</p>
<Button size="lg" className="glow-button bg-primary hover:bg-primary-glow text-white text-lg font-bold h-14 px-10">
Get Started Now
<ArrowRight className="w-5 h-5 ml-2" />
</Button>
</div>
</section>
</MarketingLayout>
)
}

110
app/register/page.tsx Normal file
View File

@ -0,0 +1,110 @@
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
import { AuthLayout } from '@/components/auth/auth-layout'
import { SocialLoginButtons } from '@/components/auth/social-login-buttons'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
export default function RegisterPage() {
return (
<AuthLayout
title="Create Account"
subtitle="Start scaling your business with AI & Web solutions today."
>
<div className="flex flex-col gap-6">
{/* Social Login */}
<SocialLoginButtons />
{/* Divider */}
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-border" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-background text-muted-foreground">
Or continue with email
</span>
</div>
</div>
{/* Register Form */}
<form className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="name">Full Name</Label>
<Input
id="name"
type="text"
placeholder="John Doe"
className="bg-card border-border"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
className="bg-card border-border"
/>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
className="bg-card border-border"
/>
</div>
<div className="flex items-start gap-2">
<input
type="checkbox"
id="terms"
className="mt-1 w-4 h-4 rounded border-border"
/>
<Label htmlFor="terms" className="text-sm text-muted-foreground">
I agree to the{' '}
<Link href="#" className="text-primary hover:underline">
Terms of Service
</Link>{' '}
and{' '}
<Link href="#" className="text-primary hover:underline">
Privacy Policy
</Link>
</Label>
</div>
<Button
type="submit"
className="glow-button bg-primary hover:bg-primary-glow text-white w-full"
>
Create Account
</Button>
</form>
{/* Log in link */}
<p className="text-center text-sm text-muted-foreground">
Already have an account?{' '}
<Link href="/login" className="text-primary hover:underline font-medium">
Log in
</Link>
</p>
{/* Back to home */}
<div className="text-center">
<Link
href="/"
className="text-sm text-muted-foreground hover:text-foreground inline-flex items-center gap-1"
>
<ArrowLeft className="w-4 h-4" />
Back to home
</Link>
</div>
</div>
</AuthLayout>
)
}

82
app/services/page.tsx Normal file
View File

@ -0,0 +1,82 @@
'use client'
import { useState, useMemo } from 'react'
import { MarketingLayout } from '@/components/layouts/marketing-layout'
import { ServiceTypeToggle } from '@/components/services/service-type-toggle'
import { ProgressStepper } from '@/components/services/progress-stepper'
import { PricingCard } from '@/components/marketing/pricing-card'
import { TrustBadges } from '@/components/services/trust-badges'
import { getPricingPlansByServiceType, type ServiceType } from '@/lib/mock-data'
import { Button } from '@/components/ui/button'
import Link from 'next/link'
export default function ServicesPage() {
const [serviceType, setServiceType] = useState<ServiceType>('web-design')
const pricingPlans = useMemo(
() => getPricingPlansByServiceType(serviceType),
[serviceType]
)
return (
<MarketingLayout>
<section className="w-full py-20 min-h-screen">
<div className="w-full max-w-[1280px] mx-auto px-4 md:px-10">
{/* Progress Stepper */}
<ProgressStepper
currentStep={2}
totalSteps={4}
stepLabel="Choose Your Plan"
/>
{/* Service Type Toggle */}
<ServiceTypeToggle
defaultValue="web-design"
onChange={setServiceType}
/>
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-12">
<h1 className="text-3xl md:text-5xl font-bold text-foreground mb-4">
{serviceType === 'web-design' ? 'Web Design Packages' : 'AI Automation Packages'}
</h1>
<p className="text-muted-foreground text-lg">
{serviceType === 'web-design'
? 'Professional websites built to convert visitors into customers. Choose the package that fits your needs.'
: 'Intelligent automation solutions that save time and boost efficiency. Start automating today.'}
</p>
</div>
{/* Pricing Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 max-w-6xl mx-auto mb-12">
{pricingPlans.map((plan) => (
<PricingCard
key={plan.id}
tier={plan.tier}
price={plan.price}
currency={plan.currency}
features={plan.features}
popular={plan.popular}
planId={plan.id}
/>
))}
</div>
{/* Trust Badges */}
<TrustBadges />
{/* Back Button */}
<div className="flex justify-center mt-12">
<Button
variant="outline"
asChild
className="bg-card border border-border hover:border-primary text-foreground"
>
<Link href="/"> Back to Home</Link>
</Button>
</div>
</div>
</section>
</MarketingLayout>
)
}

23
components.json Normal file
View File

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

View File

@ -0,0 +1,48 @@
import { Rocket } from 'lucide-react'
import { ReactNode } from 'react'
interface AuthLayoutProps {
children: ReactNode
title: string
subtitle: string
}
export function AuthLayout({ children, title, subtitle }: AuthLayoutProps) {
return (
<div className="min-h-screen flex flex-col lg:flex-row">
{/* Left side - Hero */}
<div className="lg:w-1/2 relative overflow-hidden">
<div
className="absolute inset-0 flex flex-col justify-center items-center p-12 bg-black/50"
style={{
backgroundImage: 'url("https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1200")',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<div className="relative z-10 max-w-md text-center">
<div className="inline-flex items-center justify-center gap-3 mb-6">
<div className="size-12 flex items-center justify-center rounded-xl bg-gradient-to-br from-primary to-primary-glow text-white shadow-lg">
<Rocket className="w-7 h-7" />
</div>
<h1 className="text-white text-3xl font-bold">ScaleSite</h1>
</div>
<h2 className="text-white text-2xl md:text-3xl font-bold mb-4">
{title}
</h2>
<p className="text-gray-300 text-lg">
{subtitle}
</p>
</div>
</div>
</div>
{/* Right side - Form */}
<div className="lg:w-1/2 flex items-center justify-center p-8 lg:p-12 bg-background">
<div className="w-full max-w-md">
{children}
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,44 @@
'use client'
import { Button } from '@/components/ui/button'
export function SocialLoginButtons() {
return (
<div className="flex flex-col gap-3 mb-6">
<Button
variant="outline"
className="w-full bg-card border-border hover:border-primary text-foreground gap-3"
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
/>
<path
fill="currentColor"
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
/>
<path
fill="currentColor"
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
/>
<path
fill="currentColor"
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
/>
</svg>
Continue with Google
</Button>
<Button
variant="outline"
className="w-full bg-card border-border hover:border-primary text-foreground gap-3"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
Continue with GitHub
</Button>
</div>
)
}

View File

@ -0,0 +1,89 @@
'use client'
import { Download } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import type { Invoice } from '@/lib/types'
interface InvoiceTableProps {
invoices: Invoice[]
}
const statusColors: Record<Invoice['status'], string> = {
paid: 'bg-emerald-500/10 text-emerald-400',
pending: 'bg-yellow-500/10 text-yellow-400',
refunded: 'bg-red-500/10 text-red-400',
cancelled: 'bg-gray-500/10 text-gray-400',
}
export function InvoiceTable({ invoices }: InvoiceTableProps) {
return (
<Card className="bg-card border-border">
<CardHeader>
<CardTitle>Invoice History</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-4 text-sm font-semibold text-foreground">
Description
</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-foreground">
Date
</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-foreground">
Amount
</th>
<th className="text-left py-3 px-4 text-sm font-semibold text-foreground">
Status
</th>
<th className="text-right py-3 px-4 text-sm font-semibold text-foreground">
Download
</th>
</tr>
</thead>
<tbody>
{invoices.map((invoice) => (
<tr key={invoice.id} className="border-b border-border hover:bg-white/5">
<td className="py-4 px-4">
<p className="font-medium text-foreground">{invoice.description}</p>
{invoice.projectId && (
<p className="text-xs text-muted-foreground mt-1">
Project: {invoice.projectId}
</p>
)}
</td>
<td className="py-4 px-4 text-sm text-muted-foreground">
{new Date(invoice.date).toLocaleDateString()}
</td>
<td className="py-4 px-4">
<p className="font-semibold text-foreground">
{invoice.total} {invoice.currency}
</p>
</td>
<td className="py-4 px-4">
<Badge className={statusColors[invoice.status]}>
{invoice.status}
</Badge>
</td>
<td className="py-4 px-4 text-right">
{invoice.downloadUrl && (
<Button variant="ghost" size="sm" asChild>
<a href={invoice.downloadUrl} download>
<Download className="w-4 h-4" />
</a>
</Button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,70 @@
'use client'
import { CreditCard, Wallet, Edit, Trash2, Plus } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import type { PaymentMethod } from '@/lib/types'
interface PaymentMethodListProps {
paymentMethods: PaymentMethod[]
}
export function PaymentMethodList({ paymentMethods }: PaymentMethodListProps) {
return (
<Card className="bg-card border-border">
<CardHeader>
<CardTitle>Payment Methods</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-4">
{paymentMethods.map((method) => (
<div
key={method.id}
className="flex items-center justify-between p-4 rounded-xl bg-background border border-border"
>
<div className="flex items-center gap-4">
{/* Icon */}
<div className="p-3 bg-card border border-border rounded-lg">
{method.type === 'card' ? (
<CreditCard className="w-6 h-6 text-foreground" />
) : (
<Wallet className="w-6 h-6 text-foreground" />
)}
</div>
{/* Details */}
<div>
<div className="flex items-center gap-2">
<p className="font-semibold text-foreground">
{method.type === 'card'
? `${method.brand?.toUpperCase()} •••• ${method.last4}`
: `PayPal - ${method.email}`}
</p>
{method.isDefault && (
<Badge className="bg-primary/10 text-primary">Default</Badge>
)}
</div>
</div>
</div>
{/* Actions */}
<div className="flex gap-2">
<Button variant="ghost" size="sm">
<Edit className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" className="text-red-400 hover:text-red-300">
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
{/* Add New */}
<Button variant="outline" className="w-full border-border">
<Plus className="w-4 h-4 mr-2" />
Add Payment Method
</Button>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,60 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import type { Subscription } from '@/lib/types'
interface SubscriptionCardProps {
subscription: Subscription
}
export function SubscriptionCard({ subscription }: SubscriptionCardProps) {
return (
<Card className="bg-card border-border">
<CardHeader>
<CardTitle>Current Subscription</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-6">
{/* Plan Details */}
<div className="flex items-start justify-between">
<div>
<h3 className="text-2xl font-bold text-foreground">Growth Plan</h3>
<p className="text-muted-foreground">AI Automation & Web Design</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-foreground">150</p>
<p className="text-muted-foreground text-sm">/month</p>
</div>
</div>
{/* Status Badge */}
<div className="flex items-center gap-2">
<span
className={`px-3 py-1 rounded-full text-sm font-semibold ${
subscription.status === 'active'
? 'bg-emerald-500/10 text-emerald-400'
: 'bg-red-500/10 text-red-400'
}`}
>
{subscription.status === 'active' ? 'Active' : subscription.status}
</span>
{subscription.status === 'active' && (
<span className="text-muted-foreground text-sm">
Renews on {new Date(subscription.currentPeriodEnd).toLocaleDateString()}
</span>
)}
</div>
{/* Actions */}
<div className="flex gap-3">
<Button variant="outline" className="flex-1 border-border">
Manage Subscription
</Button>
{subscription.status === 'active' && !subscription.cancelAtPeriodEnd && (
<Button variant="ghost" className="text-muted-foreground hover:text-foreground">
Cancel
</Button>
)}
</div>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,54 @@
import Link from 'next/link'
interface CheckoutStepsProps {
currentStep: 'account' | 'billing' | 'payment'
}
const steps = [
{ id: 'account', label: 'Account' },
{ id: 'billing', label: 'Billing' },
{ id: 'payment', label: 'Payment' },
]
export function CheckoutSteps({ currentStep }: CheckoutStepsProps) {
const currentIndex = steps.findIndex((s) => s.id === currentStep)
return (
<div className="flex items-center justify-center mb-12">
<div className="flex items-center gap-4">
{steps.map((step, index) => (
<div key={step.id} className="flex items-center">
{index > 0 && (
<div
className={`w-12 h-0.5 ${
index <= currentIndex ? 'bg-primary' : 'bg-border'
}`}
/>
)}
<Link
href={index <= currentIndex ? `/checkout?step=${step.id}` : '#'}
className={`flex items-center justify-center w-10 h-10 rounded-full text-sm font-bold transition-colors ${
index === currentIndex
? 'bg-primary text-white'
: index < currentIndex
? 'bg-primary/20 text-primary'
: 'bg-card border border-border text-muted-foreground'
}`}
>
{index < currentIndex ? '✓' : index + 1}
</Link>
<span
className={`ml-2 text-sm font-medium ${
index === currentIndex
? 'text-foreground'
: 'text-muted-foreground'
}`}
>
{step.label}
</span>
</div>
))}
</div>
</div>
)
}

View File

@ -0,0 +1,79 @@
import { Rocket, Lock, ShieldCheck } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Button } from '@/components/ui/button'
interface CheckoutSummaryProps {
serviceName: string
tier: string
price: number
currency: string
}
export function CheckoutSummary({ serviceName, tier, price, currency }: CheckoutSummaryProps) {
const vat = Math.round(price * 0.2)
const total = price + vat
return (
<Card className="bg-card border-border">
<CardHeader>
<CardTitle>Order Summary</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-6">
{/* Selected Service */}
<div className="flex items-start gap-4 pb-4 border-b border-border">
<div className="p-3 bg-primary/10 text-primary rounded-lg">
<Rocket className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-foreground">{serviceName}</h4>
<p className="text-sm text-muted-foreground capitalize">{tier} Tier</p>
</div>
<p className="font-bold text-foreground">
{price} {currency}
</p>
</div>
{/* Price Breakdown */}
<div className="flex flex-col gap-3">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="text-foreground">
{price} {currency}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">VAT (20%)</span>
<span className="text-foreground">
{vat} {currency}
</span>
</div>
<Separator />
<div className="flex justify-between">
<span className="font-bold text-foreground">Total</span>
<span className="font-bold text-xl text-foreground">
{total} {currency}
</span>
</div>
</div>
{/* Complete Purchase Button */}
<Button className="glow-button bg-primary hover:bg-primary-glow text-white w-full text-lg font-bold h-12">
Complete Purchase
</Button>
{/* Trust Badges */}
<div className="flex items-center justify-center gap-4 text-xs text-muted-foreground">
<div className="flex items-center gap-1">
<Lock className="w-4 h-4" />
<span>Secure Payment</span>
</div>
<div className="flex items-center gap-1">
<ShieldCheck className="w-4 h-4" />
<span>SSL Encrypted</span>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,59 @@
'use client'
import { CreditCard, Link as LinkIcon, Wallet } from 'lucide-react'
import { Card } from '@/components/ui/card'
interface PaymentMethodCardProps {
id: string
icon: string
label: string
description: string
selected: boolean
onSelect: () => void
}
const iconMap: Record<string, React.ComponentType<{ className?: string }>> = {
card: CreditCard,
paypal: Wallet,
'stripe-link': LinkIcon,
}
export function PaymentMethodCard({
id,
icon,
label,
description,
selected,
onSelect,
}: PaymentMethodCardProps) {
const IconComponent = iconMap[icon] || CreditCard
return (
<Card
onClick={onSelect}
className={`cursor-pointer transition-all ${
selected
? 'border-primary bg-primary/5 ring-2 ring-primary/20'
: 'border-border bg-card hover:border-primary/50'
}`}
>
<div className="p-4 flex items-center gap-4">
<div
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
selected ? 'border-primary' : 'border-border'
}`}
>
{selected && (
<div className="w-3 h-3 rounded-full bg-primary" />
)}
</div>
<div className="p-2 bg-card border border-border rounded">
<IconComponent className="w-5 h-5" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-foreground">{label}</h4>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
</div>
</Card>
)
}

View File

@ -0,0 +1,82 @@
'use client'
import { ArrowRight, Circle, CircleCheck } from 'lucide-react'
import Link from 'next/link'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Progress } from '@/components/ui/progress'
import type { Project } from '@/lib/types'
interface ProjectCardProps {
project: Project
}
const statusColors: Record<Project['status'], string> = {
discovery: 'bg-blue-500/10 text-blue-400',
design: 'bg-purple-500/10 text-purple-400',
development: 'bg-yellow-500/10 text-yellow-400',
content: 'bg-orange-500/10 text-orange-400',
testing: 'bg-cyan-500/10 text-cyan-400',
completed: 'bg-emerald-500/10 text-emerald-400',
}
export function ProjectCard({ project }: ProjectCardProps) {
return (
<Link href={`/dashboard/projects/${project.id}`}>
<Card className="glow-card bg-card border-border hover:border-primary transition-all duration-300">
<CardContent className="p-6">
{/* Thumbnail */}
<div
className="w-full h-40 rounded-lg mb-4 bg-cover bg-center"
style={{ backgroundImage: `url(${project.thumbnail})` }}
/>
{/* Title and Status */}
<div className="flex items-start justify-between mb-3">
<div className="flex-1">
<h3 className="font-bold text-foreground text-lg mb-1">{project.title}</h3>
<p className="text-sm text-muted-foreground line-clamp-2">
{project.description}
</p>
</div>
<Badge className={`ml-2 ${statusColors[project.status]}`}>
{project.status}
</Badge>
</div>
{/* Progress */}
<div className="mb-4">
<div className="flex justify-between text-sm mb-2">
<span className="text-muted-foreground">Progress</span>
<span className="text-foreground font-medium">{project.progress}%</span>
</div>
<Progress value={project.progress} className="h-2" />
</div>
{/* Stages */}
<div className="flex items-center gap-2 text-xs text-muted-foreground">
{project.stages.map((stage, index) => (
<div key={index} className="flex items-center gap-1">
{stage.completed ? (
<CircleCheck className="w-4 h-4 text-primary" />
) : (
<Circle className="w-4 h-4" />
)}
<span className={stage.completed ? 'text-foreground' : ''}>
{stage.name}
</span>
{index < project.stages.length - 1 && <span></span>}
</div>
))}
</div>
{/* Updated */}
<div className="mt-4 pt-4 border-t border-border flex items-center justify-between text-xs text-muted-foreground">
<span>Updated {new Date(project.updatedAt).toLocaleDateString()}</span>
<ArrowRight className="w-4 h-4" />
</div>
</CardContent>
</Card>
</Link>
)
}

View File

@ -0,0 +1,40 @@
import { Rocket, Bot, Ticket, Calendar, LucideIcon, Plus } from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
interface StatsCardProps {
value: string | number
label: string
icon: LucideIcon
iconColor?: string
trend?: string
trendColor?: string
}
export function StatsCard({
value,
label,
icon: Icon,
iconColor = 'bg-primary/10 text-primary',
trend,
trendColor = 'bg-emerald-500/10 text-emerald-400 border-emerald-500/10',
}: StatsCardProps) {
return (
<Card className="bg-card border-border shadow-sm hover:shadow-md transition-shadow rounded-2xl">
<CardContent className="p-5">
<div className="flex items-start justify-between mb-4">
<div className={`p-2.5 ${iconColor} rounded-xl`}>
<Icon className="w-6 h-6" />
</div>
{trend && (
<Badge className={`text-xs font-bold px-2.5 py-1 rounded-full border ${trendColor}`}>
{trend}
</Badge>
)}
</div>
<p className="text-muted-foreground text-sm font-medium">{label}</p>
<p className="text-2xl font-bold text-foreground mt-1">{value}</p>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,71 @@
'use client'
import { Circle, CircleCheck } from 'lucide-react'
import Link from 'next/link'
import type { SupportTicket } from '@/lib/types'
import { Badge } from '@/components/ui/badge'
const statusIcons: Record<SupportTicket['status'], React.ComponentType<{ className?: string }>> = {
open: Circle,
'in-progress': Circle,
resolved: CircleCheck,
closed: CircleCheck,
}
const statusColors: Record<SupportTicket['status'], string> = {
open: 'bg-emerald-500/10 text-emerald-400',
'in-progress': 'bg-blue-500/10 text-blue-400',
resolved: 'bg-gray-500/10 text-gray-400',
closed: 'bg-gray-500/10 text-gray-400',
}
const priorityColors: Record<SupportTicket['priority'], string> = {
low: '',
medium: 'bg-orange-500/10 text-orange-400',
high: 'bg-red-500/10 text-red-400',
}
interface SupportTicketItemProps {
ticket: SupportTicket
}
export function SupportTicketItem({ ticket }: SupportTicketItemProps) {
return (
<Link
href={`/dashboard/support/${ticket.id}`}
className="flex items-start gap-4 p-4 rounded-xl hover:bg-white/5 transition-colors group"
>
{/* Status indicator */}
<div className="flex-shrink-0 mt-1">
{ticket.status === 'open' || ticket.status === 'in-progress' ? (
<Circle className={`w-5 h-5 ${ticket.status === 'open' ? 'text-emerald-400' : 'text-blue-400'}`} />
) : (
<CircleCheck className="w-5 h-5 text-gray-400" />
)}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<h4 className="font-semibold text-foreground truncate">{ticket.subject}</h4>
{ticket.priority === 'high' && (
<Badge className={`${priorityColors[ticket.priority]} text-xs`}>
High Priority
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground line-clamp-2">
{ticket.description}
</p>
</div>
{/* Ticket number */}
<div className="flex-shrink-0 text-right">
<div className="text-xs text-muted-foreground">#{ticket.id}</div>
<Badge className={`mt-1 ${statusColors[ticket.status]}`}>
{ticket.status}
</Badge>
</div>
</Link>
)
}

View File

@ -0,0 +1,38 @@
import { Award, CircleCheck } from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
export function UpgradeCard() {
return (
<Card className="bg-gradient-to-br from-primary to-violet-600 border-0 text-white">
<CardContent className="p-6">
<div className="flex items-center gap-3 mb-4">
<Award className="w-8 h-8" />
<div>
<h3 className="font-bold text-xl">Upgrade to Premium</h3>
<p className="text-white/80 text-sm">Unlock all features</p>
</div>
</div>
<ul className="space-y-2 mb-6">
<li className="flex items-center gap-2 text-sm">
<CircleCheck className="w-4 h-4" />
<span>Priority Support</span>
</li>
<li className="flex items-center gap-2 text-sm">
<CircleCheck className="w-4 h-4" />
<span>Unlimited Revisions</span>
</li>
<li className="flex items-center gap-2 text-sm">
<CircleCheck className="w-4 h-4" />
<span>Dedicated Account Manager</span>
</li>
</ul>
<Button className="w-full bg-white text-primary hover:bg-white/90 font-bold">
Upgrade Now
</Button>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,122 @@
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { mockCurrentUser, getActiveProjects } from '@/lib/mock-data'
import { Rocket, FolderOpen, Bot, Headphones, Receipt, Settings, LogOut, LayoutDashboard } from 'lucide-react'
const navItems = [
{
href: '/dashboard',
label: 'Dashboard',
icon: LayoutDashboard,
filled: true,
},
{
href: '/dashboard/projects',
label: 'My Projects',
icon: FolderOpen,
badge: getActiveProjects().length,
},
{
href: '/dashboard/automations',
label: 'AI Automations',
icon: Bot,
},
{
href: '/dashboard/support',
label: 'Support',
icon: Headphones,
},
{
href: '/billing',
label: 'Billing',
icon: Receipt,
},
{
href: '/dashboard/settings',
label: 'Settings',
icon: Settings,
},
]
export function DashboardSidebar() {
const pathname = usePathname()
return (
<aside className="hidden lg:flex flex-col w-72 bg-card border-r border-border h-screen sticky top-0 shrink-0">
<div className="flex flex-col h-full p-4">
{/* Logo */}
<div className="flex items-center gap-3 px-2 py-4 mb-8">
<div className="bg-gradient-to-br from-primary to-violet-600 p-2.5 rounded-xl text-white flex items-center justify-center shadow-lg shadow-primary/20">
<Rocket className="w-6 h-6" />
</div>
<div className="flex flex-col">
<h1 className="text-foreground text-lg font-bold leading-none tracking-tight">
ScaleSite
</h1>
<p className="text-muted-foreground text-xs font-medium mt-1">Client Portal</p>
</div>
</div>
{/* Navigation */}
<nav className="flex flex-col gap-1.5 flex-1">
{navItems.map((item) => {
const isActive = pathname === item.href || pathname?.startsWith(item.href + '/')
const Icon = item.icon
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 px-4 py-3 rounded-xl transition-all group ${
isActive
? 'bg-primary text-white shadow-lg shadow-primary/20'
: 'text-muted-foreground hover:bg-white/5 hover:text-foreground'
}`}
>
<Icon className="w-5 h-5" />
<p className={`text-sm ${isActive ? 'font-semibold' : 'font-medium'}`}>
{item.label}
</p>
{item.badge && (
<Badge
variant="secondary"
className={`ml-auto text-[10px] font-bold px-2 py-0.5 rounded-full ${
isActive
? 'bg-white/20 text-white group-hover:bg-white group-hover:text-primary'
: 'bg-primary/20 text-primary'
}`}
>
{item.badge}
</Badge>
)}
</Link>
)
})}
</nav>
{/* User profile */}
<div className="mt-auto border-t border-border pt-4">
<button className="flex items-center w-full gap-3 px-4 py-3 rounded-xl hover:bg-white/5 transition-colors text-left group">
<Avatar className="size-10 border border-border group-hover:border-primary transition-colors">
<AvatarImage src={mockCurrentUser.avatar} alt={mockCurrentUser.name} />
<AvatarFallback>{mockCurrentUser.name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="flex flex-col min-w-0">
<p className="text-foreground text-sm font-semibold truncate">
{mockCurrentUser.name}
</p>
<p className="text-muted-foreground text-xs truncate group-hover:text-foreground">
{mockCurrentUser.email}
</p>
</div>
<LogOut className="w-5 h-5 text-muted-foreground ml-auto group-hover:text-foreground transition-colors" />
</button>
</div>
</div>
</aside>
)
}

View File

@ -0,0 +1,14 @@
import { SiteHeader } from './site-header'
import { SiteFooter } from './site-footer'
export function MarketingLayout({ children }: { children: React.ReactNode }) {
return (
<div className="relative flex h-auto min-h-screen w-full flex-col overflow-x-hidden">
<SiteHeader />
<main className="flex-1 flex flex-col pt-16">
{children}
</main>
<SiteFooter />
</div>
)
}

View File

@ -0,0 +1,131 @@
import Link from 'next/link'
import { Globe, Mail } from 'lucide-react'
export function SiteFooter() {
return (
<footer className="bg-background border-t border-border pt-16 pb-8 mt-auto">
<div className="w-full max-w-[1280px] mx-auto px-4 md:px-10">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 mb-12">
{/* Brand */}
<div className="col-span-2 md:col-span-1">
<div className="flex items-center gap-3 mb-4">
<div className="size-6 flex items-center justify-center rounded bg-primary text-white">
<Globe className="w-4 h-4" />
</div>
<span className="text-lg font-bold">ScaleSite</span>
</div>
<p className="text-muted-foreground text-sm leading-relaxed">
Making enterprise-grade web and AI technology accessible to everyone.
</p>
</div>
{/* Platform */}
<div>
<h4 className="font-bold mb-4">Platform</h4>
<ul className="flex flex-col gap-2">
<li>
<Link
href="/#services"
className="text-muted-foreground hover:text-primary text-sm transition-colors"
>
Services
</Link>
</li>
<li>
<Link
href="/services"
className="text-muted-foreground hover:text-primary text-sm transition-colors"
>
Pricing
</Link>
</li>
<li>
<Link
href="/login"
className="text-muted-foreground hover:text-primary text-sm transition-colors"
>
Dashboard Login
</Link>
</li>
</ul>
</div>
{/* Company */}
<div>
<h4 className="font-bold mb-4">Company</h4>
<ul className="flex flex-col gap-2">
<li>
<Link
href="/#about"
className="text-muted-foreground hover:text-primary text-sm transition-colors"
>
About Us
</Link>
</li>
<li>
<a
href="#"
className="text-muted-foreground hover:text-primary text-sm transition-colors"
>
Careers
</a>
</li>
<li>
<a
href="#"
className="text-muted-foreground hover:text-primary text-sm transition-colors"
>
Contact
</a>
</li>
</ul>
</div>
{/* Legal */}
<div>
<h4 className="font-bold mb-4">Legal</h4>
<ul className="flex flex-col gap-2">
<li>
<a
href="#"
className="text-muted-foreground hover:text-primary text-sm transition-colors"
>
Privacy Policy
</a>
</li>
<li>
<a
href="#"
className="text-muted-foreground hover:text-primary text-sm transition-colors"
>
Terms of Service
</a>
</li>
</ul>
</div>
</div>
{/* Bottom bar */}
<div className="border-t border-border pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
<p className="text-muted-foreground text-sm">© 2024 ScaleSite. All rights reserved.</p>
<div className="flex gap-4">
<a
href="#"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Website"
>
<Globe className="w-5 h-5" />
</a>
<a
href="mailto:hello@scalesite.com"
className="text-muted-foreground hover:text-foreground transition-colors"
aria-label="Email"
>
<Mail className="w-5 h-5" />
</a>
</div>
</div>
</div>
</footer>
)
}

View File

@ -0,0 +1,106 @@
'use client'
import Link from 'next/link'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Rocket, Menu, X } from 'lucide-react'
export function SiteHeader() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
return (
<header className="fixed top-0 left-0 right-0 z-50 glass-nav border-b border-border">
<div className="flex justify-center w-full">
<div className="w-full max-w-[1280px] px-4 md:px-10 py-3 flex items-center justify-between">
{/* Logo */}
<Link href="/" className="flex items-center gap-3">
<div className="size-8 flex items-center justify-center rounded bg-gradient-to-br from-primary to-primary-glow text-white">
<Rocket className="w-5 h-5" />
</div>
<h2 className="text-foreground text-lg font-bold leading-tight tracking-tight">
ScaleSite
</h2>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
<Link
href="/#services"
className="text-muted-foreground hover:text-foreground hover:text-primary transition-colors text-sm font-medium"
>
Services
</Link>
<Link
href="/services"
className="text-muted-foreground hover:text-foreground hover:text-primary transition-colors text-sm font-medium"
>
Pricing
</Link>
<Link
href="/#about"
className="text-muted-foreground hover:text-foreground hover:text-primary transition-colors text-sm font-medium"
>
About
</Link>
</nav>
{/* Right side */}
<div className="flex items-center gap-4">
<Button
asChild
className="hidden sm:flex glow-button bg-primary hover:bg-primary-glow text-white text-sm font-bold h-9 px-5 rounded-lg"
>
<Link href="/login">Client Login</Link>
</Button>
{/* Mobile menu button */}
<button
className="md:hidden text-foreground"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label="Toggle menu"
>
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
</div>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="md:hidden bg-card border-b border-border">
<nav className="flex flex-col px-4 py-4 gap-4">
<Link
href="/#services"
className="text-muted-foreground hover:text-foreground hover:text-primary transition-colors text-sm font-medium"
onClick={() => setMobileMenuOpen(false)}
>
Services
</Link>
<Link
href="/services"
className="text-muted-foreground hover:text-foreground hover:text-primary transition-colors text-sm font-medium"
onClick={() => setMobileMenuOpen(false)}
>
Pricing
</Link>
<Link
href="/#about"
className="text-muted-foreground hover:text-foreground hover:text-primary transition-colors text-sm font-medium"
onClick={() => setMobileMenuOpen(false)}
>
About
</Link>
<Button
asChild
className="glow-button bg-primary hover:bg-primary-glow text-white text-sm font-bold h-9 px-5 rounded-lg w-full"
>
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
Client Login
</Link>
</Button>
</nav>
</div>
)}
</header>
)
}

View File

@ -0,0 +1,21 @@
import { ChevronDown } from 'lucide-react'
import { FAQ } from '@/lib/types'
import { Card, CardContent } from '@/components/ui/card'
interface FAQItemProps {
faq: FAQ
}
export function FAQItem({ faq }: FAQItemProps) {
return (
<details className="group bg-card rounded-xl border border-border overflow-hidden transition-all duration-300 open:border-primary/50 open:ring-1 open:ring-primary/20">
<summary className="flex items-center justify-between p-6 cursor-pointer select-none hover:bg-white/5 transition-colors">
<span className="text-foreground font-semibold text-lg">{faq.question}</span>
<ChevronDown className="w-6 h-6 text-primary transition-transform duration-300 group-open:rotate-180" />
</summary>
<div className="px-6 pb-6 text-muted-foreground leading-relaxed border-t border-border/50 pt-4">
{faq.answer}
</div>
</details>
)
}

View File

@ -0,0 +1,57 @@
import { Button } from '@/components/ui/button'
export function HeroSection() {
return (
<section className="relative w-full flex justify-center py-12 md:py-20 lg:py-28 overflow-hidden">
{/* Background glow effect */}
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[600px] h-[600px] bg-primary/20 rounded-full blur-[120px] -z-10 pointer-events-none" />
<div className="w-full max-w-[1280px] px-4 md:px-10 flex flex-col items-center z-10">
<div className="@container w-full max-w-[1000px]">
<div className="flex flex-col gap-8 items-center text-center">
{/* Hero image with gradient overlay */}
<div className="w-full h-[300px] md:h-[420px] rounded-xl overflow-hidden relative mb-4 shadow-2xl border border-border group">
<div
className="absolute inset-0 flex flex-col justify-center items-center p-6 bg-black/40 backdrop-blur-[2px]"
style={{
backgroundImage: 'linear-gradient(rgba(11, 11, 11, 0.3) 0%, rgba(11, 11, 11, 0.9) 100%), url("https://images.unsplash.com/photo-1451187580459-43490279c0fa?w=1200")',
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<h1 className="text-foreground text-4xl md:text-6xl lg:text-7xl font-black leading-tight tracking-tight mb-4 drop-shadow-lg">
Scale Your Business <br />
<span className="text-gradient">
with AI & Web
</span>
</h1>
<h2 className="text-gray-300 text-base md:text-xl font-normal leading-relaxed max-w-2xl drop-shadow-md">
Premium websites and intelligent automations starting at just 50.{' '}
<br className="hidden md:block" />
Built for growth, designed for the future.
</h2>
</div>
</div>
{/* CTA buttons */}
<div className="flex flex-wrap gap-4 justify-center mt-2">
<Button
size="lg"
className="glow-button bg-primary hover:bg-primary-glow text-white text-base font-bold h-12 px-8 rounded-lg"
>
Start Scaling
</Button>
<Button
variant="outline"
size="lg"
className="bg-card border border-border hover:border-gray-500 text-foreground text-base font-bold h-12 px-8 rounded-lg"
>
View Portfolio
</Button>
</div>
</div>
</div>
</div>
</section>
)
}

View File

@ -0,0 +1,69 @@
'use client'
import { Check } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import Link from 'next/link'
interface PricingCardProps {
tier: string
price: number
currency: string
features: string[]
popular?: boolean
planId: string
}
export function PricingCard({ tier, price, currency, features, popular, planId }: PricingCardProps) {
return (
<Card
className={`relative bg-card border-border rounded-xl transition-all duration-300 ${
popular ? 'border-primary shadow-lg shadow-primary/20 scale-105' : 'hover:border-primary/50'
}`}
>
{popular && (
<Badge className="absolute -top-3 left-1/2 -translate-x-1/2 bg-primary text-white">
Most Popular
</Badge>
)}
<CardContent className="p-6">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h3 className="text-foreground font-bold text-2xl capitalize">{tier}</h3>
<div className="flex items-baseline gap-1">
<span className="text-4xl font-black text-foreground">
{price}
</span>
<span className="text-muted-foreground">{currency}</span>
</div>
</div>
<ul className="flex flex-col gap-3">
{features.map((feature, index) => (
<li key={index} className="flex items-start gap-3">
<div className="flex-shrink-0 w-5 h-5 rounded-full bg-primary/20 flex items-center justify-center mt-0.5">
<Check className="w-3 h-3 text-primary" />
</div>
<span className="text-sm text-muted-foreground">{feature}</span>
</li>
))}
</ul>
<Button
asChild
className={`w-full ${
popular
? 'glow-button bg-primary hover:bg-primary-glow text-white'
: 'bg-card border border-border hover:border-primary text-foreground'
} font-bold rounded-lg`}
>
<Link href={`/checkout?plan=${planId}`}>
Select {tier}
</Link>
</Button>
</div>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,26 @@
import { Card, CardContent } from '@/components/ui/card'
import { LucideIcon } from 'lucide-react'
interface ServiceCardProps {
icon: LucideIcon
title: string
description: string
}
export function ServiceCard({ icon: Icon, title, description }: ServiceCardProps) {
return (
<Card className="glow-card bg-card border-border hover:border-primary transition-all duration-300 rounded-xl">
<CardContent className="p-6">
<div className="flex flex-col gap-4">
<div className="p-3 bg-primary/10 text-primary rounded-xl w-fit">
<Icon className="w-8 h-8" />
</div>
<div className="flex flex-col gap-2">
<h3 className="text-foreground font-bold text-xl">{title}</h3>
<p className="text-muted-foreground text-sm leading-relaxed">{description}</p>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,51 @@
import { Star } from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
interface TestimonialCardProps {
name: string
role: string
company: string
avatar: string
rating: number
quote: string
}
export function TestimonialCard({ name, role, company, avatar, rating, quote }: TestimonialCardProps) {
return (
<Card className="bg-card border-border rounded-xl">
<CardContent className="p-6">
<div className="flex flex-col gap-4">
{/* Rating stars */}
<div className="flex gap-1">
{Array.from({ length: 5 }).map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${
i < rating ? 'fill-primary text-primary' : 'fill-muted text-muted-foreground'
}`}
/>
))}
</div>
{/* Quote */}
<p className="text-muted-foreground text-sm leading-relaxed">&ldquo;{quote}&rdquo;</p>
{/* Author */}
<div className="flex items-center gap-3 pt-2">
<Avatar className="w-10 h-10">
<AvatarImage src={avatar} alt={name} />
<AvatarFallback>{name.charAt(0)}</AvatarFallback>
</Avatar>
<div className="flex flex-col">
<p className="text-foreground font-semibold text-sm">{name}</p>
<p className="text-muted-foreground text-xs">
{role}, {company}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,32 @@
interface ProgressStepperProps {
currentStep: number
totalSteps: number
stepLabel: string
}
export function ProgressStepper({ currentStep, totalSteps, stepLabel }: ProgressStepperProps) {
const progress = (currentStep / totalSteps) * 100
return (
<div className="flex flex-col items-center gap-4 mb-12">
<div className="text-sm font-medium text-muted-foreground">
Step {currentStep} of {totalSteps}
</div>
<div className="w-full max-w-md">
<div className="flex justify-between text-xs text-muted-foreground mb-2">
<span>Select Service</span>
<span>Complete</span>
</div>
<div className="h-2 bg-card border border-border rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-primary to-primary-glow transition-all duration-500"
style={{ width: `${progress}%` }}
/>
</div>
</div>
<div className="text-lg font-semibold text-foreground">{stepLabel}</div>
</div>
)
}

View File

@ -0,0 +1,46 @@
'use client'
import { useState } from 'react'
type ServiceType = 'web-design' | 'ai-automation'
interface ServiceTypeToggleProps {
defaultValue: ServiceType
onChange: (value: ServiceType) => void
}
export function ServiceTypeToggle({ defaultValue, onChange }: ServiceTypeToggleProps) {
const [value, setValue] = useState<ServiceType>(defaultValue)
const handleChange = (newValue: ServiceType) => {
setValue(newValue)
onChange(newValue)
}
return (
<div className="flex items-center justify-center mb-12">
<div className="inline-flex bg-card border border-border rounded-lg p-1">
<button
onClick={() => handleChange('web-design')}
className={`px-6 py-3 rounded-md text-sm font-bold transition-all ${
value === 'web-design'
? 'bg-primary text-white shadow-lg'
: 'text-muted-foreground hover:text-foreground'
}`}
>
Web Design
</button>
<button
onClick={() => handleChange('ai-automation')}
className={`px-6 py-3 rounded-md text-sm font-bold transition-all ${
value === 'ai-automation'
? 'bg-primary text-white shadow-lg'
: 'text-muted-foreground hover:text-foreground'
}`}
>
AI Automation
</button>
</div>
</div>
)
}

View File

@ -0,0 +1,21 @@
import { ShieldCheck, Clock, CalendarCheck, Headphones } from 'lucide-react'
const badges = [
{ icon: ShieldCheck, text: 'Secure Payment' },
{ icon: Clock, text: '7-Day Delivery' },
{ icon: CalendarCheck, text: 'Cancel Anytime' },
{ icon: Headphones, text: '24/7 Support' },
]
export function TrustBadges() {
return (
<div className="flex flex-wrap justify-center gap-8 mt-12">
{badges.map((badge, index) => (
<div key={index} className="flex items-center gap-2 text-sm text-muted-foreground">
<badge.icon className="w-5 h-5 text-primary" />
<span>{badge.text}</span>
</div>
))}
</div>
)
}

109
components/ui/avatar.tsx Normal file
View File

@ -0,0 +1,109 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
function Avatar({
className,
size = "default",
...props
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
size?: "default" | "sm" | "lg"
}) {
return (
<AvatarPrimitive.Root
data-slot="avatar"
data-size={size}
className={cn(
"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
className
)}
{...props}
/>
)
}
function AvatarImage({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
return (
<AvatarPrimitive.Image
data-slot="avatar-image"
className={cn("aspect-square size-full", className)}
{...props}
/>
)
}
function AvatarFallback({
className,
...props
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
return (
<AvatarPrimitive.Fallback
data-slot="avatar-fallback"
className={cn(
"bg-muted text-muted-foreground flex size-full items-center justify-center rounded-full text-sm group-data-[size=sm]/avatar:text-xs",
className
)}
{...props}
/>
)
}
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="avatar-badge"
className={cn(
"bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full ring-2 select-none",
"group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
"group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
"group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
className
)}
{...props}
/>
)
}
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group"
className={cn(
"*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2",
className
)}
{...props}
/>
)
}
function AvatarGroupCount({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="avatar-group-count"
className={cn(
"bg-muted text-muted-foreground ring-background relative flex size-8 shrink-0 items-center justify-center rounded-full text-sm ring-2 group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
className
)}
{...props}
/>
)
}
export {
Avatar,
AvatarImage,
AvatarFallback,
AvatarBadge,
AvatarGroup,
AvatarGroupCount,
}

48
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,48 @@
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 badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-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 transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
link: "text-primary underline-offset-4 [a&]:hover:underline",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant = "default",
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
data-variant={variant}
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

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

@ -0,0 +1,64 @@
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 hover:bg-primary/90",
destructive:
"bg-destructive text-white 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 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",
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-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",
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

92
components/ui/card.tsx Normal file
View File

@ -0,0 +1,92 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn("leading-none font-semibold", className)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@ -0,0 +1,32 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

158
components/ui/dialog.tsx Normal file
View File

@ -0,0 +1,158 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({
className,
showCloseButton = false,
children,
...props
}: React.ComponentProps<"div"> & {
showCloseButton?: boolean
}) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close asChild>
<Button variant="outline">Close</Button>
</DialogPrimitive.Close>
)}
</div>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@ -0,0 +1,257 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

21
components/ui/input.tsx Normal file
View File

@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"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",
className
)}
{...props}
/>
)
}
export { Input }

24
components/ui/label.tsx Normal file
View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

190
components/ui/select.tsx Normal file
View File

@ -0,0 +1,190 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

View File

@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

40
components/ui/sonner.tsx Normal file
View File

@ -0,0 +1,40 @@
"use client"
import {
CircleCheckIcon,
InfoIcon,
Loader2Icon,
OctagonXIcon,
TriangleAlertIcon,
} from "lucide-react"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: <CircleCheckIcon className="size-4" />,
info: <InfoIcon className="size-4" />,
warning: <TriangleAlertIcon className="size-4" />,
error: <OctagonXIcon className="size-4" />,
loading: <Loader2Icon className="size-4 animate-spin" />,
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

91
components/ui/tabs.tsx Normal file
View File

@ -0,0 +1,91 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
function Tabs({
className,
orientation = "horizontal",
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
data-orientation={orientation}
orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className
)}
{...props}
/>
)
}
const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
},
},
defaultVariants: {
variant: "default",
},
}
)
function TabsList({
className,
variant = "default",
...props
}: React.ComponentProps<typeof TabsPrimitive.List> &
VariantProps<typeof tabsListVariants>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
data-variant={variant}
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }

9
lib/mock-data/index.ts Normal file
View File

@ -0,0 +1,9 @@
// Export all mock data
export * from './pricing'
export * from './projects'
export * from './invoices'
export * from './testimonials'
export * from './support'
// Re-export types
export type { ServiceType, PricingTier } from '../types'

102
lib/mock-data/invoices.ts Normal file
View File

@ -0,0 +1,102 @@
import { Invoice, PaymentMethod, Subscription } from '../types'
export const mockInvoices: Invoice[] = [
{
id: 'inv-1',
userId: 'user-1',
projectId: 'proj-1',
description: 'Landing Page Design - Growth Tier',
amount: 150,
currency: 'EUR',
vat: 30,
total: 180,
status: 'paid',
date: new Date('2024-10-01'),
dueDate: new Date('2024-10-15'),
downloadUrl: '/invoices/inv-1.pdf',
},
{
id: 'inv-2',
userId: 'user-1',
projectId: 'proj-2',
description: 'Monthly Automation - Growth Tier',
amount: 50,
currency: 'EUR',
vat: 10,
total: 60,
status: 'paid',
date: new Date('2024-09-01'),
dueDate: new Date('2024-09-15'),
downloadUrl: '/invoices/inv-2.pdf',
},
{
id: 'inv-3',
userId: 'user-1',
description: 'Consultation Hour',
amount: 75,
currency: 'EUR',
vat: 15,
total: 90,
status: 'refunded',
date: new Date('2024-07-15'),
dueDate: new Date('2024-07-15'),
downloadUrl: '/invoices/inv-3.pdf',
},
{
id: 'inv-4',
userId: 'user-1',
description: 'AI Chatbot - Starter Tier',
amount: 50,
currency: 'EUR',
vat: 10,
total: 60,
status: 'pending',
date: new Date('2024-10-16'),
dueDate: new Date('2024-10-30'),
downloadUrl: '/invoices/inv-4.pdf',
},
]
export const mockPaymentMethods: PaymentMethod[] = [
{
id: 'pm-1',
userId: 'user-1',
type: 'card',
isDefault: true,
last4: '4242',
brand: 'visa',
},
{
id: 'pm-2',
userId: 'user-1',
type: 'paypal',
isDefault: false,
email: 'alex@bakery.com',
},
]
export const mockSubscription: Subscription = {
id: 'sub-1',
userId: 'user-1',
planId: 'growth-ai',
status: 'active',
currentPeriodStart: new Date('2024-10-01'),
currentPeriodEnd: new Date('2024-11-01'),
cancelAtPeriodEnd: false,
}
export function getInvoicesByUserId(userId: string): Invoice[] {
return mockInvoices.filter(inv => inv.userId === userId)
}
export function getInvoiceById(id: string): Invoice | undefined {
return mockInvoices.find(inv => inv.id === id)
}
export function getPaymentMethodsByUserId(userId: string): PaymentMethod[] {
return mockPaymentMethods.filter(pm => pm.userId === userId)
}
export function getDefaultPaymentMethod(userId: string): PaymentMethod | undefined {
return mockPaymentMethods.find(pm => pm.userId === userId && pm.isDefault)
}

114
lib/mock-data/pricing.ts Normal file
View File

@ -0,0 +1,114 @@
import { PricingPlan } from '../types'
export const mockPricingPlans: PricingPlan[] = [
// Web Design Plans
{
id: 'starter-web',
tier: 'starter',
serviceType: 'web-design',
price: 50,
currency: 'EUR',
features: [
'1-Page Landing Page',
'Contact Form',
'Mobile Responsive',
'2 Revisions',
],
popular: false,
deliveryDays: 3,
revisions: 2,
},
{
id: 'growth-web',
tier: 'growth',
serviceType: 'web-design',
price: 150,
currency: 'EUR',
features: [
'5-Page Complete Site',
'CMS Integration',
'Basic SEO Setup',
'Social Media Links',
'5 Revisions',
],
popular: true,
deliveryDays: 7,
revisions: 5,
},
{
id: 'pro-web',
tier: 'pro',
serviceType: 'web-design',
price: 300,
currency: 'EUR',
features: [
'E-commerce Ready',
'Custom Animations',
'Priority Support',
'Advanced SEO',
'Unlimited Revisions',
],
popular: false,
deliveryDays: 14,
revisions: -1,
},
// AI Automation Plans
{
id: 'starter-ai',
tier: 'starter',
serviceType: 'ai-automation',
price: 50,
currency: 'EUR',
features: [
'Basic Chatbot',
'FAQ Integration',
'Email Notifications',
'Weekly Reports',
],
popular: false,
deliveryDays: 2,
revisions: 1,
},
{
id: 'growth-ai',
tier: 'growth',
serviceType: 'ai-automation',
price: 150,
currency: 'EUR',
features: [
'Advanced Chatbot',
'Multi-language Support',
'CRM Integration',
'Analytics Dashboard',
'Priority Support',
],
popular: true,
deliveryDays: 5,
revisions: 3,
},
{
id: 'pro-ai',
tier: 'pro',
serviceType: 'ai-automation',
price: 300,
currency: 'EUR',
features: [
'Full Automation Suite',
'Custom AI Training',
'Workflow Integration',
'24/7 Monitoring',
'Dedicated Manager',
],
popular: false,
deliveryDays: 10,
revisions: -1,
},
]
export function getPricingPlansByServiceType(serviceType: 'web-design' | 'ai-automation'): PricingPlan[] {
return mockPricingPlans.filter(plan => plan.serviceType === serviceType)
}
export function getPricingPlanById(id: string): PricingPlan | undefined {
return mockPricingPlans.find(plan => plan.id === id)
}

68
lib/mock-data/projects.ts Normal file
View File

@ -0,0 +1,68 @@
import { Project, User } from '../types'
// Mock current user
export const mockCurrentUser: User = {
id: 'user-1',
email: 'alex@bakery.com',
name: 'Alex Johnson',
avatar: 'https://lh3.googleusercontent.com/a/default-user',
role: 'client',
}
export const mockProjects: Project[] = [
{
id: 'proj-1',
userId: 'user-1',
title: 'Bakery Website Redesign',
description: 'Modernizing the online presence with e-commerce integration for custom cake orders.',
type: 'web-design',
tier: 'growth',
status: 'development',
progress: 75,
thumbnail: 'https://images.unsplash.com/photo-1517433367423-c7e5b0f35086?w=400',
stages: [
{ name: 'Discovery', completed: true },
{ name: 'Design', completed: true },
{ name: 'Development', completed: false },
{ name: 'Content', completed: false },
],
createdAt: new Date('2024-10-01'),
updatedAt: new Date('2024-10-15'),
estimatedDelivery: new Date('2024-10-20'),
},
{
id: 'proj-2',
userId: 'user-1',
title: 'Customer Service Bot',
description: 'Training the model on your FAQs to handle initial customer inquiries automatically.',
type: 'ai-automation',
tier: 'growth',
status: 'design',
progress: 20,
thumbnail: 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=400',
stages: [
{ name: 'Setup', completed: true },
{ name: 'Training', completed: false },
{ name: 'Testing', completed: false },
],
createdAt: new Date('2024-10-10'),
updatedAt: new Date('2024-10-14'),
estimatedDelivery: new Date('2024-10-25'),
},
]
export function getProjectsByUserId(userId: string): Project[] {
return mockProjects.filter(p => p.userId === userId)
}
export function getProjectById(id: string): Project | undefined {
return mockProjects.find(p => p.id === id)
}
export function getActiveProjects(): Project[] {
return mockProjects.filter(p => p.status !== 'completed')
}
export function getCompletedProjects(): Project[] {
return mockProjects.filter(p => p.status === 'completed')
}

101
lib/mock-data/support.ts Normal file
View File

@ -0,0 +1,101 @@
import { SupportTicket, DashboardStats, FAQ } from '../types'
export const mockSupportTickets: SupportTicket[] = [
{
id: 'ticket-1',
userId: 'user-1',
projectId: 'proj-1',
subject: 'Update product images',
description: 'We need to replace the product images with higher resolution versions.',
status: 'in-progress',
priority: 'medium',
createdAt: new Date('2024-10-12'),
updatedAt: new Date('2024-10-14'),
},
{
id: 'ticket-2',
userId: 'user-1',
projectId: 'proj-2',
subject: 'Chatbot training data review',
description: 'Please review and update the FAQ responses for the customer service bot.',
status: 'open',
priority: 'low',
createdAt: new Date('2024-10-14'),
updatedAt: new Date('2024-10-14'),
},
{
id: 'ticket-3',
userId: 'user-1',
subject: 'Billing inquiry',
description: 'Question about the invoice from September.',
status: 'resolved',
priority: 'low',
createdAt: new Date('2024-09-20'),
updatedAt: new Date('2024-09-22'),
},
]
export const mockDashboardStats: DashboardStats = {
totalProjects: 2,
activeProjects: 2,
pendingTickets: 1,
totalSpent: 330, // EUR
}
export const mockFAQs: FAQ[] = [
{
id: 'faq-1',
question: 'How long does it take to complete a website?',
answer: 'Delivery times depend on the tier you choose: Starter (3 days), Growth (7 days), and Pro (14 days). Complex projects may take longer.',
category: 'General',
},
{
id: 'faq-2',
question: 'What payment methods do you accept?',
answer: 'We accept all major credit cards, PayPal, and Stripe Link for secure payments.',
category: 'Billing',
},
{
id: 'faq-3',
question: 'Can I request revisions?',
answer: 'Yes! The Starter tier includes 2 revisions, Growth includes 5, and Pro offers unlimited revisions.',
category: 'Services',
},
{
id: 'faq-4',
question: 'Do you offer ongoing support?',
answer: 'Yes, we provide various support packages. Pro tier customers get priority support and a dedicated account manager.',
category: 'Support',
},
{
id: 'faq-5',
question: 'How does the AI automation work?',
answer: 'We train AI models on your specific business data and FAQs. The chatbot learns to handle customer inquiries automatically.',
category: 'AI Services',
},
{
id: 'faq-6',
question: 'Is my data secure?',
answer: 'Absolutely. We use industry-standard encryption and security measures. Your data is never shared with third parties.',
category: 'Security',
},
]
export function getTicketsByUserId(userId: string): SupportTicket[] {
return mockSupportTickets.filter(ticket => ticket.userId === userId)
}
export function getOpenTickets(userId: string): SupportTicket[] {
return mockSupportTickets.filter(
ticket => ticket.userId === userId && ticket.status !== 'closed'
)
}
export function getDashboardStats(userId: string): DashboardStats {
// In a real app, this would calculate stats based on actual data
return mockDashboardStats
}
export function getAllFAQs(): FAQ[] {
return mockFAQs
}

View File

@ -0,0 +1,48 @@
import { Testimonial } from '../types'
export const mockTestimonials: Testimonial[] = [
{
id: 'test-1',
name: 'Sarah Schmidt',
role: 'Marketing Director',
company: 'TechStart Berlin',
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100',
rating: 5,
quote: 'ScaleSite transformed our online presence. The new website increased our conversion rate by 40% in just two months. Absolutely professional work!',
},
{
id: 'test-2',
name: 'Michael Weber',
role: 'Founder',
company: 'Bakery Dreams',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100',
rating: 5,
quote: 'The AI chatbot handles 80% of our customer inquiries automatically. Saved us countless hours and improved customer satisfaction.',
},
{
id: 'test-3',
name: 'Julia Hoffman',
role: 'E-commerce Manager',
company: 'Fashion Forward',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=100',
rating: 4,
quote: 'Fast delivery, great communication, and the design exceeded our expectations. The e-commerce integration works flawlessly.',
},
{
id: 'test-4',
name: 'Thomas Klein',
role: 'CEO',
company: 'Startup Ventures',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=100',
rating: 5,
quote: 'We needed a complete digital transformation and ScaleSite delivered. From website to AI automation, everything works perfectly together.',
},
]
export function getAllTestimonials(): Testimonial[] {
return mockTestimonials
}
export function getTestimonialsByIds(ids: string[]): Testimonial[] {
return mockTestimonials.filter(t => ids.includes(t.id))
}

167
lib/types/index.ts Normal file
View File

@ -0,0 +1,167 @@
// User & Auth
export interface User {
id: string
email: string
name: string
avatar?: string
role: 'client' | 'admin'
}
// Services & Pricing
export type ServiceType = 'web-design' | 'ai-automation'
export type PricingTier = 'starter' | 'growth' | 'pro'
export interface PricingPlan {
id: string
tier: PricingTier
serviceType: ServiceType
price: number
currency: string
features: string[]
popular?: boolean
deliveryDays: number
revisions: number
}
export interface Service {
id: string
type: ServiceType
title: string
description: string
icon: string
image?: string
}
// Projects & Dashboard
export type ProjectStatus = 'discovery' | 'design' | 'development' | 'content' | 'testing' | 'completed'
export interface ProjectStage {
name: string
completed: boolean
}
export interface Project {
id: string
userId: string
title: string
description: string
type: ServiceType
tier: PricingTier
status: ProjectStatus
progress: number
thumbnail: string
stages: ProjectStage[]
createdAt: Date
updatedAt: Date
estimatedDelivery?: Date
}
// Support Tickets
export type TicketStatus = 'open' | 'in-progress' | 'resolved' | 'closed'
export interface SupportTicket {
id: string
userId: string
projectId?: string
subject: string
description: string
status: TicketStatus
priority: 'low' | 'medium' | 'high'
createdAt: Date
updatedAt: Date
}
// Billing & Invoices
export type InvoiceStatus = 'paid' | 'pending' | 'refunded' | 'cancelled'
export interface Invoice {
id: string
userId: string
projectId?: string
description: string
amount: number
currency: string
vat: number
total: number
status: InvoiceStatus
date: Date
dueDate: Date
downloadUrl?: string
}
export type PaymentMethodType = 'card' | 'paypal' | 'stripe-link'
export interface PaymentMethod {
id: string
userId: string
type: PaymentMethodType
isDefault: boolean
last4?: string
brand?: string
email?: string
}
export interface Subscription {
id: string
userId: string
planId: string
status: 'active' | 'cancelled' | 'past_due'
currentPeriodStart: Date
currentPeriodEnd: Date
cancelAtPeriodEnd: boolean
}
// Testimonials & Marketing
export interface Testimonial {
id: string
name: string
role: string
company: string
avatar: string
rating: number
quote: string
}
// Checkout
export type CheckoutStep = 'account' | 'billing' | 'payment'
export interface CheckoutSession {
id: string
userId?: string
serviceType: ServiceType
tier: PricingTier
email: string
billingAddress?: Address
paymentMethodId?: string
amount: number
vat: number
total: number
status: 'pending' | 'processing' | 'completed' | 'failed'
currentStep: CheckoutStep
createdAt: Date
}
export interface Address {
fullName: string
street: string
city: string
postalCode: string
country: string
}
// FAQ
export interface FAQ {
id: string
question: string
answer: string
category?: string
}
// Stats for dashboard
export interface DashboardStats {
totalProjects: number
activeProjects: number
pendingTickets: number
totalSpent: number
}

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

34
logs/server.log Normal file
View File

@ -0,0 +1,34 @@
> scalesite-nextjs@0.1.0 dev
> next dev
⚠ Port 3000 is in use by an unknown process, using available port 3001 instead.
▲ Next.js 16.1.6 (Turbopack)
- Local: http://localhost:3001
- Network: http://192.168.178.115:3001
✓ Starting...
✓ Ready in 718ms
GET / 200 in 846ms (compile: 272ms, render: 574ms)
GET / 200 in 193ms (compile: 7ms, render: 186ms)
GET /login 200 in 739ms (compile: 703ms, render: 36ms)
✓ Compiled in 133ms
GET / 200 in 167ms (compile: 54ms, render: 113ms)
GET / 200 in 154ms (compile: 46ms, render: 108ms)
GET / 200 in 175ms (compile: 54ms, render: 122ms)
GET / 200 in 88ms (compile: 4ms, render: 85ms)
GET / 200 in 216ms (compile: 46ms, render: 170ms)
✓ Compiled in 45ms
GET / 200 in 77ms (compile: 4ms, render: 72ms)
✓ Compiled in 61ms
GET / 200 in 76ms (compile: 5ms, render: 71ms)
✓ Compiled in 85ms
GET / 200 in 77ms (compile: 3ms, render: 74ms)
GET / 200 in 182ms (compile: 46ms, render: 136ms)
GET /dashboard 200 in 1174ms (compile: 807ms, render: 366ms)
GET /dashboard/projects 404 in 104ms (compile: 79ms, render: 25ms)
GET /dashboard/projects 404 in 64ms (compile: 3ms, render: 61ms)
GET /dashboard 200 in 129ms (compile: 4ms, render: 125ms)
✓ Compiled in 73ms
GET /dashboard 200 in 67ms (compile: 3ms, render: 64ms)
[?25h

2004
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,9 +9,31 @@
"lint": "eslint"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toast": "^1.2.15",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.29.2",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next-themes": "^0.4.6",
"react": "19.2.3",
"react-dom": "19.2.3"
"react-dom": "19.2.3",
"react-hook-form": "^7.71.1",
"sonner": "^2.0.7",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
@ -21,6 +43,7 @@
"eslint": "^9",
"eslint-config-next": "16.1.6",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
}