first commit
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import { Locale, localeNames, localeFlags } from '@/i18n';
|
||||
import { ChevronDownIcon, GlobeAltIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface LanguageToggleProps {
|
||||
variant?: 'dropdown' | 'buttons';
|
||||
showFlags?: boolean;
|
||||
}
|
||||
|
||||
export default function LanguageToggle({
|
||||
variant = 'dropdown',
|
||||
showFlags = true
|
||||
}: LanguageToggleProps) {
|
||||
const { locale, setLocale } = useLanguage();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const availableLocales = Object.keys(localeNames) as Locale[];
|
||||
|
||||
if (variant === 'buttons') {
|
||||
return (
|
||||
<div className="flex items-center gap-1 bg-secondary-gray rounded-btn p-1">
|
||||
{availableLocales.map((loc) => (
|
||||
<button
|
||||
key={loc}
|
||||
onClick={() => setLocale(loc)}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded-btn text-sm font-medium transition-colors',
|
||||
{
|
||||
'bg-white shadow-sm text-primary-dark': locale === loc,
|
||||
'text-gray-600 hover:text-primary-dark': locale !== loc,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{showFlags && <span className="mr-1">{localeFlags[loc]}</span>}
|
||||
{loc.toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-2 px-3 py-2 rounded-btn hover:bg-secondary-gray transition-colors"
|
||||
>
|
||||
<GlobeAltIcon className="w-5 h-5 text-gray-600" />
|
||||
{showFlags && <span>{localeFlags[locale]}</span>}
|
||||
<span className="text-sm font-medium">{localeNames[locale]}</span>
|
||||
<ChevronDownIcon
|
||||
className={clsx(
|
||||
'w-4 h-4 text-gray-500 transition-transform',
|
||||
{ 'rotate-180': isOpen }
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0 z-10"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<div className="absolute right-0 mt-2 w-40 bg-white rounded-card shadow-card-hover border border-secondary-light-gray z-20 overflow-hidden">
|
||||
{availableLocales.map((loc) => (
|
||||
<button
|
||||
key={loc}
|
||||
onClick={() => {
|
||||
setLocale(loc);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className={clsx(
|
||||
'w-full flex items-center gap-2 px-4 py-2.5 text-left transition-colors',
|
||||
{
|
||||
'bg-secondary-gray': locale === loc,
|
||||
'hover:bg-gray-50': locale !== loc,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{showFlags && <span>{localeFlags[loc]}</span>}
|
||||
<span className="text-sm font-medium">{localeNames[loc]}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import {
|
||||
ShareIcon,
|
||||
LinkIcon,
|
||||
CheckIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
interface ShareButtonsProps {
|
||||
title: string;
|
||||
url?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function ShareButtons({ title, url, description }: ShareButtonsProps) {
|
||||
const { locale } = useLanguage();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
// Use provided URL or current page URL
|
||||
const shareUrl = url || (typeof window !== 'undefined' ? window.location.href : '');
|
||||
const shareText = description || title;
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
setCopied(true);
|
||||
toast.success(locale === 'es' ? 'Enlace copiado' : 'Link copied');
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
toast.error(locale === 'es' ? 'Error al copiar' : 'Failed to copy');
|
||||
}
|
||||
};
|
||||
|
||||
const shareToWhatsApp = () => {
|
||||
const text = encodeURIComponent(`${shareText}\n\n${shareUrl}`);
|
||||
window.open(`https://wa.me/?text=${text}`, '_blank');
|
||||
};
|
||||
|
||||
const shareToFacebook = () => {
|
||||
const encodedUrl = encodeURIComponent(shareUrl);
|
||||
window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, '_blank', 'width=600,height=400');
|
||||
};
|
||||
|
||||
const shareToTwitter = () => {
|
||||
const text = encodeURIComponent(shareText);
|
||||
const encodedUrl = encodeURIComponent(shareUrl);
|
||||
window.open(`https://twitter.com/intent/tweet?text=${text}&url=${encodedUrl}`, '_blank', 'width=600,height=400');
|
||||
};
|
||||
|
||||
const shareToLinkedIn = () => {
|
||||
const encodedUrl = encodeURIComponent(shareUrl);
|
||||
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`, '_blank', 'width=600,height=400');
|
||||
};
|
||||
|
||||
const handleNativeShare = async () => {
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title,
|
||||
text: shareText,
|
||||
url: shareUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
// User cancelled or error
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm font-medium text-gray-600">
|
||||
{locale === 'es' ? 'Compartir evento' : 'Share event'}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{/* WhatsApp */}
|
||||
<button
|
||||
onClick={shareToWhatsApp}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-[#25D366] text-white hover:opacity-90 transition-opacity"
|
||||
title="WhatsApp"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Facebook */}
|
||||
<button
|
||||
onClick={shareToFacebook}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-[#1877F2] text-white hover:opacity-90 transition-opacity"
|
||||
title="Facebook"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Twitter/X */}
|
||||
<button
|
||||
onClick={shareToTwitter}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-black text-white hover:opacity-90 transition-opacity"
|
||||
title="X (Twitter)"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* LinkedIn */}
|
||||
<button
|
||||
onClick={shareToLinkedIn}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-[#0A66C2] text-white hover:opacity-90 transition-opacity"
|
||||
title="LinkedIn"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Copy Link */}
|
||||
<button
|
||||
onClick={handleCopyLink}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors"
|
||||
title={locale === 'es' ? 'Copiar enlace' : 'Copy link'}
|
||||
>
|
||||
{copied ? (
|
||||
<CheckIcon className="w-5 h-5 text-green-600" />
|
||||
) : (
|
||||
<LinkIcon className="w-5 h-5" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Native Share (mobile) */}
|
||||
{typeof navigator !== 'undefined' && typeof navigator.share === 'function' && (
|
||||
<button
|
||||
onClick={handleNativeShare}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-primary-yellow text-primary-dark hover:bg-primary-yellow/90 transition-colors"
|
||||
title={locale === 'es' ? 'Más opciones' : 'More options'}
|
||||
>
|
||||
<ShareIcon className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import { getSocialLinks, socialIcons } from '@/lib/socialLinks';
|
||||
|
||||
const legalLinks = [
|
||||
{ slug: 'terms-policy', en: 'Terms & Conditions', es: 'Términos y Condiciones' },
|
||||
{ slug: 'privacy-policy', en: 'Privacy Policy', es: 'Política de Privacidad' },
|
||||
{ slug: 'refund-cancelation-policy', en: 'Refund Policy', es: 'Política de Reembolso' },
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const { t, locale } = useLanguage();
|
||||
const currentYear = new Date().getFullYear();
|
||||
const socialLinks = getSocialLinks();
|
||||
|
||||
return (
|
||||
<footer className="bg-secondary-gray border-t border-secondary-light-gray">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
{/* Brand */}
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<Link href="/" className="inline-block">
|
||||
<span className="text-2xl font-bold font-heading text-primary-dark">
|
||||
Span<span className="text-primary-yellow">glish</span>
|
||||
</span>
|
||||
</Link>
|
||||
<p className="mt-3 text-gray-600 max-w-md">
|
||||
{t('footer.tagline')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div>
|
||||
<h3 className="font-semibold text-primary-dark mb-4">
|
||||
{t('footer.links')}
|
||||
</h3>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link
|
||||
href="/events"
|
||||
className="text-gray-600 hover:text-primary-dark transition-colors"
|
||||
>
|
||||
{t('nav.events')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/community"
|
||||
className="text-gray-600 hover:text-primary-dark transition-colors"
|
||||
>
|
||||
{t('nav.community')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="text-gray-600 hover:text-primary-dark transition-colors"
|
||||
>
|
||||
{t('nav.contact')}
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
href="/faq"
|
||||
className="text-gray-600 hover:text-primary-dark transition-colors"
|
||||
>
|
||||
{t('nav.faq')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Social */}
|
||||
{socialLinks.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold text-primary-dark mb-4">
|
||||
{t('footer.social')}
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{socialLinks.map((link) => (
|
||||
<a
|
||||
key={link.type}
|
||||
href={link.url}
|
||||
target={link.type === 'email' ? undefined : '_blank'}
|
||||
rel={link.type === 'email' ? undefined : 'noopener noreferrer'}
|
||||
className="w-10 h-10 flex items-center justify-center rounded-full bg-white shadow-sm hover:shadow-md hover:bg-primary-yellow/10 transition-all"
|
||||
title={link.label}
|
||||
>
|
||||
{socialIcons[link.type]}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legal Links */}
|
||||
<div className="border-t border-secondary-light-gray mt-10 pt-8">
|
||||
<div className="flex flex-wrap justify-center gap-4 md:gap-6 mb-4">
|
||||
{legalLinks.map((link) => (
|
||||
<Link
|
||||
key={link.slug}
|
||||
href={`/legal/${link.slug}`}
|
||||
className="text-gray-500 hover:text-primary-dark transition-colors text-sm"
|
||||
>
|
||||
{locale === 'es' ? link.es : link.en}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="text-center text-gray-500 text-sm">
|
||||
{t('footer.copyright', { year: currentYear })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { useLanguage } from '@/context/LanguageContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import LanguageToggle from '@/components/LanguageToggle';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export default function Header() {
|
||||
const { t } = useLanguage();
|
||||
const { user, isAdmin, logout } = useAuth();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/', label: t('nav.home') },
|
||||
{ href: '/events', label: t('nav.events') },
|
||||
{ href: '/community', label: t('nav.community') },
|
||||
{ href: '/contact', label: t('nav.contact') },
|
||||
];
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white shadow-sm">
|
||||
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<span className="text-2xl font-bold font-heading text-primary-dark">
|
||||
Span<span className="text-primary-yellow">glish</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-gray-700 hover:text-primary-dark font-medium transition-colors"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right side actions */}
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
<LanguageToggle />
|
||||
|
||||
{user ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/dashboard">
|
||||
<Button variant="ghost" size="sm">
|
||||
{t('nav.dashboard')}
|
||||
</Button>
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
<Link href="/admin">
|
||||
<Button variant="ghost" size="sm">
|
||||
{t('nav.admin')}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<span className="text-sm text-gray-600">{user.name}</span>
|
||||
<Button variant="outline" size="sm" onClick={logout}>
|
||||
{t('nav.logout')}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/login">
|
||||
<Button variant="ghost" size="sm">
|
||||
{t('nav.login')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/events">
|
||||
<Button size="sm">
|
||||
{t('nav.joinEvent')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<button
|
||||
className="md:hidden p-2 rounded-lg hover:bg-gray-100"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
>
|
||||
{mobileMenuOpen ? (
|
||||
<XMarkIcon className="w-6 h-6" />
|
||||
) : (
|
||||
<Bars3Icon className="w-6 h-6" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<div
|
||||
className={clsx(
|
||||
'md:hidden overflow-hidden transition-all duration-300',
|
||||
{
|
||||
'max-h-0': !mobileMenuOpen,
|
||||
'max-h-96 pb-4': mobileMenuOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-2 pt-4">
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="px-4 py-2 text-gray-700 hover:bg-gray-50 rounded-lg font-medium"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className="border-t border-gray-100 mt-2 pt-4 px-4">
|
||||
<LanguageToggle variant="buttons" />
|
||||
</div>
|
||||
|
||||
<div className="px-4 pt-2 flex flex-col gap-2">
|
||||
{user ? (
|
||||
<>
|
||||
<Link href="/dashboard" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button variant="outline" className="w-full">
|
||||
{t('nav.dashboard')}
|
||||
</Button>
|
||||
</Link>
|
||||
{isAdmin && (
|
||||
<Link href="/admin" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button variant="outline" className="w-full">
|
||||
{t('nav.admin')}
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Button variant="secondary" onClick={logout} className="w-full">
|
||||
{t('nav.logout')}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button variant="outline" className="w-full">
|
||||
{t('nav.login')}
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/events" onClick={() => setMobileMenuOpen(false)}>
|
||||
<Button className="w-full">
|
||||
{t('nav.joinEvent')}
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
'use client';
|
||||
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import Link from 'next/link';
|
||||
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
|
||||
|
||||
interface LegalPageLayoutProps {
|
||||
title: string;
|
||||
content: string;
|
||||
lastUpdated?: string;
|
||||
}
|
||||
|
||||
export default function LegalPageLayout({ title, content, lastUpdated }: LegalPageLayoutProps) {
|
||||
return (
|
||||
<div className="section-padding">
|
||||
<div className="container-page max-w-4xl">
|
||||
{/* Back link */}
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center text-gray-600 hover:text-primary-dark transition-colors mb-8"
|
||||
>
|
||||
<ArrowLeftIcon className="w-4 h-4 mr-2" />
|
||||
Back to Home
|
||||
</Link>
|
||||
|
||||
{/* Title */}
|
||||
<div className="mb-8 pb-6 border-b border-gray-200">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-primary-dark mb-2">
|
||||
{title}
|
||||
</h1>
|
||||
{lastUpdated && lastUpdated !== '[Insert Date]' && (
|
||||
<p className="text-sm text-gray-500">
|
||||
Last updated: {lastUpdated}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Markdown content */}
|
||||
<article className="prose prose-gray max-w-none legal-content">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
// Style headings
|
||||
h1: ({ children }) => (
|
||||
<h1 className="text-3xl font-bold text-primary-dark mt-8 mb-4 first:mt-0">
|
||||
{children}
|
||||
</h1>
|
||||
),
|
||||
h2: ({ children }) => (
|
||||
<h2 className="text-2xl font-semibold text-primary-dark mt-8 mb-4 pb-2 border-b border-gray-200">
|
||||
{children}
|
||||
</h2>
|
||||
),
|
||||
h3: ({ children }) => (
|
||||
<h3 className="text-xl font-semibold text-primary-dark mt-6 mb-3">
|
||||
{children}
|
||||
</h3>
|
||||
),
|
||||
h4: ({ children }) => (
|
||||
<h4 className="text-lg font-semibold text-primary-dark mt-4 mb-2">
|
||||
{children}
|
||||
</h4>
|
||||
),
|
||||
// Style paragraphs
|
||||
p: ({ children }) => (
|
||||
<p className="text-gray-700 leading-relaxed mb-4">
|
||||
{children}
|
||||
</p>
|
||||
),
|
||||
// Style lists
|
||||
ul: ({ children }) => (
|
||||
<ul className="list-disc list-inside space-y-2 mb-4 text-gray-700 ml-4">
|
||||
{children}
|
||||
</ul>
|
||||
),
|
||||
ol: ({ children }) => (
|
||||
<ol className="list-decimal list-inside space-y-2 mb-4 text-gray-700 ml-4">
|
||||
{children}
|
||||
</ol>
|
||||
),
|
||||
li: ({ children }) => (
|
||||
<li className="text-gray-700">
|
||||
{children}
|
||||
</li>
|
||||
),
|
||||
// Style links
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
className="text-primary-dark underline hover:text-primary-yellow transition-colors"
|
||||
target={href?.startsWith('http') ? '_blank' : undefined}
|
||||
rel={href?.startsWith('http') ? 'noopener noreferrer' : undefined}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
// Style horizontal rules
|
||||
hr: () => (
|
||||
<hr className="my-8 border-gray-200" />
|
||||
),
|
||||
// Style blockquotes
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-4 border-primary-yellow pl-4 my-4 italic text-gray-600">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
// Style tables
|
||||
table: ({ children }) => (
|
||||
<div className="overflow-x-auto my-6">
|
||||
<table className="min-w-full divide-y divide-gray-200 border border-gray-200 rounded-lg">
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
),
|
||||
thead: ({ children }) => (
|
||||
<thead className="bg-gray-50">
|
||||
{children}
|
||||
</thead>
|
||||
),
|
||||
tbody: ({ children }) => (
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{children}
|
||||
</tbody>
|
||||
),
|
||||
tr: ({ children }) => (
|
||||
<tr>
|
||||
{children}
|
||||
</tr>
|
||||
),
|
||||
th: ({ children }) => (
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-primary-dark">
|
||||
{children}
|
||||
</th>
|
||||
),
|
||||
td: ({ children }) => (
|
||||
<td className="px-4 py-3 text-sm text-gray-700">
|
||||
{children}
|
||||
</td>
|
||||
),
|
||||
// Style code blocks
|
||||
code: ({ className, children }) => {
|
||||
const isInline = !className;
|
||||
if (isInline) {
|
||||
return (
|
||||
<code className="bg-gray-100 px-1.5 py-0.5 rounded text-sm text-gray-800">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<code className={className}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
},
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-gray-100 rounded-lg p-4 overflow-x-auto my-4">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
// Style strong and emphasis
|
||||
strong: ({ children }) => (
|
||||
<strong className="font-semibold text-primary-dark">
|
||||
{children}
|
||||
</strong>
|
||||
),
|
||||
em: ({ children }) => (
|
||||
<em className="italic">
|
||||
{children}
|
||||
</em>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
</article>
|
||||
|
||||
{/* Back to top link */}
|
||||
<div className="mt-12 pt-6 border-t border-gray-200">
|
||||
<button
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
className="text-gray-500 hover:text-primary-dark transition-colors text-sm"
|
||||
>
|
||||
Back to top
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import { ButtonHTMLAttributes, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = 'primary', size = 'md', isLoading, children, disabled, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
disabled={disabled || isLoading}
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center font-medium transition-all duration-200 rounded-btn focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||
{
|
||||
// Variants
|
||||
'bg-primary-yellow text-primary-dark hover:bg-yellow-400 focus:ring-yellow-400':
|
||||
variant === 'primary',
|
||||
'bg-primary-dark text-white hover:bg-gray-800 focus:ring-gray-800':
|
||||
variant === 'secondary',
|
||||
'border-2 border-primary-dark text-primary-dark bg-transparent hover:bg-gray-50 focus:ring-gray-400':
|
||||
variant === 'outline',
|
||||
'text-primary-dark hover:bg-gray-100 focus:ring-gray-300':
|
||||
variant === 'ghost',
|
||||
'bg-red-600 text-white hover:bg-red-700 focus:ring-red-500':
|
||||
variant === 'danger',
|
||||
// Sizes
|
||||
'text-sm px-3 py-1.5': size === 'sm',
|
||||
'text-base px-5 py-2.5': size === 'md',
|
||||
'text-lg px-7 py-3': size === 'lg',
|
||||
// States
|
||||
'opacity-50 cursor-not-allowed': disabled || isLoading,
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-2 h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
/>
|
||||
</svg>
|
||||
Loading...
|
||||
</>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export default Button;
|
||||
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { HTMLAttributes, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: 'default' | 'elevated' | 'bordered';
|
||||
}
|
||||
|
||||
const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, variant = 'default', children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
'bg-white rounded-card overflow-hidden',
|
||||
{
|
||||
'shadow-card': variant === 'default',
|
||||
'shadow-card-hover hover:shadow-lg transition-shadow duration-200':
|
||||
variant === 'elevated',
|
||||
'border border-secondary-light-gray': variant === 'bordered',
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
export default Card;
|
||||
@@ -0,0 +1,48 @@
|
||||
'use client';
|
||||
|
||||
import { InputHTMLAttributes, forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, label, error, id, ...props }, ref) => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className="block text-sm font-medium text-primary-dark mb-1.5"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={clsx(
|
||||
'w-full px-4 py-3 rounded-btn border transition-colors duration-200',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary-yellow focus:border-transparent',
|
||||
'placeholder:text-gray-400',
|
||||
{
|
||||
'border-secondary-light-gray': !error,
|
||||
'border-red-500 focus:ring-red-500': error,
|
||||
},
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{error && (
|
||||
<p className="mt-1.5 text-sm text-red-600">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export default Input;
|
||||
Reference in New Issue
Block a user