Spaces:
Running
Running
| import React from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { useTranslation } from 'react-i18next'; | |
| import Sidebar from '../components/Sidebar'; | |
| import { useAuth } from '../context/AuthContext'; | |
| import axios from 'axios'; | |
| import { | |
| Sparkles, | |
| Check, | |
| X, | |
| Zap, | |
| ArrowRight, | |
| Bot, | |
| Layout, | |
| Heart, | |
| Users, | |
| BookTemplate | |
| } from 'lucide-react'; | |
| const PricingCard = ({ type, price, description, features, popular, color, buttonText, isCurrentPlan, onClick, loading }) => { | |
| const { t } = useTranslation(); | |
| return ( | |
| <motion.div | |
| whileHover={isCurrentPlan ? {} : { y: -10 }} | |
| className={`relative bg-surface-container-lowest p-10 rough-border flex flex-col gap-8 h-full ${ | |
| popular ? 'shadow-[12px_12px_0px_0px_var(--color-primary)] dark:shadow-[12px_12px_0px_0px_rgba(255,255,255,0.07)] border-secondary' : 'rough-shadow opacity-90' | |
| }`} | |
| > | |
| {popular && ( | |
| <div className="absolute -top-6 left-1/2 -translate-x-1/2 bg-secondary text-primary px-6 py-2 rough-border font-bold text-sm uppercase tracking-widest"> | |
| {t("subscription.most_recommended")} | |
| </div> | |
| )} | |
| <div className="text-center border-b-2 border-primary/10 pb-8"> | |
| <h3 className={`text-5xl font-display-lg text-primary mb-2`}>{type}</h3> | |
| <div className="flex items-center justify-center gap-1"> | |
| <span className="text-2xl font-display-lg text-secondary">$</span> | |
| <span className="text-6xl font-display-lg text-primary">{price}</span> | |
| <span className="text-sm font-bold text-on-surface-variant uppercase tracking-widest mt-4">{t("subscription.per_month")}</span> | |
| </div> | |
| <p className="mt-4 text-sm text-on-surface-variant font-medium">{description}</p> | |
| </div> | |
| <ul className="flex flex-col gap-4 flex-grow"> | |
| {features.map((f, i) => ( | |
| <li key={i} className="flex items-start gap-3"> | |
| {f.included ? ( | |
| <Check size={18} className="text-secondary mt-1 flex-shrink-0" /> | |
| ) : ( | |
| <X size={18} className="text-on-surface-variant/30 mt-1 flex-shrink-0" /> | |
| )} | |
| <span className={`text-sm font-bold ${f.included ? 'text-primary' : 'text-on-surface-variant/40'}`}> | |
| {f.text} | |
| </span> | |
| </li> | |
| ))} | |
| </ul> | |
| <motion.button | |
| whileHover={isCurrentPlan ? {} : { scale: 1.02 }} | |
| whileTap={isCurrentPlan ? {} : { scale: 0.98 }} | |
| onClick={isCurrentPlan ? undefined : onClick} | |
| disabled={isCurrentPlan || loading} | |
| className={`w-full py-4 text-xl font-bold flex items-center justify-center gap-2 rough-border transition-all ${ | |
| isCurrentPlan | |
| ? 'bg-surface-container-high text-on-surface-variant/50 border-primary/20 cursor-default' | |
| : popular | |
| ? 'bg-primary text-white hover:bg-primary/95 cursor-pointer' | |
| : 'bg-surface-container-lowest text-primary border-primary hover:bg-primary/5 cursor-pointer' | |
| }`} | |
| > | |
| <span>{isCurrentPlan ? t("subscription.current_plan", "Current Plan") : loading ? t("subscription.upgrading", "Upgrading...") : buttonText}</span> | |
| {!isCurrentPlan && !loading && <ArrowRight size={20} />} | |
| </motion.button> | |
| </motion.div> | |
| ); | |
| }; | |
| const SubscriptionPage = () => { | |
| const { t } = useTranslation(); | |
| const { user, updateUser } = useAuth(); | |
| const [loadingTier, setLoadingTier] = React.useState(null); | |
| const [notification, setNotification] = React.useState(null); | |
| const handleUpgrade = async (tierName) => { | |
| if (!user) return; | |
| setLoadingTier(tierName); | |
| try { | |
| const API_URL = import.meta.env.VITE_API_URL; | |
| const res = await axios.post(`${API_URL}/payments/change-tier/${user.id}`, { tier: tierName }); | |
| updateUser(res.data); | |
| setNotification({ | |
| type: 'success', | |
| message: t('subscription.upgrade_success', { tier: tierName }) || `Plan switched to ${tierName} successfully!` | |
| }); | |
| setTimeout(() => setNotification(null), 4000); | |
| } catch (err) { | |
| console.error('[Subscription] Upgrade failed:', err); | |
| setNotification({ | |
| type: 'error', | |
| message: err.response?.data?.message || 'Upgrade failed. Please try again.' | |
| }); | |
| setTimeout(() => setNotification(null), 4000); | |
| } finally { | |
| setLoadingTier(null); | |
| } | |
| }; | |
| const plans = [ | |
| { | |
| key: "Free", | |
| type: t("subscription.plans.free.title"), | |
| price: "0", | |
| description: t("subscription.plans.free.desc"), | |
| buttonText: t("subscription.plans.free.btn"), | |
| features: [ | |
| { text: t("subscription.plans.free.feat1"), included: true }, | |
| { text: t("subscription.plans.free.feat2"), included: true }, | |
| { text: t("subscription.plans.free.feat3"), included: true }, | |
| { text: t("subscription.plans.free.feat4"), included: true }, | |
| { text: t("subscription.plans.free.feat5"), included: true }, | |
| ] | |
| }, | |
| { | |
| key: "Pro", | |
| type: t("subscription.plans.pro.title"), | |
| price: "10", | |
| popular: true, | |
| description: t("subscription.plans.pro.desc"), | |
| buttonText: t("subscription.plans.pro.btn"), | |
| features: [ | |
| { text: t("subscription.plans.pro.feat1"), included: true }, | |
| { text: t("subscription.plans.pro.feat2"), included: true }, | |
| { text: t("subscription.plans.pro.feat3"), included: true }, | |
| { text: t("subscription.plans.pro.feat4"), included: true }, | |
| { text: t("subscription.plans.pro.feat5"), included: true }, | |
| ] | |
| }, | |
| { | |
| key: "Ultra", | |
| type: t("subscription.plans.ultra.title"), | |
| price: "25", | |
| description: t("subscription.plans.ultra.desc"), | |
| buttonText: t("subscription.plans.ultra.btn"), | |
| features: [ | |
| { text: t("subscription.plans.ultra.feat1"), included: true }, | |
| { text: t("subscription.plans.ultra.feat2"), included: true }, | |
| { text: t("subscription.plans.ultra.feat3"), included: true }, | |
| { text: t("subscription.plans.ultra.feat4"), included: true }, | |
| { text: t("subscription.plans.ultra.feat5"), included: true }, | |
| { text: t("subscription.plans.ultra.feat6"), included: true }, | |
| ] | |
| } | |
| ]; | |
| const currentUserTier = user?.subscriptionTier || "Free"; | |
| return ( | |
| <div className="flex h-screen bg-background overflow-hidden"> | |
| <Sidebar /> | |
| <main className="flex-grow flex flex-col relative overflow-y-auto graph-paper-bg scrollbar-hide"> | |
| {/* Notification Banner */} | |
| <AnimatePresence> | |
| {notification && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: -50 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| exit={{ opacity: 0, y: -50 }} | |
| className={`fixed top-6 left-1/2 -translate-x-1/2 px-8 py-4 z-50 rough-border shadow-2xl font-bold text-lg text-white ${ | |
| notification.type === 'success' ? 'bg-primary' : 'bg-error' | |
| }`} | |
| > | |
| {notification.message} | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| {/* Background Elements */} | |
| <div className="absolute top-0 right-0 p-20 opacity-10 pointer-events-none"> | |
| <Sparkles size={400} className="text-secondary" /> | |
| </div> | |
| <section className="p-8 md:p-16 flex flex-col gap-10 max-w-7xl mx-auto w-full relative z-10 pb-16"> | |
| <div className="text-center flex flex-col items-center gap-6"> | |
| <motion.div | |
| initial={{ scale: 0 }} | |
| animate={{ scale: 1 }} | |
| className="w-24 h-24 bg-secondary rounded-full flex items-center justify-center border-4 border-primary shadow-xl" | |
| > | |
| <Zap size={48} className="text-primary" /> | |
| </motion.div> | |
| <h1 className="text-7xl font-display-lg text-primary leading-tight max-w-3xl"> | |
| {t("subscription.title_p1", "Unlock the ")} | |
| <span className="highlighter-bg">{t("subscription.title_highlight", "Pro Tools")}</span> | |
| {t("subscription.title_p2", " of TaskFlow")} | |
| </h1> | |
| <p className="font-accent-note text-3xl text-secondary -rotate-1 max-w-xl"> | |
| {t("subscription.subtitle")} | |
| </p> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-10 items-stretch"> | |
| {plans.map((plan, i) => ( | |
| <PricingCard | |
| key={i} | |
| {...plan} | |
| isCurrentPlan={currentUserTier.toLowerCase() === plan.key.toLowerCase()} | |
| onClick={() => handleUpgrade(plan.key)} | |
| loading={loadingTier === plan.key} | |
| /> | |
| ))} | |
| </div> | |
| <div className="w-full overflow-x-auto"> | |
| <table className="w-full text-sm border-2 border-primary/15 rough-border"> | |
| <thead> | |
| <tr className="border-b-2 border-primary/15"> | |
| <th className="text-left p-4 font-bold text-on-surface-variant uppercase tracking-widest text-xs">{t('subscription.compare.feature', 'Feature')}</th> | |
| <th className="p-4 font-bold text-primary text-center">{t('subscription.plans.free.title', 'Free')}</th> | |
| <th className="p-4 font-bold text-secondary text-center bg-secondary/5">{t('subscription.plans.pro.title', 'Pro')}</th> | |
| <th className="p-4 font-bold text-primary text-center">{t('subscription.plans.ultra.title', 'Ultra')}</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {[ | |
| { icon: Bot, label: t('subscription.compare.ai_gens', 'AI Generations / day'), free: '3', pro: '200', ultra: '∞' }, | |
| { icon: Layout, label: t('subscription.compare.canvases', 'Canvases'), free: '3', pro: '∞', ultra: '∞' }, | |
| { icon: Sparkles, label: t('subscription.compare.nodes', 'Nodes per canvas'), free: '200', pro: '∞', ultra: '∞' }, | |
| { icon: Heart, label: t('subscription.compare.habits', 'Habits'), free: '3', pro: '∞', ultra: '∞' }, | |
| { icon: Users, label: t('subscription.compare.board_members', 'Board members'), free: '2', pro: '10', ultra: '∞' }, | |
| { icon: BookTemplate, label: t('subscription.compare.custom_templates', 'Custom Templates'), free: false, pro: false, ultra: true }, | |
| ].map((row, i) => ( | |
| <tr key={i} className={`border-b border-primary/10 ${i % 2 === 0 ? '' : 'bg-primary/2'}`}> | |
| <td className="p-4 font-semibold text-on-surface-variant flex items-center gap-2"> | |
| <row.icon size={16} className="text-secondary flex-shrink-0" /> | |
| {row.label} | |
| </td> | |
| {[row.free, row.pro, row.ultra].map((val, j) => ( | |
| <td key={j} className={`p-4 text-center font-bold ${j === 1 ? 'bg-secondary/5' : ''}`}> | |
| {val === true ? ( | |
| <Check size={18} className="text-secondary mx-auto" /> | |
| ) : val === false ? ( | |
| <X size={18} className="text-on-surface-variant/30 mx-auto" /> | |
| ) : ( | |
| <span className={val === '∞' ? 'text-secondary text-lg' : 'text-primary'}>{val}</span> | |
| )} | |
| </td> | |
| ))} | |
| </tr> | |
| ))} | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| export default SubscriptionPage; | |