Initial commit: SmoothSchedule multi-tenant scheduling platform

This commit includes:
- Django backend with multi-tenancy (django-tenants)
- React + TypeScript frontend with Vite
- Platform administration API with role-based access control
- Authentication system with token-based auth
- Quick login dev tools for testing different user roles
- CORS and CSRF configuration for local development
- Docker development environment setup

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-11-27 01:43:20 -05:00
commit 2e111364a2
567 changed files with 96410 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Lightbulb, Shield, Eye, HeartHandshake } from 'lucide-react';
import CTASection from '../../components/marketing/CTASection';
const AboutPage: React.FC = () => {
const { t } = useTranslation();
const values = [
{
icon: Lightbulb,
titleKey: 'marketing.about.values.simplicity.title',
descriptionKey: 'marketing.about.values.simplicity.description',
color: 'brand',
},
{
icon: Shield,
titleKey: 'marketing.about.values.reliability.title',
descriptionKey: 'marketing.about.values.reliability.description',
color: 'green',
},
{
icon: Eye,
titleKey: 'marketing.about.values.transparency.title',
descriptionKey: 'marketing.about.values.transparency.description',
color: 'purple',
},
{
icon: HeartHandshake,
titleKey: 'marketing.about.values.support.title',
descriptionKey: 'marketing.about.values.support.description',
color: 'orange',
},
];
const colorClasses: Record<string, string> = {
brand: 'bg-brand-100 dark:bg-brand-900/30 text-brand-600 dark:text-brand-400',
green: 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400',
purple: 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400',
orange: 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400',
};
return (
<div>
{/* Header Section */}
<section className="py-20 lg:py-28 bg-gradient-to-br from-white via-brand-50/30 to-white dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-4xl sm:text-5xl font-bold text-gray-900 dark:text-white mb-6">
{t('marketing.about.title')}
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400">
{t('marketing.about.subtitle')}
</p>
</div>
</section>
{/* Story Section */}
<section className="py-20 bg-white dark:bg-gray-900">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid md:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-6">
{t('marketing.about.story.title')}
</h2>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed mb-6">
{t('marketing.about.story.content')}
</p>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
{t('marketing.about.story.content2')}
</p>
</div>
<div className="bg-gradient-to-br from-brand-500 to-brand-600 rounded-2xl p-8 text-white">
<div className="text-6xl font-bold mb-2">2017</div>
<div className="text-brand-100">{t('marketing.about.story.founded')}</div>
<div className="mt-8 space-y-4">
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-brand-200 rounded-full" />
<span className="text-brand-100">8+ years building scheduling solutions</span>
</div>
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-brand-200 rounded-full" />
<span className="text-brand-100">Battle-tested with real businesses</span>
</div>
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-brand-200 rounded-full" />
<span className="text-brand-100">Features born from customer feedback</span>
</div>
<div className="flex items-center gap-3">
<div className="w-2 h-2 bg-brand-200 rounded-full" />
<span className="text-brand-100">Now available to everyone</span>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Mission Section */}
<section className="py-20 bg-gray-50 dark:bg-gray-800/50">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-6">
{t('marketing.about.mission.title')}
</h2>
<p className="text-xl text-gray-600 dark:text-gray-400 leading-relaxed max-w-3xl mx-auto">
{t('marketing.about.mission.content')}
</p>
</div>
</section>
{/* Values Section */}
<section className="py-20 bg-white dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-12 text-center">
{t('marketing.about.values.title')}
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{values.map((value) => (
<div key={value.titleKey} className="text-center">
<div className={`inline-flex p-4 rounded-2xl ${colorClasses[value.color]} mb-4`}>
<value.icon className="h-8 w-8" />
</div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{t(value.titleKey)}
</h3>
<p className="text-gray-600 dark:text-gray-400">
{t(value.descriptionKey)}
</p>
</div>
))}
</div>
</div>
</section>
{/* CTA Section */}
<CTASection variant="minimal" />
</div>
);
};
export default AboutPage;

View File

@@ -0,0 +1,261 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Mail, Phone, MapPin, Send, MessageSquare } from 'lucide-react';
const ContactPage: React.FC = () => {
const { t } = useTranslation();
const [formState, setFormState] = useState({
name: '',
email: '',
subject: '',
message: '',
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate form submission
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsSubmitting(false);
setSubmitted(true);
setFormState({ name: '', email: '', subject: '', message: '' });
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setFormState((prev) => ({
...prev,
[e.target.name]: e.target.value,
}));
};
const contactInfo = [
{
icon: Mail,
label: 'Email',
value: t('marketing.contact.info.email'),
href: `mailto:${t('marketing.contact.info.email')}`,
},
{
icon: Phone,
label: 'Phone',
value: t('marketing.contact.info.phone'),
href: `tel:${t('marketing.contact.info.phone').replace(/[^0-9+]/g, '')}`,
},
{
icon: MapPin,
label: 'Address',
value: t('marketing.contact.info.address'),
href: null,
},
];
return (
<div>
{/* Header Section */}
<section className="py-20 lg:py-28 bg-gradient-to-br from-white via-brand-50/30 to-white dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-4xl sm:text-5xl font-bold text-gray-900 dark:text-white mb-6">
{t('marketing.contact.title')}
</h1>
<p className="text-xl text-gray-600 dark:text-gray-400">
{t('marketing.contact.subtitle')}
</p>
</div>
</section>
{/* Contact Content */}
<section className="py-20 bg-white dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid lg:grid-cols-2 gap-12 lg:gap-16">
{/* Contact Form */}
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-8">
Send us a message
</h2>
{submitted ? (
<div className="p-8 bg-green-50 dark:bg-green-900/20 rounded-2xl border border-green-200 dark:border-green-800 text-center">
<div className="w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-4">
<Send className="h-8 w-8 text-green-600 dark:text-green-400" />
</div>
<h3 className="text-lg font-semibold text-green-800 dark:text-green-200 mb-2">
Message Sent!
</h3>
<p className="text-green-600 dark:text-green-400">
{t('marketing.contact.form.success')}
</p>
<button
onClick={() => setSubmitted(false)}
className="mt-4 text-sm text-green-700 dark:text-green-300 underline"
>
Send another message
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.contact.form.name')}
</label>
<input
type="text"
id="name"
name="name"
value={formState.name}
onChange={handleChange}
required
placeholder={t('marketing.contact.form.namePlaceholder')}
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-brand-500 focus:border-brand-500 transition-colors"
/>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.contact.form.email')}
</label>
<input
type="email"
id="email"
name="email"
value={formState.email}
onChange={handleChange}
required
placeholder={t('marketing.contact.form.emailPlaceholder')}
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-brand-500 focus:border-brand-500 transition-colors"
/>
</div>
<div>
<label
htmlFor="subject"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.contact.form.subject')}
</label>
<input
type="text"
id="subject"
name="subject"
value={formState.subject}
onChange={handleChange}
required
placeholder={t('marketing.contact.form.subjectPlaceholder')}
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-brand-500 focus:border-brand-500 transition-colors"
/>
</div>
<div>
<label
htmlFor="message"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.contact.form.message')}
</label>
<textarea
id="message"
name="message"
value={formState.message}
onChange={handleChange}
required
rows={5}
placeholder={t('marketing.contact.form.messagePlaceholder')}
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:ring-brand-500 focus:border-brand-500 transition-colors resize-none"
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full py-3.5 px-6 text-base font-semibold text-white bg-brand-600 rounded-xl hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors flex items-center justify-center gap-2"
>
{isSubmitting ? (
<>
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
{t('marketing.contact.form.sending')}
</>
) : (
<>
<Send className="h-5 w-5" />
{t('marketing.contact.form.submit')}
</>
)}
</button>
</form>
)}
</div>
{/* Contact Info */}
<div>
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-8">
Get in touch
</h2>
<div className="space-y-6 mb-12">
{contactInfo.map((item) => (
<div key={item.label} className="flex items-start gap-4">
<div className="p-3 bg-brand-50 dark:bg-brand-900/30 rounded-xl">
<item.icon className="h-6 w-6 text-brand-600 dark:text-brand-400" />
</div>
<div>
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">
{item.label}
</div>
{item.href ? (
<a
href={item.href}
className="text-gray-900 dark:text-white hover:text-brand-600 dark:hover:text-brand-400 transition-colors"
>
{item.value}
</a>
) : (
<span className="text-gray-900 dark:text-white">{item.value}</span>
)}
</div>
</div>
))}
</div>
{/* Sales CTA */}
<div className="p-6 bg-gray-50 dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700">
<div className="flex items-start gap-4">
<div className="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-xl">
<MessageSquare className="h-6 w-6 text-purple-600 dark:text-purple-400" />
</div>
<div>
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{t('marketing.contact.sales.title')}
</h3>
<p className="text-gray-600 dark:text-gray-400 mb-4">
{t('marketing.contact.sales.description')}
</p>
<a
href="mailto:sales@smoothschedule.com"
className="inline-flex items-center gap-2 text-brand-600 dark:text-brand-400 font-medium hover:text-brand-700 dark:hover:text-brand-300 transition-colors"
>
Schedule a call
<span aria-hidden="true">&rarr;</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
);
};
export default ContactPage;

View File

@@ -0,0 +1,173 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
Calendar,
Users,
CreditCard,
Building2,
Palette,
BarChart3,
Plug,
UserCircle,
Bell,
Shield,
Smartphone,
Clock,
} from 'lucide-react';
import FeatureCard from '../../components/marketing/FeatureCard';
import CTASection from '../../components/marketing/CTASection';
const FeaturesPage: React.FC = () => {
const { t } = useTranslation();
const featureCategories = [
{
title: 'Scheduling & Calendar',
features: [
{
icon: Calendar,
titleKey: 'marketing.features.scheduling.title',
descriptionKey: 'marketing.features.scheduling.description',
color: 'brand',
},
{
icon: Clock,
title: 'Real-Time Availability',
description: 'Customers see only available time slots. No double bookings, ever.',
color: 'green',
},
{
icon: Bell,
title: 'Automated Reminders',
description: 'Reduce no-shows with email and SMS reminders sent automatically.',
color: 'purple',
},
],
},
{
title: 'Resource Management',
features: [
{
icon: Users,
titleKey: 'marketing.features.resources.title',
descriptionKey: 'marketing.features.resources.description',
color: 'orange',
},
{
icon: Smartphone,
title: 'Staff Mobile App',
description: 'Your team can view schedules and manage appointments on the go.',
color: 'pink',
},
{
icon: Shield,
title: 'Role-Based Access',
description: 'Control what each team member can see and do with granular permissions.',
color: 'cyan',
},
],
},
{
title: 'Customer Experience',
features: [
{
icon: UserCircle,
titleKey: 'marketing.features.customers.title',
descriptionKey: 'marketing.features.customers.description',
color: 'brand',
},
{
icon: CreditCard,
titleKey: 'marketing.features.payments.title',
descriptionKey: 'marketing.features.payments.description',
color: 'green',
},
{
icon: Palette,
titleKey: 'marketing.features.whiteLabel.title',
descriptionKey: 'marketing.features.whiteLabel.description',
color: 'purple',
},
],
},
{
title: 'Business Growth',
features: [
{
icon: Building2,
titleKey: 'marketing.features.multiTenant.title',
descriptionKey: 'marketing.features.multiTenant.description',
color: 'orange',
},
{
icon: BarChart3,
titleKey: 'marketing.features.analytics.title',
descriptionKey: 'marketing.features.analytics.description',
color: 'pink',
},
{
icon: Plug,
titleKey: 'marketing.features.integrations.title',
descriptionKey: 'marketing.features.integrations.description',
color: 'cyan',
},
],
},
];
return (
<div>
{/* Header Section */}
<section className="py-20 lg:py-28 bg-gradient-to-br from-white via-brand-50/30 to-white dark:from-gray-900 dark:via-gray-800 dark:to-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h1 className="text-4xl sm:text-5xl font-bold text-gray-900 dark:text-white mb-4">
{t('marketing.features.title')}
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
{t('marketing.features.subtitle')}
</p>
</div>
</div>
</section>
{/* Feature Categories */}
{featureCategories.map((category, categoryIndex) => (
<section
key={categoryIndex}
className={`py-20 ${
categoryIndex % 2 === 0
? 'bg-white dark:bg-gray-900'
: 'bg-gray-50 dark:bg-gray-800/50'
}`}
>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h2 className="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-white mb-10 text-center">
{category.title}
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{category.features.map((feature, featureIndex) => (
<FeatureCard
key={featureIndex}
icon={feature.icon}
title={feature.titleKey ? t(feature.titleKey) : feature.title || ''}
description={
feature.descriptionKey
? t(feature.descriptionKey)
: feature.description || ''
}
iconColor={feature.color}
/>
))}
</div>
</div>
</section>
))}
{/* CTA Section */}
<CTASection />
</div>
);
};
export default FeaturesPage;

View File

@@ -0,0 +1,258 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Calendar,
Users,
CreditCard,
Building2,
Palette,
BarChart3,
Plug,
UserCircle,
ArrowRight,
} from 'lucide-react';
import Hero from '../../components/marketing/Hero';
import FeatureCard from '../../components/marketing/FeatureCard';
import HowItWorks from '../../components/marketing/HowItWorks';
import StatsSection from '../../components/marketing/StatsSection';
import TestimonialCard from '../../components/marketing/TestimonialCard';
import CTASection from '../../components/marketing/CTASection';
const HomePage: React.FC = () => {
const { t } = useTranslation();
const features = [
{
icon: Calendar,
titleKey: 'marketing.features.scheduling.title',
descriptionKey: 'marketing.features.scheduling.description',
color: 'brand',
},
{
icon: Users,
titleKey: 'marketing.features.resources.title',
descriptionKey: 'marketing.features.resources.description',
color: 'green',
},
{
icon: UserCircle,
titleKey: 'marketing.features.customers.title',
descriptionKey: 'marketing.features.customers.description',
color: 'purple',
},
{
icon: CreditCard,
titleKey: 'marketing.features.payments.title',
descriptionKey: 'marketing.features.payments.description',
color: 'orange',
},
{
icon: Building2,
titleKey: 'marketing.features.multiTenant.title',
descriptionKey: 'marketing.features.multiTenant.description',
color: 'pink',
},
{
icon: Palette,
titleKey: 'marketing.features.whiteLabel.title',
descriptionKey: 'marketing.features.whiteLabel.description',
color: 'cyan',
},
];
const testimonials = [
{
quote: "SmoothSchedule transformed how we manage appointments. Our no-show rate dropped by 40% with automated reminders.",
author: "Sarah Johnson",
role: "Owner",
company: "Luxe Salon",
rating: 5,
},
{
quote: "The white-label feature is perfect for our multi-location business. Each location has its own branded booking experience.",
author: "Michael Chen",
role: "CEO",
company: "FitLife Studios",
rating: 5,
},
{
quote: "Setup was incredibly easy. We were up and running in under an hour, and our clients love the self-service booking.",
author: "Emily Rodriguez",
role: "Manager",
company: "Peak Performance Therapy",
rating: 5,
},
];
return (
<div>
{/* Hero Section */}
<Hero />
{/* Features Section */}
<section className="py-20 lg:py-28 bg-white dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{t('marketing.features.title')}
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
{t('marketing.features.subtitle')}
</p>
</div>
{/* Feature Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{features.map((feature) => (
<FeatureCard
key={feature.titleKey}
icon={feature.icon}
title={t(feature.titleKey)}
description={t(feature.descriptionKey)}
iconColor={feature.color}
/>
))}
</div>
{/* View All Features Link */}
<div className="text-center mt-12">
<Link
to="/features"
className="inline-flex items-center gap-2 text-brand-600 dark:text-brand-400 font-medium hover:text-brand-700 dark:hover:text-brand-300 transition-colors"
>
{t('common.viewAll')} {t('marketing.nav.features').toLowerCase()}
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div>
</section>
{/* How It Works Section */}
<HowItWorks />
{/* Stats Section */}
<StatsSection />
{/* Testimonials Section */}
<section className="py-20 lg:py-28 bg-gray-50 dark:bg-gray-800/50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{t('marketing.testimonials.title')}
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
{t('marketing.testimonials.subtitle')}
</p>
</div>
{/* Testimonials Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
{testimonials.map((testimonial, index) => (
<TestimonialCard key={index} {...testimonial} />
))}
</div>
</div>
</section>
{/* Pricing Preview Section */}
<section className="py-20 lg:py-28 bg-white dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-16">
<h2 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-4">
{t('marketing.pricing.title')}
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
{t('marketing.pricing.subtitle')}
</p>
</div>
{/* Pricing Cards Preview */}
<div className="grid md:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto">
{/* Free */}
<div className="p-6 bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{t('marketing.pricing.tiers.free.name')}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
{t('marketing.pricing.tiers.free.description')}
</p>
<div className="mb-6">
<span className="text-4xl font-bold text-gray-900 dark:text-white">$0</span>
<span className="text-gray-500 dark:text-gray-400">{t('marketing.pricing.perMonth')}</span>
</div>
<Link
to="/signup"
className="block w-full py-3 px-4 text-center text-sm font-medium text-brand-600 bg-brand-50 dark:bg-brand-900/30 rounded-lg hover:bg-brand-100 dark:hover:bg-brand-900/50 transition-colors"
>
{t('marketing.pricing.getStarted')}
</Link>
</div>
{/* Professional - Highlighted */}
<div className="relative p-6 bg-brand-600 rounded-2xl shadow-xl shadow-brand-600/20">
<div className="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 bg-brand-500 text-white text-xs font-semibold rounded-full">
{t('marketing.pricing.mostPopular')}
</div>
<h3 className="text-lg font-semibold text-white mb-2">
{t('marketing.pricing.tiers.professional.name')}
</h3>
<p className="text-sm text-brand-100 mb-4">
{t('marketing.pricing.tiers.professional.description')}
</p>
<div className="mb-6">
<span className="text-4xl font-bold text-white">$29</span>
<span className="text-brand-200">{t('marketing.pricing.perMonth')}</span>
</div>
<Link
to="/signup"
className="block w-full py-3 px-4 text-center text-sm font-medium text-brand-600 bg-white rounded-lg hover:bg-brand-50 transition-colors"
>
{t('marketing.pricing.getStarted')}
</Link>
</div>
{/* Business */}
<div className="p-6 bg-white dark:bg-gray-800 rounded-2xl border border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-2">
{t('marketing.pricing.tiers.business.name')}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
{t('marketing.pricing.tiers.business.description')}
</p>
<div className="mb-6">
<span className="text-4xl font-bold text-gray-900 dark:text-white">$79</span>
<span className="text-gray-500 dark:text-gray-400">{t('marketing.pricing.perMonth')}</span>
</div>
<Link
to="/signup"
className="block w-full py-3 px-4 text-center text-sm font-medium text-brand-600 bg-brand-50 dark:bg-brand-900/30 rounded-lg hover:bg-brand-100 dark:hover:bg-brand-900/50 transition-colors"
>
{t('marketing.pricing.getStarted')}
</Link>
</div>
</div>
{/* View Full Pricing Link */}
<div className="text-center mt-10">
<Link
to="/pricing"
className="inline-flex items-center gap-2 text-brand-600 dark:text-brand-400 font-medium hover:text-brand-700 dark:hover:text-brand-300 transition-colors"
>
View full pricing details
<ArrowRight className="h-4 w-4" />
</Link>
</div>
</div>
</section>
{/* Final CTA */}
<CTASection />
</div>
);
};
export default HomePage;

View File

@@ -0,0 +1,124 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import PricingCard from '../../components/marketing/PricingCard';
import FAQAccordion from '../../components/marketing/FAQAccordion';
import CTASection from '../../components/marketing/CTASection';
const PricingPage: React.FC = () => {
const { t } = useTranslation();
const [billingPeriod, setBillingPeriod] = useState<'monthly' | 'annual'>('monthly');
const faqItems = [
{
question: t('marketing.faq.questions.freePlan.question'),
answer: t('marketing.faq.questions.freePlan.answer'),
},
{
question: t('marketing.faq.questions.cancel.question'),
answer: t('marketing.faq.questions.cancel.answer'),
},
{
question: t('marketing.faq.questions.payment.question'),
answer: t('marketing.faq.questions.payment.answer'),
},
{
question: t('marketing.faq.questions.migrate.question'),
answer: t('marketing.faq.questions.migrate.answer'),
},
{
question: t('marketing.faq.questions.support.question'),
answer: t('marketing.faq.questions.support.answer'),
},
{
question: t('marketing.faq.questions.customDomain.question'),
answer: t('marketing.faq.questions.customDomain.answer'),
},
];
return (
<div>
{/* Header Section */}
<section className="py-20 lg:py-28 bg-gradient-to-br from-white via-brand-50/30 to-white dark:from-gray-900 dark:via-gray-900 dark:to-gray-900">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h1 className="text-4xl sm:text-5xl font-bold text-gray-900 dark:text-white mb-4">
{t('marketing.pricing.title')}
</h1>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
{t('marketing.pricing.subtitle')}
</p>
</div>
{/* Billing Toggle */}
<div className="flex flex-wrap items-center justify-center gap-3 mb-12">
<div className="flex items-center gap-3">
<span
className={`text-sm font-medium whitespace-nowrap transition-colors ${
billingPeriod === 'monthly'
? 'text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400'
}`}
>
{t('marketing.pricing.monthly')}
</span>
<button
onClick={() => setBillingPeriod(billingPeriod === 'monthly' ? 'annual' : 'monthly')}
className="relative flex-shrink-0 w-12 h-6 bg-brand-600 rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900"
aria-label="Toggle billing period"
>
<span
className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform duration-200 ${
billingPeriod === 'annual' ? 'translate-x-6' : 'translate-x-0'
}`}
/>
</button>
<span
className={`text-sm font-medium whitespace-nowrap transition-colors ${
billingPeriod === 'annual'
? 'text-gray-900 dark:text-white'
: 'text-gray-500 dark:text-gray-400'
}`}
>
{t('marketing.pricing.annual')}
</span>
</div>
{billingPeriod === 'annual' && (
<span className="px-2 py-1 text-xs font-semibold text-brand-700 bg-brand-100 dark:bg-brand-900/30 dark:text-brand-300 rounded-full whitespace-nowrap">
{t('marketing.pricing.annualSave')}
</span>
)}
</div>
{/* Pricing Cards */}
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8">
<PricingCard tier="free" billingPeriod={billingPeriod} />
<PricingCard tier="professional" billingPeriod={billingPeriod} highlighted />
<PricingCard tier="business" billingPeriod={billingPeriod} />
<PricingCard tier="enterprise" billingPeriod={billingPeriod} />
</div>
</div>
</section>
{/* FAQ Section */}
<section className="py-20 lg:py-28 bg-gray-50 dark:bg-gray-800/50">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">
{t('marketing.faq.title')}
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
{t('marketing.faq.subtitle')}
</p>
</div>
<FAQAccordion items={faqItems} />
</div>
</section>
{/* CTA Section */}
<CTASection variant="minimal" />
</div>
);
};
export default PricingPage;

View File

@@ -0,0 +1,970 @@
import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useSearchParams } from 'react-router-dom';
import {
Building2,
User,
CreditCard,
CheckCircle,
ArrowRight,
ArrowLeft,
Check,
AlertCircle,
Loader2,
} from 'lucide-react';
import apiClient from '../../api/client';
interface SignupFormData {
// Step 1: Business info
businessName: string;
subdomain: string;
addressLine1: string;
addressLine2: string;
city: string;
state: string;
postalCode: string;
country: string;
phone: string;
// Step 2: User info
firstName: string;
lastName: string;
email: string;
password: string;
confirmPassword: string;
// Step 3: Plan selection
plan: 'free' | 'professional' | 'business' | 'enterprise';
}
const SignupPage: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [currentStep, setCurrentStep] = useState(1);
const firstNameRef = useRef<HTMLInputElement>(null);
// Focus on firstName when entering step 2
useEffect(() => {
if (currentStep === 2 && firstNameRef.current) {
firstNameRef.current.focus();
}
}, [currentStep]);
const [formData, setFormData] = useState<SignupFormData>({
businessName: '',
subdomain: '',
addressLine1: '',
addressLine2: '',
city: '',
state: '',
postalCode: '',
country: 'US',
phone: '',
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
plan: (searchParams.get('plan') as SignupFormData['plan']) || 'professional',
});
// Total steps: Business Info, User Info, Plan Selection, Confirmation
const totalSteps = 4;
const [errors, setErrors] = useState<Partial<Record<keyof SignupFormData, string>>>({});
const [subdomainAvailable, setSubdomainAvailable] = useState<boolean | null>(null);
const [checkingSubdomain, setCheckingSubdomain] = useState(false);
const [subdomainManuallyEdited, setSubdomainManuallyEdited] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitError, setSubmitError] = useState<string | null>(null);
const [signupComplete, setSignupComplete] = useState(false);
// Signup steps
const steps = [
{ number: 1, title: t('marketing.signup.steps.business'), icon: Building2 },
{ number: 2, title: t('marketing.signup.steps.account'), icon: User },
{ number: 3, title: t('marketing.signup.steps.plan'), icon: CreditCard },
{ number: 4, title: t('marketing.signup.steps.confirm'), icon: CheckCircle },
];
const plans = [
{
id: 'free' as const,
name: t('marketing.pricing.tiers.free.name'),
price: '$0',
period: t('marketing.pricing.period'),
features: [
t('marketing.pricing.tiers.free.features.0'),
t('marketing.pricing.tiers.free.features.1'),
t('marketing.pricing.tiers.free.features.2'),
],
},
{
id: 'professional' as const,
name: t('marketing.pricing.tiers.professional.name'),
price: '$29',
period: t('marketing.pricing.period'),
popular: true,
features: [
t('marketing.pricing.tiers.professional.features.0'),
t('marketing.pricing.tiers.professional.features.1'),
t('marketing.pricing.tiers.professional.features.2'),
t('marketing.pricing.tiers.professional.features.3'),
],
},
{
id: 'business' as const,
name: t('marketing.pricing.tiers.business.name'),
price: '$79',
period: t('marketing.pricing.period'),
features: [
t('marketing.pricing.tiers.business.features.0'),
t('marketing.pricing.tiers.business.features.1'),
t('marketing.pricing.tiers.business.features.2'),
t('marketing.pricing.tiers.business.features.3'),
],
},
{
id: 'enterprise' as const,
name: t('marketing.pricing.tiers.enterprise.name'),
price: t('marketing.pricing.tiers.enterprise.price'),
period: '',
features: [
t('marketing.pricing.tiers.enterprise.features.0'),
t('marketing.pricing.tiers.enterprise.features.1'),
t('marketing.pricing.tiers.enterprise.features.2'),
t('marketing.pricing.tiers.enterprise.features.3'),
],
},
];
// Generate subdomain from business name (only if user hasn't manually edited it)
useEffect(() => {
if (formData.businessName && !subdomainManuallyEdited) {
const generated = formData.businessName
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '')
.substring(0, 30);
setFormData((prev) => ({ ...prev, subdomain: generated }));
}
}, [formData.businessName, subdomainManuallyEdited]);
// Check subdomain availability with debounce
useEffect(() => {
if (!formData.subdomain || formData.subdomain.length < 3) {
setSubdomainAvailable(null);
return;
}
const timer = setTimeout(async () => {
setCheckingSubdomain(true);
try {
const response = await apiClient.post('/api/auth/signup/check-subdomain/', {
subdomain: formData.subdomain,
});
setSubdomainAvailable(response.data.available);
} catch {
setSubdomainAvailable(null);
} finally {
setCheckingSubdomain(false);
}
}, 500);
return () => clearTimeout(timer);
}, [formData.subdomain]);
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
setErrors((prev) => ({ ...prev, [name]: undefined }));
};
const handleSubdomainChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
setFormData((prev) => ({ ...prev, subdomain: value }));
setErrors((prev) => ({ ...prev, subdomain: undefined }));
setSubdomainAvailable(null);
setSubdomainManuallyEdited(true);
};
const validateStep = (step: number): boolean => {
const newErrors: Partial<Record<keyof SignupFormData, string>> = {};
if (step === 1) {
if (!formData.businessName.trim()) {
newErrors.businessName = t('marketing.signup.errors.businessNameRequired');
}
if (!formData.subdomain.trim()) {
newErrors.subdomain = t('marketing.signup.errors.subdomainRequired');
} else if (formData.subdomain.length < 3) {
newErrors.subdomain = t('marketing.signup.errors.subdomainTooShort');
} else if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(formData.subdomain) && formData.subdomain.length > 2) {
newErrors.subdomain = t('marketing.signup.errors.subdomainInvalid');
} else if (subdomainAvailable === false) {
newErrors.subdomain = t('marketing.signup.errors.subdomainTaken');
}
// Address validation
if (!formData.addressLine1.trim()) {
newErrors.addressLine1 = t('marketing.signup.errors.addressRequired');
}
if (!formData.city.trim()) {
newErrors.city = t('marketing.signup.errors.cityRequired');
}
if (!formData.state.trim()) {
newErrors.state = t('marketing.signup.errors.stateRequired');
}
if (!formData.postalCode.trim()) {
newErrors.postalCode = t('marketing.signup.errors.postalCodeRequired');
}
}
if (step === 2) {
if (!formData.firstName.trim()) {
newErrors.firstName = t('marketing.signup.errors.firstNameRequired');
}
if (!formData.lastName.trim()) {
newErrors.lastName = t('marketing.signup.errors.lastNameRequired');
}
if (!formData.email.trim()) {
newErrors.email = t('marketing.signup.errors.emailRequired');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
newErrors.email = t('marketing.signup.errors.emailInvalid');
}
if (!formData.password) {
newErrors.password = t('marketing.signup.errors.passwordRequired');
} else if (formData.password.length < 8) {
newErrors.password = t('marketing.signup.errors.passwordTooShort');
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = t('marketing.signup.errors.passwordMismatch');
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleNext = () => {
if (validateStep(currentStep)) {
setCurrentStep((prev) => Math.min(prev + 1, totalSteps));
}
};
const handleBack = () => {
setCurrentStep((prev) => Math.max(prev - 1, 1));
};
// Determine if current step is the confirmation step (last step)
const isConfirmationStep = currentStep === totalSteps;
const handleSubmit = async () => {
if (!validateStep(currentStep)) return;
setIsSubmitting(true);
setSubmitError(null);
try {
await apiClient.post('/api/auth/signup/', {
business_name: formData.businessName,
subdomain: formData.subdomain,
address_line1: formData.addressLine1,
address_line2: formData.addressLine2,
city: formData.city,
state: formData.state,
postal_code: formData.postalCode,
country: formData.country,
phone: formData.phone,
first_name: formData.firstName,
last_name: formData.lastName,
email: formData.email,
password: formData.password,
tier: formData.plan.toUpperCase(),
payments_enabled: false,
});
setSignupComplete(true);
} catch (error: any) {
const errorMessage =
error.response?.data?.detail ||
error.response?.data?.message ||
t('marketing.signup.errors.generic');
setSubmitError(errorMessage);
} finally {
setIsSubmitting(false);
}
};
if (signupComplete) {
return (
<div className="min-h-screen bg-gradient-to-br from-white via-brand-50/30 to-white dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-20">
<div className="max-w-lg mx-auto px-4 sm:px-6 lg:px-8">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-8 text-center">
<div className="w-20 h-20 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mx-auto mb-6">
<CheckCircle className="w-10 h-10 text-green-600 dark:text-green-400" />
</div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
{t('marketing.signup.success.title')}
</h1>
<p className="text-gray-600 dark:text-gray-400 mb-6">
{t('marketing.signup.success.message')}
</p>
<div className="bg-gray-50 dark:bg-gray-700 rounded-xl p-4 mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">
{t('marketing.signup.success.yourUrl')}
</p>
<p className="text-lg font-semibold text-brand-600 dark:text-brand-400">
{formData.subdomain}.smoothschedule.com
</p>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
{t('marketing.signup.success.checkEmail')}
</p>
<button
onClick={() => {
const port = window.location.port ? `:${window.location.port}` : '';
window.location.href = `http://${formData.subdomain}.lvh.me${port}/login`;
}}
className="w-full py-3 px-6 text-base font-semibold text-white bg-brand-600 rounded-xl hover:bg-brand-700 transition-colors"
>
{t('marketing.signup.success.goToLogin')}
</button>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-white via-brand-50/30 to-white dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12 lg:py-20">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-10">
<h1 className="text-3xl sm:text-4xl font-bold text-gray-900 dark:text-white mb-3">
{t('marketing.signup.title')}
</h1>
<p className="text-gray-600 dark:text-gray-400">
{t('marketing.signup.subtitle')}
</p>
</div>
{/* Progress Steps */}
<div className="mb-10">
<div className="flex items-center justify-between">
{steps.map((step, index) => (
<React.Fragment key={step.number}>
<div className="flex flex-col items-center">
<div
className={`w-12 h-12 rounded-full flex items-center justify-center transition-colors ${
currentStep > step.number
? 'bg-green-500 text-white'
: currentStep === step.number
? 'bg-brand-600 text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-400'
}`}
>
{currentStep > step.number ? (
<Check className="w-6 h-6" />
) : (
<step.icon className="w-6 h-6" />
)}
</div>
<span
className={`mt-2 text-xs font-medium hidden sm:block ${
currentStep >= step.number
? 'text-gray-900 dark:text-white'
: 'text-gray-400'
}`}
>
{step.title}
</span>
</div>
{index < steps.length - 1 && (
<div
className={`flex-1 h-1 mx-2 rounded ${
currentStep > step.number
? 'bg-green-500'
: 'bg-gray-200 dark:bg-gray-700'
}`}
/>
)}
</React.Fragment>
))}
</div>
</div>
{/* Form Card */}
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-6 sm:p-8">
{/* Step 1: Business Info */}
{currentStep === 1 && (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
{t('marketing.signup.businessInfo.title')}
</h2>
<div>
<label
htmlFor="businessName"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.businessInfo.name')}
</label>
<input
type="text"
id="businessName"
name="businessName"
value={formData.businessName}
onChange={handleInputChange}
autoComplete="organization"
placeholder={t('marketing.signup.businessInfo.namePlaceholder')}
className={`w-full px-4 py-3 rounded-xl border ${
errors.businessName
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.businessName && (
<p className="mt-1 text-sm text-red-500">{errors.businessName}</p>
)}
</div>
<div>
<label
htmlFor="subdomain"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.businessInfo.subdomain')}
</label>
<div className="flex items-center">
<input
type="text"
id="subdomain"
name="subdomain"
value={formData.subdomain}
onChange={handleSubdomainChange}
autoComplete="off"
placeholder="your-business"
className={`flex-1 px-4 py-3 rounded-l-xl border-y border-l ${
errors.subdomain
? 'border-red-500 focus:ring-red-500'
: subdomainAvailable === true
? 'border-green-500 focus:ring-green-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
<span className="px-4 py-3 bg-gray-100 dark:bg-gray-600 border border-gray-300 dark:border-gray-600 rounded-r-xl text-gray-500 dark:text-gray-400 text-sm">
.smoothschedule.com
</span>
</div>
<div className="mt-1 flex items-center gap-2">
{checkingSubdomain && (
<span className="text-sm text-gray-500 flex items-center gap-1">
<Loader2 className="w-4 h-4 animate-spin" />
{t('marketing.signup.businessInfo.checking')}
</span>
)}
{!checkingSubdomain && subdomainAvailable === true && (
<span className="text-sm text-green-500 flex items-center gap-1">
<Check className="w-4 h-4" />
{t('marketing.signup.businessInfo.available')}
</span>
)}
{!checkingSubdomain && subdomainAvailable === false && (
<span className="text-sm text-red-500 flex items-center gap-1">
<AlertCircle className="w-4 h-4" />
{t('marketing.signup.businessInfo.taken')}
</span>
)}
{errors.subdomain && !subdomainAvailable && (
<span className="text-sm text-red-500">{errors.subdomain}</span>
)}
</div>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
{t('marketing.signup.businessInfo.subdomainNote')}
</p>
</div>
{/* Business Address */}
<div className="border-t border-gray-200 dark:border-gray-700 pt-6 mt-6">
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4">
{t('marketing.signup.businessInfo.address')}
</h3>
<div className="space-y-4">
<div>
<label
htmlFor="addressLine1"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.businessInfo.addressLine1')}
</label>
<input
type="text"
id="addressLine1"
name="addressLine1"
value={formData.addressLine1}
onChange={handleInputChange}
autoComplete="address-line1"
placeholder={t('marketing.signup.businessInfo.addressLine1Placeholder')}
className={`w-full px-4 py-3 rounded-xl border ${
errors.addressLine1
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.addressLine1 && (
<p className="mt-1 text-sm text-red-500">{errors.addressLine1}</p>
)}
</div>
<div>
<label
htmlFor="addressLine2"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.businessInfo.addressLine2')}
</label>
<input
type="text"
id="addressLine2"
name="addressLine2"
value={formData.addressLine2}
onChange={handleInputChange}
autoComplete="address-line2"
placeholder={t('marketing.signup.businessInfo.addressLine2Placeholder')}
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 focus:ring-brand-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors"
/>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label
htmlFor="city"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.businessInfo.city')}
</label>
<input
type="text"
id="city"
name="city"
value={formData.city}
onChange={handleInputChange}
autoComplete="address-level2"
className={`w-full px-4 py-3 rounded-xl border ${
errors.city
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.city && (
<p className="mt-1 text-sm text-red-500">{errors.city}</p>
)}
</div>
<div>
<label
htmlFor="state"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.businessInfo.state')}
</label>
<input
type="text"
id="state"
name="state"
value={formData.state}
onChange={handleInputChange}
autoComplete="address-level1"
className={`w-full px-4 py-3 rounded-xl border ${
errors.state
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.state && (
<p className="mt-1 text-sm text-red-500">{errors.state}</p>
)}
</div>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label
htmlFor="postalCode"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.businessInfo.postalCode')}
</label>
<input
type="text"
id="postalCode"
name="postalCode"
value={formData.postalCode}
onChange={handleInputChange}
autoComplete="postal-code"
className={`w-full px-4 py-3 rounded-xl border ${
errors.postalCode
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.postalCode && (
<p className="mt-1 text-sm text-red-500">{errors.postalCode}</p>
)}
</div>
<div>
<label
htmlFor="phone"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.businessInfo.phone')}
</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleInputChange}
autoComplete="tel"
placeholder={t('marketing.signup.businessInfo.phonePlaceholder')}
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-600 focus:ring-brand-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors"
/>
</div>
</div>
</div>
</div>
</div>
)}
{/* Step 2: User Info */}
{currentStep === 2 && (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
{t('marketing.signup.accountInfo.title')}
</h2>
<div className="grid sm:grid-cols-2 gap-4">
<div>
<label
htmlFor="firstName"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.accountInfo.firstName')}
</label>
<input
ref={firstNameRef}
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
autoComplete="given-name"
className={`w-full px-4 py-3 rounded-xl border ${
errors.firstName
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.firstName && (
<p className="mt-1 text-sm text-red-500">{errors.firstName}</p>
)}
</div>
<div>
<label
htmlFor="lastName"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.accountInfo.lastName')}
</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
autoComplete="family-name"
className={`w-full px-4 py-3 rounded-xl border ${
errors.lastName
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.lastName && (
<p className="mt-1 text-sm text-red-500">{errors.lastName}</p>
)}
</div>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.accountInfo.email')}
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
autoComplete="email"
placeholder="you@example.com"
className={`w-full px-4 py-3 rounded-xl border ${
errors.email
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.email && (
<p className="mt-1 text-sm text-red-500">{errors.email}</p>
)}
</div>
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.accountInfo.password')}
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
autoComplete="new-password"
className={`w-full px-4 py-3 rounded-xl border ${
errors.password
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.password && (
<p className="mt-1 text-sm text-red-500">{errors.password}</p>
)}
</div>
<div>
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
{t('marketing.signup.accountInfo.confirmPassword')}
</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
autoComplete="new-password"
className={`w-full px-4 py-3 rounded-xl border ${
errors.confirmPassword
? 'border-red-500 focus:ring-red-500'
: 'border-gray-300 dark:border-gray-600 focus:ring-brand-500'
} bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 focus:ring-2 focus:border-transparent transition-colors`}
/>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-500">{errors.confirmPassword}</p>
)}
</div>
</div>
)}
{/* Step 3: Plan Selection */}
{currentStep === 3 && (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
{t('marketing.signup.planSelection.title')}
</h2>
<div className="grid sm:grid-cols-2 gap-4">
{plans.map((plan) => (
<button
key={plan.id}
type="button"
onClick={() => setFormData((prev) => ({ ...prev, plan: plan.id }))}
className={`relative text-left p-4 rounded-xl border-2 transition-all ${
formData.plan === plan.id
? 'border-brand-600 bg-brand-50 dark:bg-gray-800 dark:border-brand-500'
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800/50 hover:border-gray-300 dark:hover:border-gray-600'
}`}
>
{plan.popular && (
<span className="absolute -top-3 left-4 px-2 py-0.5 text-xs font-semibold text-white bg-brand-600 rounded-full">
{t('marketing.pricing.popular')}
</span>
)}
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="font-semibold text-gray-900 dark:text-white">
{plan.name}
</h3>
<p className="text-2xl font-bold text-gray-900 dark:text-white">
{plan.price}
{plan.period && (
<span className="text-sm font-normal text-gray-500">
/{plan.period}
</span>
)}
</p>
</div>
<div
className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
formData.plan === plan.id
? 'border-brand-600 bg-brand-600'
: 'border-gray-300 dark:border-gray-600'
}`}
>
{formData.plan === plan.id && (
<Check className="w-3 h-3 text-white" />
)}
</div>
</div>
<ul className="space-y-1">
{plan.features.slice(0, 3).map((feature, index) => (
<li
key={index}
className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-1"
>
<Check className="w-3 h-3 text-green-500 flex-shrink-0" />
{feature}
</li>
))}
</ul>
</button>
))}
</div>
</div>
)}
{/* Confirmation Step (last step) */}
{isConfirmationStep && (
<div className="space-y-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-6">
{t('marketing.signup.confirm.title')}
</h2>
<div className="space-y-4">
<div className="bg-gray-50 dark:bg-gray-700 rounded-xl p-4">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
{t('marketing.signup.confirm.business')}
</h3>
<p className="text-gray-900 dark:text-white font-medium">
{formData.businessName}
</p>
<p className="text-sm text-brand-600 dark:text-brand-400">
{formData.subdomain}.smoothschedule.com
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-700 rounded-xl p-4">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
{t('marketing.signup.confirm.account')}
</h3>
<p className="text-gray-900 dark:text-white font-medium">
{formData.firstName} {formData.lastName}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{formData.email}
</p>
</div>
<div className="bg-gray-50 dark:bg-gray-700 rounded-xl p-4">
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">
{t('marketing.signup.confirm.plan')}
</h3>
<p className="text-gray-900 dark:text-white font-medium">
{plans.find((p) => p.id === formData.plan)?.name}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{plans.find((p) => p.id === formData.plan)?.price}
{plans.find((p) => p.id === formData.plan)?.period &&
`/${plans.find((p) => p.id === formData.plan)?.period}`}
</p>
</div>
</div>
{submitError && (
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-xl p-4 flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-red-600 dark:text-red-400">{submitError}</p>
</div>
)}
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">
{t('marketing.signup.confirm.terms')}
</p>
</div>
)}
{/* Navigation Buttons */}
<div className="flex items-center justify-between mt-8 pt-6 border-t border-gray-200 dark:border-gray-700">
{currentStep > 1 ? (
<button
type="button"
onClick={handleBack}
className="flex items-center gap-2 px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors"
>
<ArrowLeft className="w-4 h-4" />
{t('marketing.signup.back')}
</button>
) : (
<div />
)}
{!isConfirmationStep ? (
<button
type="button"
onClick={handleNext}
className="flex items-center gap-2 px-6 py-3 bg-brand-600 text-white rounded-xl hover:bg-brand-700 transition-colors font-medium"
>
{t('marketing.signup.next')}
<ArrowRight className="w-4 h-4" />
</button>
) : (
<button
type="button"
onClick={handleSubmit}
disabled={isSubmitting}
className="flex items-center gap-2 px-6 py-3 bg-brand-600 text-white rounded-xl hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
{t('marketing.signup.creating')}
</>
) : (
<>
<CheckCircle className="w-4 h-4" />
{t('marketing.signup.createAccount')}
</>
)}
</button>
)}
</div>
</div>
{/* Login Link */}
<p className="text-center mt-6 text-gray-600 dark:text-gray-400">
{t('marketing.signup.haveAccount')}{' '}
<button
onClick={() => navigate('/login')}
className="text-brand-600 dark:text-brand-400 hover:underline font-medium"
>
{t('marketing.signup.signIn')}
</button>
</p>
</div>
</div>
);
};
export default SignupPage;