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:
parent
aa211a53d3
commit
98552163a8
192
CLAUDE.md
Normal file
192
CLAUDE.md
Normal 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
16
app/billing/layout.tsx
Normal 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
34
app/billing/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
285
app/checkout/checkout-content.tsx
Normal file
285
app/checkout/checkout-content.tsx
Normal 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
10
app/checkout/page.tsx
Normal 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
16
app/dashboard/layout.tsx
Normal 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
106
app/dashboard/page.tsx
Normal 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's what'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>
|
||||
)
|
||||
}
|
||||
206
app/globals.css
206
app/globals.css
@ -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;
|
||||
:root {
|
||||
--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);
|
||||
}
|
||||
|
||||
@ -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
90
app/login/page.tsx
Normal 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'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>
|
||||
)
|
||||
}
|
||||
211
app/page.tsx
211
app/page.tsx
@ -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="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}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
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>
|
||||
);
|
||||
</section>
|
||||
</MarketingLayout>
|
||||
)
|
||||
}
|
||||
|
||||
110
app/register/page.tsx
Normal file
110
app/register/page.tsx
Normal 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
82
app/services/page.tsx
Normal 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
23
components.json
Normal 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": {}
|
||||
}
|
||||
48
components/auth/auth-layout.tsx
Normal file
48
components/auth/auth-layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
44
components/auth/social-login-buttons.tsx
Normal file
44
components/auth/social-login-buttons.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
89
components/billing/invoice-table.tsx
Normal file
89
components/billing/invoice-table.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
70
components/billing/payment-method-list.tsx
Normal file
70
components/billing/payment-method-list.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
60
components/billing/subscription-card.tsx
Normal file
60
components/billing/subscription-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
54
components/checkout/checkout-steps.tsx
Normal file
54
components/checkout/checkout-steps.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
79
components/checkout/checkout-summary.tsx
Normal file
79
components/checkout/checkout-summary.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
59
components/checkout/payment-method-card.tsx
Normal file
59
components/checkout/payment-method-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
82
components/dashboard/project-card.tsx
Normal file
82
components/dashboard/project-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
40
components/dashboard/stats-card.tsx
Normal file
40
components/dashboard/stats-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
71
components/dashboard/support-ticket-item.tsx
Normal file
71
components/dashboard/support-ticket-item.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
38
components/dashboard/upgrade-card.tsx
Normal file
38
components/dashboard/upgrade-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
122
components/layouts/dashboard-sidebar.tsx
Normal file
122
components/layouts/dashboard-sidebar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
14
components/layouts/marketing-layout.tsx
Normal file
14
components/layouts/marketing-layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
131
components/layouts/site-footer.tsx
Normal file
131
components/layouts/site-footer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
106
components/layouts/site-header.tsx
Normal file
106
components/layouts/site-header.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
21
components/marketing/faq-item.tsx
Normal file
21
components/marketing/faq-item.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
57
components/marketing/hero-section.tsx
Normal file
57
components/marketing/hero-section.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
69
components/marketing/pricing-card.tsx
Normal file
69
components/marketing/pricing-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
26
components/marketing/service-card.tsx
Normal file
26
components/marketing/service-card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
51
components/marketing/testimonial-card.tsx
Normal file
51
components/marketing/testimonial-card.tsx
Normal 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">“{quote}”</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>
|
||||
)
|
||||
}
|
||||
32
components/services/progress-stepper.tsx
Normal file
32
components/services/progress-stepper.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
46
components/services/service-type-toggle.tsx
Normal file
46
components/services/service-type-toggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
21
components/services/trust-badges.tsx
Normal file
21
components/services/trust-badges.tsx
Normal 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
109
components/ui/avatar.tsx
Normal 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
48
components/ui/badge.tsx
Normal 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
64
components/ui/button.tsx
Normal 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
92
components/ui/card.tsx
Normal 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,
|
||||
}
|
||||
32
components/ui/checkbox.tsx
Normal file
32
components/ui/checkbox.tsx
Normal 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
158
components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
257
components/ui/dropdown-menu.tsx
Normal file
257
components/ui/dropdown-menu.tsx
Normal 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
21
components/ui/input.tsx
Normal 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
24
components/ui/label.tsx
Normal 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 }
|
||||
31
components/ui/progress.tsx
Normal file
31
components/ui/progress.tsx
Normal 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
190
components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
28
components/ui/separator.tsx
Normal file
28
components/ui/separator.tsx
Normal 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
40
components/ui/sonner.tsx
Normal 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
91
components/ui/tabs.tsx
Normal 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
9
lib/mock-data/index.ts
Normal 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
102
lib/mock-data/invoices.ts
Normal 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
114
lib/mock-data/pricing.ts
Normal 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
68
lib/mock-data/projects.ts
Normal 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
101
lib/mock-data/support.ts
Normal 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
|
||||
}
|
||||
48
lib/mock-data/testimonials.ts
Normal file
48
lib/mock-data/testimonials.ts
Normal 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
167
lib/types/index.ts
Normal 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
6
lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
34
logs/server.log
Normal file
34
logs/server.log
Normal 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
2004
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user