Add complete project files
This commit is contained in:
96
components/ServiceSection.tsx
Executable file
96
components/ServiceSection.tsx
Executable file
@@ -0,0 +1,96 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
type Service = {
|
||||
slug: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
blurb: string;
|
||||
whatWeDo: string[];
|
||||
outcomes: string[];
|
||||
iconName?: string;
|
||||
image?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
service: Service;
|
||||
index: number;
|
||||
};
|
||||
|
||||
export default function ServiceSection({ service, index }: Props) {
|
||||
const isReversed = index % 2 === 1;
|
||||
|
||||
return (
|
||||
<section id={service.slug} className="py-16 sm:py-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div
|
||||
className={`grid grid-cols-1 lg:grid-cols-2 gap-10 lg:gap-16 ${
|
||||
isReversed ? "lg:[&>*:first-child]:order-2" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Text column */}
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
{service.image && (
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-lg bg-navy mr-4">
|
||||
<Image
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
width={64}
|
||||
height={64}
|
||||
className="h-12 w-12 object-cover rounded"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm">{service.subtitle}</div>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold">{service.title}</h2>
|
||||
<p className="mt-3">{service.blurb}</p>
|
||||
|
||||
<div className="mt-8 grid grid-cols-1 sm:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">What We Do</h3>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{service.whatWeDo.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium mb-2">Outcomes</h3>
|
||||
<ul className="list-disc pl-5 space-y-1">
|
||||
{service.outcomes.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<a href={`/services/${service.slug}`} className="inline-block px-4 py-2 border rounded-md">
|
||||
Learn More About {service.title}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual card */}
|
||||
<div className="border rounded-xl p-8 flex flex-col items-center justify-center text-center">
|
||||
{service.image && (
|
||||
<div className="mb-4 h-24 w-24 border rounded-lg overflow-hidden">
|
||||
<Image
|
||||
src={service.image}
|
||||
alt={service.title}
|
||||
width={96}
|
||||
height={96}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-lg font-medium">{service.title}</div>
|
||||
<div className="text-sm text-gray-600">{service.subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
46
components/breadcrumbs.tsx
Executable file
46
components/breadcrumbs.tsx
Executable file
@@ -0,0 +1,46 @@
|
||||
import Link from 'next/link';
|
||||
import { ChevronRight, Home } from 'lucide-react';
|
||||
|
||||
interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export function Breadcrumbs({ items }: BreadcrumbsProps) {
|
||||
return (
|
||||
<nav className="flex" aria-label="Breadcrumb">
|
||||
<ol className="flex items-center space-x-2">
|
||||
<li>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-slate-500 hover:text-navy transition-colors"
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
<span className="sr-only">Home</span>
|
||||
</Link>
|
||||
</li>
|
||||
{items.map((item, index) => (
|
||||
<li key={index} className="flex items-center">
|
||||
<ChevronRight className="h-4 w-4 text-slate-400" />
|
||||
{item.href ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className="ml-2 text-sm text-slate-500 hover:text-navy transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="ml-2 text-sm font-medium text-ink">
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
187
components/contact-form.tsx
Executable file
187
components/contact-form.tsx
Executable file
@@ -0,0 +1,187 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { CheckCircle, AlertCircle } from 'lucide-react';
|
||||
|
||||
interface ContactFormData {
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function ContactForm() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<'idle' | 'success' | 'error'>('idle');
|
||||
const [formData, setFormData] = useState<ContactFormData>({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
message: '',
|
||||
});
|
||||
const [errors, setErrors] = useState<Partial<ContactFormData>>({});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Partial<ContactFormData> = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'Name is required';
|
||||
}
|
||||
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/\S+@\S+\.\S+/.test(formData.email)) {
|
||||
newErrors.email = 'Email is invalid';
|
||||
}
|
||||
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = 'Message is required';
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmitStatus('idle');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setSubmitStatus('success');
|
||||
setFormData({ name: '', email: '', phone: '', message: '' });
|
||||
} else {
|
||||
setSubmitStatus('error');
|
||||
}
|
||||
} catch (error) {
|
||||
setSubmitStatus('error');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (field: keyof ContactFormData, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
if (errors[field]) {
|
||||
setErrors(prev => ({ ...prev, [field]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{submitStatus === 'success' && (
|
||||
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-md">
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="h-5 w-5 text-green-600 mr-2" />
|
||||
<p className="text-green-800">
|
||||
Thank you for your message! We'll get back to you within 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{submitStatus === 'error' && (
|
||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||
<div className="flex items-center">
|
||||
<AlertCircle className="h-5 w-5 text-red-600 mr-2" />
|
||||
<p className="text-red-800">
|
||||
There was an error sending your message. Please try again or call us directly.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-ink mb-2">
|
||||
Full Name *
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
className={errors.name ? 'border-red-500' : ''}
|
||||
placeholder="Your full name"
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className="block text-sm font-medium text-ink mb-2">
|
||||
Email Address *
|
||||
</label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
className={errors.email ? 'border-red-500' : ''}
|
||||
placeholder="your.email@example.com"
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium text-ink mb-2">
|
||||
Phone Number (Optional)
|
||||
</label>
|
||||
<Input
|
||||
id="phone"
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
placeholder="(555) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="message" className="block text-sm font-medium text-ink mb-2">
|
||||
Message *
|
||||
</label>
|
||||
<Textarea
|
||||
id="message"
|
||||
value={formData.message}
|
||||
onChange={(e) => handleInputChange('message', e.target.value)}
|
||||
className={errors.message ? 'border-red-500' : ''}
|
||||
placeholder="Tell us about your tax needs..."
|
||||
rows={5}
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="mt-1 text-sm text-red-600">{errors.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Send Message'}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
components/cta-section.tsx
Executable file
71
components/cta-section.tsx
Executable file
@@ -0,0 +1,71 @@
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Phone, Mail } from 'lucide-react';
|
||||
|
||||
interface CTASectionProps {
|
||||
title: string;
|
||||
description: string;
|
||||
primaryCta: {
|
||||
text: string;
|
||||
href: string;
|
||||
icon?: 'phone' | 'mail';
|
||||
};
|
||||
secondaryCta?: {
|
||||
text: string;
|
||||
href: string;
|
||||
};
|
||||
variant?: 'default' | 'dark';
|
||||
}
|
||||
|
||||
export function CTASection({
|
||||
title,
|
||||
description,
|
||||
primaryCta,
|
||||
secondaryCta,
|
||||
variant = 'default',
|
||||
}: CTASectionProps) {
|
||||
const isDark = variant === 'dark';
|
||||
const bgClass = isDark ? 'bg-navy' : 'bg-teal';
|
||||
const textClass = isDark ? 'text-cloud' : 'text-cloud';
|
||||
|
||||
const getIcon = () => {
|
||||
if (primaryCta.icon === 'phone') return <Phone className="mr-2 h-4 w-4" />;
|
||||
if (primaryCta.icon === 'mail') return <Mail className="mr-2 h-4 w-4" />;
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${bgClass} py-16 sm:py-20`}>
|
||||
<div className="mx-auto max-w-7xl px-4 lg:px-6">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h2 className={`text-3xl font-bold tracking-tight ${textClass} sm:text-4xl`}>
|
||||
{title}
|
||||
</h2>
|
||||
<p className={`mt-6 text-lg leading-8 ${isDark ? 'text-slate-300' : 'text-slate-100'}`}>
|
||||
{description}
|
||||
</p>
|
||||
<div className="mt-8 flex items-center justify-center gap-x-6">
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant={isDark ? 'secondary' : 'default'}
|
||||
>
|
||||
<Link href={primaryCta.href}>
|
||||
{getIcon()}
|
||||
{primaryCta.text}
|
||||
</Link>
|
||||
</Button>
|
||||
{secondaryCta && (
|
||||
<Link
|
||||
href={secondaryCta.href}
|
||||
className={`text-sm font-semibold leading-6 ${textClass} hover:opacity-80 transition-opacity`}
|
||||
>
|
||||
{secondaryCta.text} <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
components/hero.tsx
Executable file
66
components/hero.tsx
Executable file
@@ -0,0 +1,66 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface HeroProps {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
ctaText: string;
|
||||
ctaHref: string;
|
||||
imageSrc: string;
|
||||
imageAlt: string;
|
||||
reverse?: boolean;
|
||||
}
|
||||
|
||||
export function Hero({
|
||||
title,
|
||||
subtitle,
|
||||
ctaText,
|
||||
ctaHref,
|
||||
imageSrc,
|
||||
imageAlt,
|
||||
reverse = false,
|
||||
}: HeroProps) {
|
||||
return (
|
||||
<div className="relative isolate overflow-hidden bg-navy">
|
||||
<div className="mx-auto max-w-7xl px-4 pb-16 pt-8 sm:pb-24 lg:flex lg:px-6 lg:py-24">
|
||||
<div className={`mx-auto max-w-2xl flex-shrink-0 lg:mx-0 lg:max-w-xl lg:pt-8 ${reverse ? 'lg:order-2' : ''}`}>
|
||||
<div className="mt-16 sm:mt-20 lg:mt-12">
|
||||
<Link href="/contact" className="inline-flex space-x-6">
|
||||
<span className="rounded-full bg-teal/10 px-3 py-1 text-sm font-semibold leading-6 text-teal ring-1 ring-inset ring-teal/10">
|
||||
Book Your Consultation
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
<h1 className="mt-10 text-4xl font-bold tracking-tight text-cloud sm:text-6xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mt-6 text-lg leading-8 text-cloud/90">
|
||||
{subtitle}
|
||||
</p>
|
||||
<div className="mt-10 flex items-center gap-x-6">
|
||||
<Button asChild size="lg">
|
||||
<Link href={ctaHref}>
|
||||
{ctaText}
|
||||
</Link>
|
||||
</Button>
|
||||
<Link href="/about" className="text-sm font-semibold leading-6 text-cloud hover:text-cloud/80 transition-colors">
|
||||
Learn more <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`mx-auto mt-12 flex max-w-2xl sm:mt-16 lg:ml-8 lg:mr-0 lg:mt-0 lg:max-w-none lg:flex-none xl:ml-20 ${reverse ? 'lg:order-1' : ''}`}>
|
||||
<div className="max-w-2xl flex-none sm:max-w-3xl lg:max-w-none">
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt={imageAlt}
|
||||
width={1200}
|
||||
height={800}
|
||||
className="w-[40rem] rounded-md bg-white/5 shadow-2xl ring-1 ring-white/10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
components/process-steps.tsx
Executable file
56
components/process-steps.tsx
Executable file
@@ -0,0 +1,56 @@
|
||||
import { Calendar, Upload, CheckCircle } from 'lucide-react';
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Book',
|
||||
description: 'Schedule your consultation with our tax experts',
|
||||
icon: Calendar,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Prepare',
|
||||
description: 'Securely upload your documents and we prepare your returns',
|
||||
icon: Upload,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Delivered',
|
||||
description: 'Receive your completed returns with detailed explanations',
|
||||
icon: CheckCircle,
|
||||
},
|
||||
];
|
||||
|
||||
export function ProcessSteps() {
|
||||
return (
|
||||
<div className="bg-cloud py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 lg:px-6">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-ink sm:text-4xl">
|
||||
Simple 3-Step Process
|
||||
</h2>
|
||||
<p className="mt-6 text-lg leading-8 text-slate">
|
||||
We make tax preparation straightforward and stress-free. Here's how we work together to get your taxes done right.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 max-w-2xl sm:mt-16 lg:mt-20 lg:max-w-none">
|
||||
<dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-16 lg:max-w-none lg:grid-cols-3">
|
||||
{steps.map((step) => (
|
||||
<div key={step.id} className="flex flex-col">
|
||||
<dt className="flex items-center gap-x-3 text-base font-semibold leading-7 text-ink">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-navy">
|
||||
<step.icon className="h-6 w-6 text-cloud" aria-hidden="true" />
|
||||
</div>
|
||||
Step {step.id}: {step.name}
|
||||
</dt>
|
||||
<dd className="mt-4 flex flex-auto flex-col text-base leading-7 text-slate">
|
||||
<p className="flex-auto">{step.description}</p>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
115
components/secure-upload.tsx
Executable file
115
components/secure-upload.tsx
Executable file
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Upload, File, X, CheckCircle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
export function SecureUpload() {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const handleFileSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFiles = Array.from(event.target.files || []);
|
||||
setFiles(prev => [...prev, ...selectedFiles]);
|
||||
};
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
setFiles(prev => prev.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
setIsUploading(true);
|
||||
// TODO: Implement actual file upload to S3 or similar service
|
||||
// This is a stub implementation
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setIsUploading(false);
|
||||
setFiles([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Secure Document Upload</CardTitle>
|
||||
<CardDescription>
|
||||
Upload your tax documents securely. All files are encrypted and stored safely.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="border-2 border-dashed border-slate-300 rounded-lg p-6 text-center">
|
||||
<Upload className="mx-auto h-12 w-12 text-slate-400" />
|
||||
<div className="mt-4">
|
||||
<label htmlFor="file-upload" className="cursor-pointer">
|
||||
<span className="text-sm font-medium text-navy hover:text-teal">
|
||||
Click to upload
|
||||
</span>
|
||||
<span className="text-slate-500"> or drag and drop</span>
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
type="file"
|
||||
multiple
|
||||
className="sr-only"
|
||||
onChange={handleFileSelect}
|
||||
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
PDF, JPG, PNG, DOC up to 10MB each
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h4 className="font-medium text-ink">Selected Files:</h4>
|
||||
{files.map((file, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-sand rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<File className="h-5 w-5 text-navy" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-ink">{file.name}</p>
|
||||
<p className="text-xs text-slate">
|
||||
{(file.size / 1024 / 1024).toFixed(2)} MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeFile(index)}
|
||||
className="text-slate-400 hover:text-red-500"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<CheckCircle className="h-5 w-5 text-blue-600 mt-0.5" />
|
||||
<div className="text-sm text-blue-800">
|
||||
<p className="font-medium">Secure & Private</p>
|
||||
<p className="mt-1">
|
||||
Your documents are encrypted and stored securely. Only authorized personnel can access your files.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleUpload}
|
||||
disabled={files.length === 0 || isUploading}
|
||||
className="w-full"
|
||||
size="lg"
|
||||
>
|
||||
{isUploading ? 'Uploading...' : 'Upload Documents'}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-slate-500 text-center">
|
||||
Note: This is a demo interface. In production, this would integrate with a secure file storage service.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
51
components/service-card.tsx
Executable file
51
components/service-card.tsx
Executable file
@@ -0,0 +1,51 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
|
||||
interface ServiceCardProps {
|
||||
title: string;
|
||||
description: string;
|
||||
image: string;
|
||||
imageAlt: string;
|
||||
href: string;
|
||||
features?: string[];
|
||||
}
|
||||
|
||||
export function ServiceCard({ title, description, image, imageAlt, href, features }: ServiceCardProps) {
|
||||
return (
|
||||
<Card className="overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<div className="aspect-square relative">
|
||||
<Image
|
||||
src={image}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl">{title}</CardTitle>
|
||||
<CardDescription className="text-base">{description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{features && (
|
||||
<ul className="space-y-2 mb-6">
|
||||
{features.slice(0, 3).map((feature, index) => (
|
||||
<li key={index} className="flex items-center text-sm text-slate">
|
||||
<div className="w-1.5 h-1.5 bg-teal rounded-full mr-2" />
|
||||
{feature}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<Button asChild className="w-full">
|
||||
<Link href={href}>
|
||||
Learn More
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
123
components/site-footer.tsx
Executable file
123
components/site-footer.tsx
Executable file
@@ -0,0 +1,123 @@
|
||||
import Link from 'next/link';
|
||||
import { Mail, Phone, MapPin, Clock } from 'lucide-react';
|
||||
|
||||
const navigation = {
|
||||
services: [
|
||||
{ name: 'Public Accounting', href: '/services/public-accounting' },
|
||||
{ name: 'Federal Income Tax', href: '/services/federal-income-tax' },
|
||||
{ name: 'Estate Tax Planning', href: '/services/estate-tax-planning' },
|
||||
{ name: 'Financial Planning', href: '/services/financial-planning' },
|
||||
{ name: 'Advisory Services', href: '/services/advisory' },
|
||||
],
|
||||
company: [
|
||||
{ name: 'About', href: '/about' },
|
||||
{ name: 'Pricing', href: '/pricing' },
|
||||
{ name: 'Reviews', href: '/reviews' },
|
||||
{ name: 'Contact', href: '/contact' },
|
||||
],
|
||||
legal: [
|
||||
{ name: 'Privacy Policy', href: '/legal/privacy' },
|
||||
{ name: 'Terms of Service', href: '/legal/terms' },
|
||||
],
|
||||
};
|
||||
|
||||
export function SiteFooter() {
|
||||
return (
|
||||
<footer className="bg-navy text-cloud" aria-labelledby="footer-heading">
|
||||
<h2 id="footer-heading" className="sr-only">
|
||||
Footer
|
||||
</h2>
|
||||
<div className="mx-auto max-w-7xl px-4 pb-6 pt-12 sm:pt-16 lg:px-6 lg:pt-20">
|
||||
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
|
||||
<div className="space-y-8">
|
||||
<div className="text-2xl font-bold">Hampton, Brown & Associates, PC</div>
|
||||
<p className="text-sm leading-6 text-slate-300">
|
||||
Expert federal income and estate tax services, financial planning, and goal-oriented strategies in Corpus Christi, Texas.
|
||||
We assist our clients with their tax and accounting needs and achieving economic goals.
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-3">
|
||||
<MapPin className="h-5 w-5 text-teal" />
|
||||
<span className="text-sm">Corpus Christi, TX</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Phone className="h-5 w-5 text-teal" />
|
||||
<span className="text-sm">(361) 888-7711</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Mail className="h-5 w-5 text-teal" />
|
||||
<span className="text-sm">info@hamptonbrown.com</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<Clock className="h-5 w-5 text-teal" />
|
||||
<span className="text-sm">Mon-Fri 9AM-5PM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-16 grid grid-cols-2 gap-8 xl:col-span-2 xl:mt-0">
|
||||
<div className="md:grid md:grid-cols-2 md:gap-8">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold leading-6">Services</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
{navigation.services.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link href={item.href} className="text-sm leading-6 text-slate-300 hover:text-cloud transition-colors">
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-10 md:mt-0">
|
||||
<h3 className="text-sm font-semibold leading-6">Company</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
{navigation.company.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link href={item.href} className="text-sm leading-6 text-slate-300 hover:text-cloud transition-colors">
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:grid md:grid-cols-2 md:gap-8">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold leading-6">Legal</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
{navigation.legal.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link href={item.href} className="text-sm leading-6 text-slate-300 hover:text-cloud transition-colors">
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-10 md:mt-0">
|
||||
<h3 className="text-sm font-semibold leading-6">Resources</h3>
|
||||
<ul role="list" className="mt-6 space-y-4">
|
||||
<li>
|
||||
<Link href="/resources" className="text-sm leading-6 text-slate-300 hover:text-cloud transition-colors">
|
||||
Secure Upload
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link href="/resources" className="text-sm leading-6 text-slate-300 hover:text-cloud transition-colors">
|
||||
Payment Portal
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 border-t border-slate-700 pt-6 sm:mt-16 lg:mt-20">
|
||||
<p className="text-xs leading-5 text-slate-400">
|
||||
© {new Date().getFullYear()} Hampton, Brown & Associates, PC. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
107
components/site-header.tsx
Executable file
107
components/site-header.tsx
Executable file
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { Menu, X, Phone } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Services', href: '/services' },
|
||||
{ name: 'About', href: '/about' },
|
||||
{ name: 'Resources', href: '/resources' },
|
||||
{ name: 'Reviews', href: '/reviews' },
|
||||
{ name: 'Contact', href: '/contact' },
|
||||
];
|
||||
|
||||
export function SiteHeader() {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<header className="bg-cloud shadow-sm">
|
||||
<nav className="mx-auto flex max-w-7xl items-center justify-between p-4 lg:px-6" aria-label="Global">
|
||||
<div className="flex lg:flex-1">
|
||||
<Link href="/" className="-m-1.5 p-1.5">
|
||||
<span className="sr-only">Hampton Brown & Associates, PC</span>
|
||||
<div className="text-2xl font-bold text-navy">Hampton Brown & Associates, PC</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 inline-flex items-center justify-center rounded-md p-2.5 text-slate"
|
||||
onClick={() => setMobileMenuOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open main menu</span>
|
||||
<Menu className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="hidden lg:flex lg:gap-x-12">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="text-sm font-semibold leading-6 text-ink hover:text-navy transition-colors"
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden lg:flex lg:flex-1 lg:justify-end">
|
||||
<Button asChild>
|
||||
<Link href="/contact">
|
||||
<Phone className="mr-2 h-4 w-4" />
|
||||
Book a Call
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{mobileMenuOpen && (
|
||||
<div className="lg:hidden">
|
||||
<div className="fixed inset-0 z-50" />
|
||||
<div className="fixed inset-y-0 right-0 z-50 w-full overflow-y-auto bg-cloud px-6 py-6 sm:max-w-sm sm:ring-1 sm:ring-slate-900/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href="/" className="-m-1.5 p-1.5">
|
||||
<span className="sr-only">Hampton Brown & Associates, PC</span>
|
||||
<div className="text-2xl font-bold text-navy">Hampton Brown & Associates, PC</div>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 rounded-md p-2.5 text-slate"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close menu</span>
|
||||
<X className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-6 flow-root">
|
||||
<div className="-my-6 divide-y divide-slate-500/10">
|
||||
<div className="space-y-2 py-6">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="-mx-3 block rounded-lg px-3 py-2 text-base font-semibold leading-7 text-ink hover:bg-sand"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="py-6">
|
||||
<Button asChild className="w-full">
|
||||
<Link href="/contact">
|
||||
<Phone className="mr-2 h-4 w-4" />
|
||||
Book a Call
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
44
components/testimonial.tsx
Executable file
44
components/testimonial.tsx
Executable file
@@ -0,0 +1,44 @@
|
||||
import { Star } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
interface TestimonialProps {
|
||||
quote: string;
|
||||
author: string;
|
||||
role: string;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export function Testimonial({ quote, author, role, rating }: TestimonialProps) {
|
||||
return (
|
||||
<Card className="bg-sand border-0">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex mb-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`h-5 w-5 ${
|
||||
i < rating ? 'text-yellow-400 fill-current' : 'text-slate-300'
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<blockquote className="text-lg font-medium text-ink mb-4">
|
||||
"{quote}"
|
||||
</blockquote>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-10 h-10 bg-navy rounded-full flex items-center justify-center">
|
||||
<span className="text-cloud font-semibold text-sm">
|
||||
{author.split(' ').map(n => n[0]).join('')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-semibold text-ink">{author}</p>
|
||||
<p className="text-sm text-slate">{role}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
56
components/trust-panel.tsx
Executable file
56
components/trust-panel.tsx
Executable file
@@ -0,0 +1,56 @@
|
||||
import { Shield, Lock, CheckCircle } from 'lucide-react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
const trustFeatures = [
|
||||
{
|
||||
icon: Shield,
|
||||
title: 'Bank-Level Security',
|
||||
description: 'Your data is protected with enterprise-grade encryption',
|
||||
},
|
||||
{
|
||||
icon: Lock,
|
||||
title: 'Secure Client Portal',
|
||||
description: 'Private, password-protected access to your documents',
|
||||
},
|
||||
{
|
||||
icon: CheckCircle,
|
||||
title: 'IRS Certified',
|
||||
description: 'Our preparers are IRS certified and stay current with tax law',
|
||||
},
|
||||
];
|
||||
|
||||
export function TrustPanel() {
|
||||
return (
|
||||
<div className="bg-navy py-16 sm:py-20">
|
||||
<div className="mx-auto max-w-7xl px-4 lg:px-6">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h2 className="text-3xl font-bold tracking-tight text-cloud sm:text-4xl">
|
||||
Your Security & Trust Are Our Priority
|
||||
</h2>
|
||||
<p className="mt-6 text-lg leading-8 text-slate-300">
|
||||
We understand the sensitive nature of your financial information. That's why we've implemented the highest security standards to protect your data.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto mt-12 max-w-2xl sm:mt-16 lg:mt-20 lg:max-w-none">
|
||||
<dl className="grid max-w-xl grid-cols-1 gap-x-8 gap-y-16 lg:max-w-none lg:grid-cols-3">
|
||||
{trustFeatures.map((feature) => (
|
||||
<Card key={feature.title} className="bg-slate-800 border-slate-700">
|
||||
<CardHeader>
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-teal">
|
||||
<feature.icon className="h-6 w-6 text-cloud" aria-hidden="true" />
|
||||
</div>
|
||||
<CardTitle className="text-cloud">{feature.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-slate-300">
|
||||
{feature.description}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
components/ui/badge.tsx
Executable file
36
components/ui/badge.tsx
Executable file
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-navy text-cloud hover:bg-navy/80',
|
||||
secondary:
|
||||
'border-transparent bg-teal text-cloud hover:bg-teal/80',
|
||||
destructive:
|
||||
'border-transparent bg-red-500 text-cloud hover:bg-red-500/80',
|
||||
outline: 'text-navy',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
56
components/ui/button.tsx
Executable file
56
components/ui/button.tsx
Executable file
@@ -0,0 +1,56 @@
|
||||
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 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-navy text-cloud hover:bg-navy/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-teal text-cloud hover:bg-teal/90',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-navy underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
79
components/ui/card.tsx
Executable file
79
components/ui/card.tsx
Executable file
@@ -0,0 +1,79 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border border-slate-200 bg-cloud text-ink shadow-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = 'Card';
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-2xl font-semibold leading-none tracking-tight text-navy',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = 'CardTitle';
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn('text-sm text-slate', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = 'CardDescription';
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = 'CardContent';
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
||||
25
components/ui/input.tsx
Executable file
25
components/ui/input.tsx
Executable file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-slate-200 bg-cloud px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-navy focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
24
components/ui/textarea.tsx
Executable file
24
components/ui/textarea.tsx
Executable file
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
'flex min-h-[80px] w-full rounded-md border border-slate-200 bg-cloud px-3 py-2 text-sm ring-offset-background placeholder:text-slate-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-navy focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export { Textarea };
|
||||
Reference in New Issue
Block a user