FlowWeb / src /pages /SubscriptionPage.jsx
danylokhodus's picture
feat: rebrand QuestboardAI -> TaskFlow everywhere (UI, locales, index.html)
abb5ee1
Raw
History Blame Contribute Delete
11.7 kB
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;