User can now view:
- Current tier (Free, Basic, Premium, Selfhosted) with icon
- Trial status and end date
- Access expiration date
- Feature limits with usage bars
- Progress indicators (green/orange/red based on usage)
- Reset period info (daily/monthly/never)
Coupon redemption:
- Input field for coupon code
- Auto-uppercase, monospace display
- Enter key support
- Success/error feedback
- Auto-refresh after redemption
Features:
- Clean card-based layout
- Visual tier badges with colors
- Progress bars for count limits
- Trial and access warnings
- Integrated in Settings page
Link added to SettingsPage:
- "👑 Abo-Status, Limits & Coupon einlösen"
- Easy access for all users
Phase 3 complete - all user-facing subscription features done!
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
242 lines
8.3 KiB
JavaScript
242 lines
8.3 KiB
JavaScript
import { useState, useEffect } from 'react'
|
|
import { Gift, AlertCircle, TrendingUp, Award } from 'lucide-react'
|
|
import { api } from '../utils/api'
|
|
|
|
export default function SubscriptionPage() {
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [subscription, setSubscription] = useState(null)
|
|
const [usage, setUsage] = useState([])
|
|
const [limits, setLimits] = useState([])
|
|
const [couponCode, setCouponCode] = useState('')
|
|
const [redeeming, setRedeeming] = useState(false)
|
|
const [couponSuccess, setCouponSuccess] = useState('')
|
|
|
|
useEffect(() => {
|
|
loadData()
|
|
}, [])
|
|
|
|
async function loadData() {
|
|
try {
|
|
setLoading(true)
|
|
const [subData, usageData, limitsData] = await Promise.all([
|
|
api.getMySubscription(),
|
|
api.getMyUsage(),
|
|
api.getMyLimits()
|
|
])
|
|
setSubscription(subData)
|
|
setUsage(usageData)
|
|
setLimits(limitsData)
|
|
setError('')
|
|
} catch (e) {
|
|
setError(e.message)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
async function handleRedeemCoupon() {
|
|
if (!couponCode.trim()) return
|
|
|
|
try {
|
|
setRedeeming(true)
|
|
setError('')
|
|
setCouponSuccess('')
|
|
await api.redeemCoupon(couponCode.trim().toUpperCase())
|
|
setCouponSuccess('Coupon erfolgreich eingelöst!')
|
|
setCouponCode('')
|
|
await loadData()
|
|
} catch (e) {
|
|
setError(e.message)
|
|
} finally {
|
|
setRedeeming(false)
|
|
}
|
|
}
|
|
|
|
if (loading) return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 40 }}>
|
|
<div className="spinner" />
|
|
</div>
|
|
)
|
|
|
|
const tierColors = {
|
|
free: { bg: 'var(--surface2)', color: 'var(--text2)', icon: '🆓' },
|
|
basic: { bg: '#E3F2FD', color: '#1565C0', icon: '⭐' },
|
|
premium: { bg: '#F3E5F5', color: '#6A1B9A', icon: '👑' },
|
|
selfhosted: { bg: 'var(--accent-light)', color: 'var(--accent-dark)', icon: '🏠' }
|
|
}
|
|
|
|
const currentTier = subscription?.current_tier || 'free'
|
|
const tierStyle = tierColors[currentTier] || tierColors.free
|
|
|
|
return (
|
|
<div style={{ paddingBottom: 40 }}>
|
|
{/* Header */}
|
|
<div style={{ marginBottom: 20 }}>
|
|
<div style={{ fontSize: 20, fontWeight: 700, color: 'var(--text1)' }}>
|
|
Mein Abo
|
|
</div>
|
|
<div style={{ fontSize: 13, color: 'var(--text3)', marginTop: 4 }}>
|
|
Tier, Limits und Nutzung
|
|
</div>
|
|
</div>
|
|
|
|
{/* Messages */}
|
|
{error && (
|
|
<div style={{
|
|
padding: 12, background: 'var(--danger)', color: 'white',
|
|
borderRadius: 8, marginBottom: 16, fontSize: 14
|
|
}}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
{couponSuccess && (
|
|
<div style={{
|
|
padding: 12, background: 'var(--accent)', color: 'white',
|
|
borderRadius: 8, marginBottom: 16, fontSize: 14
|
|
}}>
|
|
{couponSuccess}
|
|
</div>
|
|
)}
|
|
|
|
{/* Current Tier Card */}
|
|
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
|
|
<div style={{
|
|
width: 48, height: 48, borderRadius: 12,
|
|
background: tierStyle.bg,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: 24
|
|
}}>
|
|
{tierStyle.icon}
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ fontSize: 12, color: 'var(--text3)', textTransform: 'uppercase', fontWeight: 600 }}>
|
|
Aktueller Tier
|
|
</div>
|
|
<div style={{ fontSize: 20, fontWeight: 700, color: tierStyle.color }}>
|
|
{currentTier.charAt(0).toUpperCase() + currentTier.slice(1)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{subscription?.trial_ends_at && new Date(subscription.trial_ends_at) > new Date() && (
|
|
<div style={{
|
|
padding: 10, background: '#FFF3CD', borderRadius: 8,
|
|
fontSize: 13, color: '#856404', display: 'flex', gap: 8, alignItems: 'center'
|
|
}}>
|
|
<AlertCircle size={16} />
|
|
<div>
|
|
<strong>Trial aktiv:</strong> Endet am {new Date(subscription.trial_ends_at).toLocaleDateString('de-DE')}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{subscription?.access_until && (
|
|
<div style={{
|
|
padding: 10, background: 'var(--accent-light)', borderRadius: 8,
|
|
fontSize: 13, color: 'var(--accent-dark)', display: 'flex', gap: 8, alignItems: 'center',
|
|
marginTop: 12
|
|
}}>
|
|
<Award size={16} />
|
|
<div>
|
|
<strong>Zugriff bis:</strong> {new Date(subscription.access_until).toLocaleDateString('de-DE')}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Feature Limits */}
|
|
<div className="card" style={{ padding: 20, marginBottom: 16 }}>
|
|
<div style={{
|
|
fontSize: 14, fontWeight: 600, marginBottom: 12,
|
|
display: 'flex', alignItems: 'center', gap: 6
|
|
}}>
|
|
<TrendingUp size={16} color="var(--accent)" />
|
|
Feature-Limits & Nutzung
|
|
</div>
|
|
|
|
{limits.length === 0 ? (
|
|
<div style={{ textAlign: 'center', padding: 20, color: 'var(--text3)', fontSize: 13 }}>
|
|
Keine Limits konfiguriert
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'grid', gap: 12 }}>
|
|
{limits.map(limit => {
|
|
const usageEntry = usage.find(u => u.feature_id === limit.feature_id)
|
|
const used = usageEntry?.usage_count || 0
|
|
const limitValue = limit.limit_value
|
|
const percentage = limitValue ? Math.min((used / limitValue) * 100, 100) : 0
|
|
|
|
return (
|
|
<div key={limit.feature_id} style={{
|
|
padding: 12, background: 'var(--surface)', borderRadius: 8
|
|
}}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
|
<div style={{ fontWeight: 500, fontSize: 13 }}>{limit.feature_name}</div>
|
|
<div style={{ fontSize: 13, color: 'var(--text3)' }}>
|
|
{used} / {limitValue === null ? '∞' : limitValue}
|
|
</div>
|
|
</div>
|
|
|
|
{limitValue !== null && (
|
|
<div style={{
|
|
height: 6, background: 'var(--border)', borderRadius: 3, overflow: 'hidden'
|
|
}}>
|
|
<div style={{
|
|
width: `${percentage}%`,
|
|
height: '100%',
|
|
background: percentage > 90 ? 'var(--danger)' : percentage > 70 ? '#FFA726' : 'var(--accent)',
|
|
transition: 'width 0.3s'
|
|
}} />
|
|
</div>
|
|
)}
|
|
|
|
{limit.reset_period !== 'never' && (
|
|
<div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 4 }}>
|
|
Reset: {limit.reset_period === 'daily' ? 'Täglich' : 'Monatlich'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Coupon Redemption */}
|
|
<div className="card" style={{ padding: 20 }}>
|
|
<div style={{
|
|
fontSize: 14, fontWeight: 600, marginBottom: 12,
|
|
display: 'flex', alignItems: 'center', gap: 6
|
|
}}>
|
|
<Gift size={16} color="var(--accent)" />
|
|
Coupon einlösen
|
|
</div>
|
|
|
|
<div style={{ fontSize: 12, color: 'var(--text3)', marginBottom: 12 }}>
|
|
Hast du einen Coupon-Code? Löse ihn hier ein um Zugriff auf Premium-Features zu erhalten.
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 8 }}>
|
|
<input
|
|
className="form-input"
|
|
style={{ flex: 1, textTransform: 'uppercase', fontFamily: 'monospace' }}
|
|
value={couponCode}
|
|
onChange={(e) => setCouponCode(e.target.value)}
|
|
placeholder="Z.B. PROMO-2026"
|
|
onKeyPress={(e) => e.key === 'Enter' && handleRedeemCoupon()}
|
|
/>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleRedeemCoupon}
|
|
disabled={!couponCode.trim() || redeeming}
|
|
>
|
|
{redeeming ? 'Prüfen...' : 'Einlösen'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|