feat: implement subscription system with conversation limits

Implement complete backend subscription system that limits free users to 10
AI conversations per month and offers Premium tier ($10/month or $100/year)
with unlimited conversations.

Changes:
- Add User subscription fields (tier, status, limits, counters)
- Create Subscription model to track Stripe subscriptions
- Implement conversation limit enforcement in chat API
- Add subscription checkout and customer portal APIs
- Update Stripe webhook to handle subscription events
- Add subscription utility functions (limit checks, tier management)
- Add comprehensive subscription translations (en, ro, es, it)
- Update environment variables for Stripe price IDs
- Update footer "Sponsor Us" link to point to /donate
- Add "Sponsor Us" button to home page hero section

Database:
- User model: subscriptionTier, subscriptionStatus, conversationLimit,
  conversationCount, limitResetDate, stripeCustomerId, stripeSubscriptionId
- Subscription model: tracks Stripe subscription details, periods, status
- SubscriptionStatus enum: ACTIVE, CANCELLED, PAST_DUE, TRIALING, etc.

API Routes:
- POST /api/subscriptions/checkout - Create Stripe checkout session
- POST /api/subscriptions/portal - Get customer portal link
- Webhook handlers for: customer.subscription.created/updated/deleted,
  invoice.payment_succeeded/failed

Features:
- Free tier: 10 conversations/month with automatic monthly reset
- Premium tier: Unlimited conversations
- Automatic limit enforcement before conversation creation
- Returns LIMIT_REACHED error with upgrade URL when limit hit
- Stripe Customer Portal integration for subscription management
- Automatic tier upgrade/downgrade via webhooks

Documentation:
- SUBSCRIPTION_IMPLEMENTATION_PLAN.md - Complete implementation plan
- SUBSCRIPTION_IMPLEMENTATION_STATUS.md - Current status and next steps

Frontend UI still needed: subscription page, upgrade modal, usage display

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-12 22:14:22 +00:00
parent be22b5b4fd
commit c3cd353f2f
16 changed files with 3771 additions and 699 deletions

View File

@@ -1,39 +1,48 @@
'use client'
import { useState } from 'react'
import {
Container,
Typography,
Box,
Button,
TextField,
Paper,
CircularProgress,
Alert,
FormControlLabel,
Checkbox,
ToggleButton,
ToggleButtonGroup,
useTheme,
Divider,
Card,
CardContent,
List,
ListItem,
ListItemIcon,
ListItemText,
TextField,
Checkbox,
FormControlLabel,
ToggleButton,
ToggleButtonGroup,
CircularProgress,
Alert,
} from '@mui/material'
import {
MenuBook,
Chat,
Favorite,
CheckCircle,
Public,
Search,
Language,
CloudOff,
Security,
AutoStories,
Public,
VolunteerActivism,
CheckCircle,
} from '@mui/icons-material'
import { useRouter } from 'next/navigation'
import { useLocale } from 'next-intl'
import { useLocale, useTranslations } from 'next-intl'
import { useState } from 'react'
import { DONATION_PRESETS } from '@/lib/stripe'
export default function DonatePage() {
const theme = useTheme()
const router = useRouter()
const locale = useLocale()
const t = useTranslations('donate')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
@@ -74,12 +83,12 @@ export default function DonatePage() {
// Validation
if (!amount || amount < 1) {
setError('Please enter a valid amount (minimum $1)')
setError(t('form.errors.invalidAmount'))
return
}
if (!email || !email.includes('@')) {
setError('Please enter a valid email address')
setError(t('form.errors.invalidEmail'))
return
}
@@ -107,7 +116,7 @@ export default function DonatePage() {
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || 'Failed to create checkout session')
throw new Error(data.error || t('form.errors.checkoutFailed'))
}
// Redirect to Stripe Checkout
@@ -116,66 +125,349 @@ export default function DonatePage() {
}
} catch (err) {
console.error('Donation error:', err)
setError(err instanceof Error ? err.message : 'An error occurred')
setError(err instanceof Error ? err.message : t('form.errors.generic'))
setLoading(false)
}
}
const features = [
{
icon: <Public />,
text: '1,200+ Bible versions in multiple languages',
icon: <Public sx={{ fontSize: 48 }} />,
title: t('features.globalLibrary.title'),
description: t('features.globalLibrary.description'),
},
{
icon: <Language />,
text: 'Multilingual access for believers worldwide',
icon: <Language sx={{ fontSize: 48 }} />,
title: t('features.multilingual.title'),
description: t('features.multilingual.description'),
},
{
icon: <CloudOff />,
text: 'Offline access to Scripture anywhere',
icon: <Favorite sx={{ fontSize: 48 }} />,
title: t('features.prayerWall.title'),
description: t('features.prayerWall.description'),
},
{
icon: <Security />,
text: 'Complete privacy - no ads or tracking',
icon: <Chat sx={{ fontSize: 48 }} />,
title: t('features.aiChat.title'),
description: t('features.aiChat.description'),
},
{
icon: <Security sx={{ fontSize: 48 }} />,
title: t('features.privacy.title'),
description: t('features.privacy.description'),
},
{
icon: <CloudOff sx={{ fontSize: 48 }} />,
title: t('features.offline.title'),
description: t('features.offline.description'),
},
]
return (
<Box sx={{ bgcolor: 'grey.50', minHeight: '100vh', py: 8 }}>
<Container maxWidth="lg">
{/* Hero Section */}
<Box sx={{ textAlign: 'center', mb: 8 }}>
<Favorite sx={{ fontSize: 64, color: 'primary.main', mb: 2 }} />
<Box>
{/* Hero Section */}
<Box
sx={{
background: 'linear-gradient(135deg, #009688 0%, #00796B 100%)',
color: 'white',
py: 6.25,
textAlign: 'center',
position: 'relative',
overflow: 'hidden',
}}
>
<Container maxWidth="md">
<Typography
variant="h1"
sx={{
fontSize: { xs: '2.5rem', sm: '3.5rem', md: '4.5rem' },
fontWeight: 700,
mb: 3,
letterSpacing: '-0.02em',
}}
>
{t('hero.title')}
</Typography>
<Typography
variant="h4"
sx={{
fontSize: { xs: '1.25rem', sm: '1.75rem', md: '2rem' },
fontWeight: 400,
mb: 6,
opacity: 0.95,
letterSpacing: '-0.01em',
}}
>
{t('hero.subtitle')}
</Typography>
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center', flexWrap: 'wrap' }}>
<Button
variant="contained"
size="large"
sx={{
bgcolor: 'white',
color: 'primary.main',
px: 4,
py: 1.5,
fontSize: '1.1rem',
fontWeight: 600,
'&:hover': { bgcolor: 'grey.100' },
textTransform: 'none',
}}
startIcon={<AutoStories />}
onClick={() => router.push(`/${locale}/bible`)}
>
{t('hero.cta.readBible')}
</Button>
<Button
variant="outlined"
size="large"
sx={{
borderColor: 'white',
color: 'white',
px: 4,
py: 1.5,
fontSize: '1.1rem',
fontWeight: 600,
'&:hover': {
borderColor: 'white',
bgcolor: 'rgba(255,255,255,0.1)',
},
textTransform: 'none',
}}
onClick={() => window.scrollTo({ top: document.getElementById('donate-form')?.offsetTop || 0, behavior: 'smooth' })}
>
{t('hero.cta.supportMission')}
</Button>
</Box>
</Container>
</Box>
{/* Mission Section */}
<Container maxWidth="md" sx={{ pt: { xs: 10, md: 16 }, pb: 0, textAlign: 'center' }}>
<Typography
variant="h2"
sx={{
fontSize: { xs: '2rem', md: '3rem' },
fontWeight: 700,
mb: 4,
letterSpacing: '-0.02em',
}}
>
{t('mission.title')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 400,
lineHeight: 1.6,
color: 'text.secondary',
maxWidth: 700,
mx: 'auto',
}}
>
{t('mission.description1')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 600,
lineHeight: 1.6,
mt: 3,
maxWidth: 700,
mx: 'auto',
}}
>
{t('mission.different')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 400,
lineHeight: 1.6,
mt: 2,
maxWidth: 700,
mx: 'auto',
}}
>
{t('mission.description2')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 400,
lineHeight: 1.6,
mt: 2,
color: 'text.secondary',
maxWidth: 700,
mx: 'auto',
}}
>
{t('mission.description3')}
</Typography>
</Container>
<Divider sx={{ maxWidth: 200, mx: 'auto', borderColor: 'grey.300' }} />
{/* Donation Pitch Section */}
<Container maxWidth="md" sx={{ pt: { xs: 10, md: 16 }, pb: 0, textAlign: 'center' }}>
<Typography
variant="h2"
sx={{
fontSize: { xs: '2rem', md: '3rem' },
fontWeight: 700,
mb: 4,
letterSpacing: '-0.02em',
}}
>
{t('pitch.title')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 400,
lineHeight: 1.8,
color: 'text.secondary',
mb: 5,
maxWidth: 700,
mx: 'auto',
}}
>
{t('pitch.description1')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 400,
lineHeight: 1.8,
color: 'text.secondary',
mb: 5,
maxWidth: 700,
mx: 'auto',
}}
>
{t('pitch.description2')}
</Typography>
<Paper
elevation={0}
sx={{
bgcolor: 'primary.light',
color: 'white',
py: 4,
px: 3,
borderRadius: 3,
maxWidth: 600,
mx: 'auto',
}}
>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.3rem' },
fontWeight: 500,
fontStyle: 'italic',
lineHeight: 1.6,
}}
>
{t('pitch.verse.text')}
</Typography>
<Typography variant="h6" sx={{ mt: 2, fontWeight: 600 }}>
{t('pitch.verse.reference')}
</Typography>
</Paper>
</Container>
{/* Features Section */}
<Box sx={{ bgcolor: 'grey.50', pt: { xs: 10, md: 16 }, pb: 0 }}>
<Container maxWidth="lg">
<Typography
variant="h2"
sx={{
fontSize: { xs: '2rem', md: '3rem' },
fontWeight: 700,
mb: 2,
mb: 3,
textAlign: 'center',
letterSpacing: '-0.02em',
}}
>
Support Biblical Guide
{t('features.title')}
</Typography>
<Typography
variant="h5"
variant="h6"
sx={{
fontSize: { xs: '1.1rem', md: '1.3rem' },
textAlign: 'center',
color: 'text.secondary',
mb: 8,
maxWidth: 700,
mx: 'auto',
}}
>
Your donation keeps Scripture free and accessible to everyone, everywhere.
{t('features.subtitle')}
</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 4, flexWrap: 'wrap', justifyContent: 'center' }}>
{features.map((feature, index) => (
<Box key={index} sx={{ flex: { xs: '1 1 100%', md: '1 1 calc(33.33% - 24px)' }, maxWidth: { xs: '100%', md: 400 } }}>
<Card
elevation={0}
sx={{
height: '100%',
textAlign: 'center',
bgcolor: 'white',
border: '1px solid',
borderColor: 'grey.200',
transition: 'transform 0.2s, box-shadow 0.2s',
'&:hover': {
transform: 'translateY(-4px)',
boxShadow: 2,
},
}}
>
<CardContent sx={{ p: 4 }}>
<Box sx={{ color: 'primary.main', mb: 2 }}>
{feature.icon}
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
{feature.title}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ lineHeight: 1.6 }}>
{feature.description}
</Typography>
</CardContent>
</Card>
</Box>
))}
</Box>
</Container>
</Box>
{/* Donation Form Section */}
<Container id="donate-form" maxWidth="lg" sx={{ pt: { xs: 10, md: 16 }, pb: 0 }}>
<Typography
variant="h2"
sx={{
fontSize: { xs: '2rem', md: '3rem' },
fontWeight: 700,
mb: 6,
textAlign: 'center',
letterSpacing: '-0.02em',
}}
>
{t('form.title')}
</Typography>
<Box sx={{ display: 'flex', gap: 4, flexDirection: { xs: 'column', md: 'row' } }}>
{/* Donation Form */}
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 58%' } }}>
<Paper elevation={2} sx={{ p: 4 }}>
<Typography variant="h5" sx={{ fontWeight: 600, mb: 3 }}>
Make a Donation
{t('form.makedonation')}
</Typography>
{error && (
@@ -186,7 +478,7 @@ export default function DonatePage() {
{success && (
<Alert severity="success" sx={{ mb: 3 }}>
Thank you for your donation!
{t('form.success')}
</Alert>
)}
@@ -200,7 +492,7 @@ export default function DonatePage() {
onChange={(e) => setIsRecurring(e.target.checked)}
/>
}
label="Make this a recurring donation"
label={t('form.recurring.label')}
/>
{isRecurring && (
<ToggleButtonGroup
@@ -210,10 +502,10 @@ export default function DonatePage() {
sx={{ mt: 2, width: '100%' }}
>
<ToggleButton value="month" sx={{ flex: 1 }}>
Monthly
{t('form.recurring.monthly')}
</ToggleButton>
<ToggleButton value="year" sx={{ flex: 1 }}>
Yearly
{t('form.recurring.yearly')}
</ToggleButton>
</ToggleButtonGroup>
)}
@@ -222,7 +514,7 @@ export default function DonatePage() {
{/* Amount Selection */}
<Box sx={{ mb: 3 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Select Amount (USD)
{t('form.amount.label')}
</Typography>
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 2 }}>
{DONATION_PRESETS.map((preset) => (
@@ -244,7 +536,7 @@ export default function DonatePage() {
<TextField
fullWidth
label="Custom Amount"
label={t('form.amount.custom')}
type="number"
value={customAmount}
onChange={(e) => handleCustomAmountChange(e.target.value)}
@@ -260,12 +552,12 @@ export default function DonatePage() {
{/* Contact Information */}
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2 }}>
Your Information
{t('form.info.title')}
</Typography>
<TextField
fullWidth
label="Email Address"
label={t('form.info.email')}
type="email"
required
value={email}
@@ -276,7 +568,7 @@ export default function DonatePage() {
{!isAnonymous && (
<TextField
fullWidth
label="Name (optional)"
label={t('form.info.name')}
value={name}
onChange={(e) => setName(e.target.value)}
sx={{ mb: 2 }}
@@ -290,18 +582,18 @@ export default function DonatePage() {
onChange={(e) => setIsAnonymous(e.target.checked)}
/>
}
label="Make this donation anonymous"
label={t('form.info.anonymous')}
sx={{ mb: 2 }}
/>
<TextField
fullWidth
label="Message (optional)"
label={t('form.info.message')}
multiline
rows={3}
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Share why you're supporting Biblical Guide..."
placeholder={t('form.info.messagePlaceholder')}
sx={{ mb: 3 }}
/>
@@ -321,7 +613,7 @@ export default function DonatePage() {
{loading ? (
<CircularProgress size={24} color="inherit" />
) : (
`Donate ${getAmount() ? `$${getAmount()}` : ''}`
`${t('form.submit')} ${getAmount() ? `$${getAmount()}` : ''}`
)}
</Button>
@@ -330,9 +622,33 @@ export default function DonatePage() {
color="text.secondary"
sx={{ mt: 2, textAlign: 'center' }}
>
Secure payment powered by Stripe
{t('form.secure')}
</Typography>
</form>
{/* Alternative Donation Methods */}
<Box sx={{ mt: 4 }}>
<Divider sx={{ mb: 3 }} />
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 2, textAlign: 'center' }}>
{t('alternatives.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Button
variant="outlined"
size="large"
fullWidth
sx={{ py: 1.5, textTransform: 'none' }}
startIcon={<span style={{ fontSize: '1.5rem' }}>💳</span>}
href="https://paypal.me/andupetcu"
target="_blank"
>
{t('alternatives.paypal')}
</Button>
<Typography variant="body2" color="text.secondary" textAlign="center">
<span style={{ fontSize: '1.5rem' }}>🎯</span> {t('alternatives.kickstarter')}
</Typography>
</Box>
</Box>
</Paper>
</Box>
@@ -340,19 +656,16 @@ export default function DonatePage() {
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 42%' } }}>
<Paper elevation={2} sx={{ p: 4, mb: 3, bgcolor: 'primary.light', color: 'white' }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
Your Impact
{t('impact.title')}
</Typography>
<Typography variant="body1" sx={{ mb: 3, lineHeight: 1.8 }}>
Every donation directly supports the servers, translations, and technology that
make Biblical Guide possible.
{t('impact.description')}
</Typography>
<List>
{features.map((feature, index) => (
{features.slice(0, 4).map((feature, index) => (
<ListItem key={index} sx={{ px: 0 }}>
<ListItemIcon sx={{ color: 'white', minWidth: 40 }}>
<CheckCircle />
</ListItemIcon>
<ListItemText primary={feature.text} />
<CheckCircle sx={{ mr: 2 }} />
<ListItemText primary={feature.title} />
</ListItem>
))}
</List>
@@ -360,36 +673,230 @@ export default function DonatePage() {
<Paper elevation={2} sx={{ p: 4 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
Why Donate?
{t('why.title')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2, lineHeight: 1.8 }}>
Biblical Guide is committed to keeping God&apos;s Word free and accessible to all.
We don&apos;t have ads, paywalls, or sell your data.
{t('why.description1')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.8 }}>
When you give, you&apos;re not paying for access you&apos;re keeping access open
for millions who cannot afford to pay.
{t('why.description2')}
</Typography>
<Box
sx={{
mt: 3,
p: 2,
bgcolor: 'primary.light',
color: 'white',
borderRadius: 2,
}}
>
<Typography variant="body1" sx={{ fontStyle: 'italic', textAlign: 'center' }}>
Freely you have received; freely give.
</Typography>
<Typography variant="body2" sx={{ textAlign: 'center', mt: 1, fontWeight: 600 }}>
Matthew 10:8
</Typography>
</Box>
</Paper>
</Box>
</Box>
</Container>
{/* Why It Matters Section */}
<Box sx={{ bgcolor: 'grey.900', color: 'white', pt: { xs: 10, md: 16 }, pb: 0 }}>
<Container maxWidth="md" sx={{ textAlign: 'center' }}>
<Typography
variant="h2"
sx={{
fontSize: { xs: '2rem', md: '3rem' },
fontWeight: 700,
mb: 6,
letterSpacing: '-0.02em',
}}
>
{t('matters.title')}
</Typography>
<List sx={{ maxWidth: 700, mx: 'auto' }}>
<ListItem sx={{ py: 2, flexDirection: 'column', alignItems: 'flex-start' }}>
<Typography variant="h6" sx={{ fontSize: '1.2rem', mb: 1, lineHeight: 1.6 }}>
{t('matters.point1')}
</Typography>
</ListItem>
<ListItem sx={{ py: 2, flexDirection: 'column', alignItems: 'flex-start' }}>
<Typography variant="h6" sx={{ fontSize: '1.2rem', mb: 1, lineHeight: 1.6 }}>
{t('matters.point2')}
</Typography>
</ListItem>
<ListItem sx={{ py: 2, flexDirection: 'column', alignItems: 'flex-start' }}>
<Typography variant="h6" sx={{ fontSize: '1.2rem', mb: 1, lineHeight: 1.6 }}>
{t('matters.point3')}
</Typography>
</ListItem>
</List>
<Typography
variant="h4"
sx={{
fontSize: { xs: '1.5rem', md: '2rem' },
fontWeight: 700,
mt: 6,
mb: 3,
}}
>
{t('matters.together')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.3rem' },
fontWeight: 400,
lineHeight: 1.6,
opacity: 0.9,
}}
>
{t('matters.conclusion')}
</Typography>
</Container>
</Box>
{/* Join the Mission Section */}
<Container maxWidth="md" sx={{ pt: { xs: 10, md: 16 }, pb: 0, textAlign: 'center' }}>
<Typography
variant="h2"
sx={{
fontSize: { xs: '2rem', md: '3rem' },
fontWeight: 700,
mb: 4,
letterSpacing: '-0.02em',
}}
>
{t('join.title')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 400,
lineHeight: 1.8,
color: 'text.secondary',
mb: 2,
}}
>
{t('join.description1')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 400,
lineHeight: 1.8,
color: 'text.secondary',
mb: 6,
}}
>
{t('join.description2')}
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 600,
lineHeight: 1.8,
mb: 6,
}}
>
{t('join.callToAction')}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.95rem', fontStyle: 'italic' }}>
{t('join.closing')}
</Typography>
</Container>
{/* Footer CTA */}
<Box
sx={{
background: 'linear-gradient(135deg, #009688 0%, #00796B 100%)',
color: 'white',
py: 6.25,
textAlign: 'center',
}}
>
<Container maxWidth="md">
<Typography
variant="h3"
sx={{
fontSize: { xs: '1.75rem', md: '2.5rem' },
fontWeight: 700,
mb: 2,
letterSpacing: '-0.02em',
}}
>
Biblical-Guide.com
</Typography>
<Typography
variant="h5"
sx={{
fontSize: { xs: '1.1rem', md: '1.5rem' },
fontWeight: 400,
opacity: 0.95,
}}
>
{t('footer.tagline')}
</Typography>
<Box sx={{ display: 'flex', gap: 3, justifyContent: 'center', mt: 6, flexWrap: 'wrap' }}>
<Button
variant="outlined"
sx={{
borderColor: 'white',
color: 'white',
'&:hover': {
borderColor: 'white',
bgcolor: 'rgba(255,255,255,0.1)',
},
textTransform: 'none',
}}
onClick={() => router.push(`/${locale}/bible`)}
>
{t('footer.links.readBible')}
</Button>
<Button
variant="outlined"
sx={{
borderColor: 'white',
color: 'white',
'&:hover': {
borderColor: 'white',
bgcolor: 'rgba(255,255,255,0.1)',
},
textTransform: 'none',
}}
onClick={() => router.push(`/${locale}/prayers`)}
>
{t('footer.links.prayerWall')}
</Button>
<Button
variant="outlined"
sx={{
borderColor: 'white',
color: 'white',
'&:hover': {
borderColor: 'white',
bgcolor: 'rgba(255,255,255,0.1)',
},
textTransform: 'none',
}}
onClick={() => window.dispatchEvent(new CustomEvent('floating-chat:open', { detail: { fullscreen: true } }))}
>
{t('footer.links.aiChat')}
</Button>
<Button
variant="outlined"
sx={{
borderColor: 'white',
color: 'white',
'&:hover': {
borderColor: 'white',
bgcolor: 'rgba(255,255,255,0.1)',
},
textTransform: 'none',
}}
onClick={() => router.push(`/${locale}/contact`)}
>
{t('footer.links.contact')}
</Button>
</Box>
</Container>
</Box>
</Box>
)
}

File diff suppressed because it is too large Load Diff