Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/app/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { Metadata } from 'next';
import PricingTable from '@/components/pricing/PricingTable';

export const metadata: Metadata = {
title: 'Pricing | StrellerMinds',
description: 'Choose the best plan for your learning journey.',
};

export default function PricingPage() {
return (
<main className="min-h-screen bg-gray-50 pt-20">
<PricingTable />
</main>
);
}
166 changes: 166 additions & 0 deletions src/components/pricing/PricingTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { useState } from 'react';
import { Check, X } from 'lucide-react';
import { usePricing } from './usePricing';

interface PricingTier {
name: string;
description: string;
monthlyPrice: number;
annualPrice: number;
popular: boolean;
features: { name: string; included: boolean }[];
ctaText: string;
}

const tiers: PricingTier[] = [
{
name: 'Basic',
description: 'Essential features for individuals and beginners.',
monthlyPrice: 9.99,
annualPrice: 99,
popular: false,
features: [
{ name: 'Access to all basic courses', included: true },
{ name: 'Community forum access', included: true },
{ name: 'Email support', included: true },
{ name: 'Certificate of completion', included: false },
{ name: '1-on-1 mentorship', included: false },
],
ctaText: 'Get Started',
},
{
name: 'Pro',
description: 'Perfect for professionals looking to advance their career.',
monthlyPrice: 29.99,
annualPrice: 299,
popular: true,
features: [
{ name: 'Access to all basic courses', included: true },
{ name: 'Community forum access', included: true },
{ name: 'Priority email support', included: true },
{ name: 'Certificate of completion', included: true },
{ name: '1-on-1 mentorship', included: false },
],
ctaText: 'Start Free Trial',
},
{
name: 'Enterprise',
description: 'Advanced features for teams and organizations.',
monthlyPrice: 99.99,
annualPrice: 999,
popular: false,
features: [
{ name: 'Access to all basic courses', included: true },
{ name: 'Community forum access', included: true },
{ name: '24/7 Phone & Email support', included: true },
{ name: 'Certificate of completion', included: true },
{ name: '1-on-1 mentorship', included: true },
],
ctaText: 'Contact Sales',
},
];

export const PricingTable: React.FC = () => {
const [isAnnual, setIsAnnual] = useState(true);

// Example of using dynamic pricing data hook if applicable.
// In a real scenario, this hook might override the default prices.
const { price, loading } = usePricing({ country: 'US', currency: 'USD' });

return (
<div className="bg-gray-50 py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="text-center">
<h2 className="text-3xl font-extrabold text-gray-900 sm:text-4xl">
Simple, transparent pricing
</h2>
<p className="mt-4 text-xl text-gray-600">
Choose the plan that best fits your needs. No hidden fees.
</p>
</div>

<div className="mt-12 flex justify-center items-center gap-3">
<span className={`text-sm ${!isAnnual ? 'font-semibold text-gray-900' : 'text-gray-500'}`}>Monthly</span>
<button
type="button"
className={`${
isAnnual ? 'bg-indigo-600' : 'bg-gray-200'
} relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2`}
role="switch"
aria-checked={isAnnual}
onClick={() => setIsAnnual(!isAnnual)}
>
<span
aria-hidden="true"
className={`${
isAnnual ? 'translate-x-5' : 'translate-x-0'
} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out`}
/>
</button>
<span className={`text-sm ${isAnnual ? 'font-semibold text-gray-900' : 'text-gray-500'}`}>
Annually <span className="text-green-500 font-medium ml-1">(Save 20%)</span>
</span>
</div>

<div className="mt-16 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:gap-8">
{tiers.map((tier) => (
<div
key={tier.name}
className={`rounded-2xl border p-8 shadow-sm flex flex-col ${
tier.popular
? 'border-indigo-600 ring-2 ring-indigo-600 bg-white'
: 'border-gray-200 bg-white'
}`}
>
<div className="mb-4">
{tier.popular && (
<span className="inline-flex items-center rounded-full bg-indigo-100 px-3 py-1 text-sm font-semibold text-indigo-600 mb-4">
Most Popular
</span>
)}
<h3 className="text-2xl font-semibold text-gray-900">{tier.name}</h3>
<p className="mt-4 text-sm text-gray-500">{tier.description}</p>
</div>

<div className="mt-2 mb-8">
<span className="text-5xl font-extrabold text-gray-900">
${isAnnual ? tier.annualPrice : tier.monthlyPrice}
</span>
<span className="text-base font-medium text-gray-500">
/{isAnnual ? 'year' : 'month'}
</span>
</div>

<ul className="mt-6 space-y-4 flex-1">
{tier.features.map((feature, index) => (
<li key={index} className="flex space-x-3">
{feature.included ? (
<Check className="h-5 w-5 flex-shrink-0 text-green-500" aria-hidden="true" />
) : (
<X className="h-5 w-5 flex-shrink-0 text-gray-300" aria-hidden="true" />
)}
<span className={`text-sm ${feature.included ? 'text-gray-700' : 'text-gray-400 line-through'}`}>
{feature.name}
</span>
</li>
))}
</ul>

<button
className={`mt-8 block w-full rounded-md px-4 py-3 text-center text-sm font-semibold shadow-sm hover:scale-[1.02] transition-transform ${
tier.popular
? 'bg-indigo-600 text-white hover:bg-indigo-700 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600'
: 'bg-indigo-50 text-indigo-700 hover:bg-indigo-100'
}`}
>
{loading ? 'Loading...' : tier.ctaText}
</button>
</div>
))}
</div>
</div>
</div>
);
};

export default PricingTable;
32 changes: 27 additions & 5 deletions src/components/pricing/usePricing.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
// usePricing.ts
import { useEffect, useState } from "react";
import { fetchPrice } from "../services/pricing.api";

export function usePricing({ country, currency }) {
// Mock implementation of fetchPrice since pricing.api does not exist
const fetchPrice = async (
params: { country: string; currency: string; options: any; promoCode: string },
options: { signal: AbortSignal }
) => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (options.signal.aborted) {
reject(new Error("Aborted"));
} else {
resolve({ finalPrice: params.options.quantity * 10, currency: params.currency });
}
}, 1000);
options.signal.addEventListener("abort", () => clearTimeout(timeout));
});
};

interface UsePricingProps {
country: string;
currency: string;
}

export function usePricing({ country, currency }: UsePricingProps) {
const [options, setOptions] = useState({
quantity: 1,
premium: false,
});

const [promoCode, setPromoCode] = useState("");
const [price, setPrice] = useState(null);
const [price, setPrice] = useState<any>(null);
const [loading, setLoading] = useState(false);

useEffect(() => {
Expand All @@ -23,9 +44,10 @@ export function usePricing({ country, currency }) {
{ signal: controller.signal }
);
setPrice(data);
} catch (err) {
} catch (err: any) {
if (err.name === 'AbortError' || err.message === 'Aborted') return;
// fallback handling
setPrice((prev) => prev || { finalPrice: 0, currency });
setPrice((prev: any) => prev || { finalPrice: 0, currency });
} finally {
setLoading(false);
}
Expand Down