Files
biblical-guide.com/components/layout/footer.tsx
Andrei c3cd353f2f 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>
2025-10-12 22:14:22 +00:00

229 lines
7.3 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import {
Paper,
Container,
Box,
Typography,
Button,
Divider,
IconButton,
Chip,
} from '@mui/material'
import {
Facebook,
Twitter,
Instagram,
YouTube,
LinkedIn,
GitHub,
MusicNote as TikTok,
Share as DefaultIcon
} from '@mui/icons-material'
import { useRouter } from 'next/navigation'
import { useTranslations, useLocale } from 'next-intl'
interface DynamicPage {
id: string
title: string
slug: string
showInFooter: boolean
footerOrder?: number
}
interface SocialMediaLink {
id: string
platform: string
name: string
url: string
icon: string
order: number
}
export function Footer() {
const [dynamicPages, setDynamicPages] = useState<DynamicPage[]>([])
const [socialLinks, setSocialLinks] = useState<SocialMediaLink[]>([])
const router = useRouter()
const t = useTranslations('home')
const tSeo = useTranslations('seo')
const locale = useLocale()
useEffect(() => {
fetchDynamicPages()
fetchSocialLinks()
}, [])
const fetchDynamicPages = async () => {
try {
const response = await fetch('/api/pages?location=footer')
if (response.ok) {
const data = await response.json()
setDynamicPages(data.data || [])
}
} catch (error) {
console.error('Failed to fetch dynamic pages:', error)
}
}
const fetchSocialLinks = async () => {
try {
const response = await fetch('/api/social-media')
if (response.ok) {
const data = await response.json()
setSocialLinks(data.data || [])
}
} catch (error) {
console.error('Failed to fetch social media links:', error)
}
}
const getCurrentYear = () => {
return new Date().getFullYear()
}
const renderSocialIcon = (iconName: string) => {
const iconMap = {
'Facebook': Facebook,
'Twitter': Twitter,
'Instagram': Instagram,
'YouTube': YouTube,
'LinkedIn': LinkedIn,
'GitHub': GitHub,
'TikTok': TikTok
}
const IconComponent = iconMap[iconName as keyof typeof iconMap] || DefaultIcon
return <IconComponent />
}
return (
<Paper component="footer" sx={{ bgcolor: 'grey.900', color: 'white', py: 6 }}>
<Container maxWidth="lg">
<Box sx={{ display: 'flex', gap: 6, flexWrap: 'wrap', justifyContent: 'space-between', mb: 4 }}>
{/* Brand */}
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 auto' } }}>
<Typography variant="h5" sx={{ mb: 2, fontWeight: 600 }}>
{t('footer.brand')}
</Typography>
<Typography variant="body2" color="grey.400" sx={{ maxWidth: 300 }}>
{tSeo('footer')}
</Typography>
</Box>
{/* Quick Links */}
<Box sx={{ flex: { xs: '1 1 50%', md: '1 1 auto' } }}>
<Typography variant="h6" sx={{ mb: 2 }}>
{t('footer.quickLinks.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{/* Static important links */}
<Button
color="inherit"
sx={{ justifyContent: 'flex-start', p: 0 }}
onClick={() => router.push(`/${locale}`)}
>
{t('footer.quickLinks.home')}
</Button>
<Button
color="inherit"
sx={{
justifyContent: 'flex-start',
p: 0,
fontWeight: 600,
color: 'secondary.main'
}}
onClick={() => router.push(`/${locale}/donate`)}
>
{t('footer.quickLinks.sponsor')}
</Button>
<Button
color="inherit"
sx={{ justifyContent: 'flex-start', p: 0 }}
onClick={() => router.push(`/${locale}/contact`)}
>
{t('footer.quickLinks.contact')}
</Button>
{/* Dynamic pages - filtered for non-legal pages */}
{dynamicPages
.filter(page => !['terms', 'privacy', 'cookies', 'gdpr'].includes(page.slug))
.sort((a, b) => (a.footerOrder || 999) - (b.footerOrder || 999))
.map((page) => (
<Button
key={page.id}
color="inherit"
sx={{ justifyContent: 'flex-start', p: 0 }}
onClick={() => router.push(`/${locale}/pages/${page.slug}`)}
>
{page.title}
</Button>
))}
</Box>
</Box>
{/* Legal */}
<Box sx={{ flex: { xs: '1 1 50%', md: '1 1 auto' } }}>
<Typography variant="h6" sx={{ mb: 2 }}>
{t('footer.legal.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{/* Dynamic legal pages */}
{dynamicPages
.filter(page => ['terms', 'privacy', 'cookies', 'gdpr'].includes(page.slug))
.sort((a, b) => (a.footerOrder || 999) - (b.footerOrder || 999))
.map((page) => (
<Button
key={page.id}
color="inherit"
sx={{ justifyContent: 'flex-start', p: 0 }}
onClick={() => router.push(`/${locale}/pages/${page.slug}`)}
>
{page.title}
</Button>
))}
</Box>
</Box>
{/* Social */}
<Box sx={{ flex: { xs: '1 1 100%', md: '1 1 auto' } }}>
<Typography variant="h6" sx={{ mb: 2 }}>
{t('footer.social.title')}
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{socialLinks.map((link) => (
<IconButton
key={link.id}
color="inherit"
size="small"
href={link.url}
target="_blank"
rel="noopener noreferrer"
title={link.name}
>
{renderSocialIcon(link.icon)}
</IconButton>
))}
{socialLinks.length === 0 && (
<Typography variant="caption" color="grey.500">
No social media links configured
</Typography>
)}
</Box>
</Box>
</Box>
<Divider sx={{ bgcolor: 'grey.700', mb: 3 }} />
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 2 }}>
<Typography variant="body2" color="grey.400">
© {getCurrentYear()} Biblical Guide - {locale === 'ro' ? 'Făcut cu ❤️ și 🙏' : 'Made with ❤️ and 🙏'}
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip label="🇷🇴 Română" size="small" variant="outlined" sx={{ color: 'white', borderColor: 'grey.600' }} />
<Chip label="🇺🇸 English" size="small" variant="outlined" sx={{ color: 'white', borderColor: 'grey.600' }} />
<Chip label="+20 more" size="small" variant="outlined" sx={{ color: 'white', borderColor: 'grey.600' }} />
</Box>
</Box>
</Container>
</Paper>
)
}