Skip to content

Commit

Permalink
Merge pull request #81 from 0xdevcollins/feat/pricing
Browse files Browse the repository at this point in the history
feat: add new animations and pricing section in sidebar
  • Loading branch information
0xdevcollins authored Feb 22, 2025
2 parents 6c333be + 144d760 commit f52112c
Show file tree
Hide file tree
Showing 9 changed files with 448 additions and 2 deletions.
6 changes: 6 additions & 0 deletions app/api/user/pricing/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { handleApiRequest } from '@/lib/api-utils';
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest): Promise<NextResponse> {
return handleApiRequest(request, '/pricing', 'GET');
}
24 changes: 24 additions & 0 deletions app/api/user/pricing/subscribe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { handleApiRequest } from '@/lib/api-utils';
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest): Promise<NextResponse> {
try {
const { searchParams } = new URL(request.url);
const planId = searchParams.get('planId');

if (!planId) {
return NextResponse.json(
{ status: 'error', message: 'planId is required' },
{ status: 400 }
);
}

return await handleApiRequest(request, `/user/subscriptions/subscribe/${planId}`, 'POST');
} catch (error) {
console.error('Error processing subscription request:', error);
return NextResponse.json(
{ status: 'error', message: 'Internal server error' },
{ status: 500 }
);
}
}
105 changes: 105 additions & 0 deletions app/dashboard/payment-status/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use client';

import { useEffect, useState } from 'react';
import { useSearchParams, useRouter } from 'next/navigation';
import { CheckCircle, XCircle, ArrowLeft, RefreshCcw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import axios from 'axios';

export default function PaymentStatusPage() {
const searchParams = useSearchParams();
const router = useRouter();
const [status, setStatus] = useState<'success' | 'failed' | null>(null);
const [txRef, setTxRef] = useState<string | null>(null);
const [transactionId, setTransactionId] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const paymentStatus = searchParams.get('status');
const ref = searchParams.get('tx_ref');
const txId = searchParams.get('transaction_id');

if (paymentStatus && ref && txId) {
setStatus(paymentStatus === 'successful' ? 'success' : 'failed');
setTxRef(ref);
setTransactionId(txId);

// axios.post(`/api/payment/verify`, { tx_ref: ref, transaction_id: txId })
// .then((res) => console.log("Verification Response:", res.data))
// .catch((err) => console.error("Verification Error:", err))
// .finally(() => setIsLoading(false));
setIsLoading(false);
}
}, [searchParams]);

if (isLoading) {
return (
<div className="flex justify-center items-center min-h-screen bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<RefreshCcw className="h-8 w-8 text-primary animate-spin" />
</div>
);
}

return (
<div className="flex flex-col justify-center items-center min-h-screen p-4 bg-gradient-to-b from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800">
<Card className="w-full max-w-md shadow-xl bg-white dark:bg-gray-800">
<CardHeader className="text-center space-y-4">
{status === 'success' ? (
<CheckCircle className="h-16 w-16 text-green-500 dark:text-green-400 mx-auto" />
) : (
<XCircle className="h-16 w-16 text-red-500 dark:text-red-400 mx-auto" />
)}
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
{status === 'success' ? 'Payment Successful!' : 'Payment Failed'}
</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
Transaction ID: <span className="font-semibold">{transactionId || 'N/A'}</span>
</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
Reference: <span className="font-semibold">{txRef || 'N/A'}</span>
</p>
</CardHeader>

<CardContent className="space-y-4">
{status === 'success' ? (
<p className="text-center text-gray-600 dark:text-gray-300">
Your subscription is now active. You will receive a confirmation email shortly.
</p>
) : (
<Alert className="bg-red-50 dark:bg-red-900/30 border-red-100 dark:border-red-900">
<AlertDescription>
Your payment could not be processed. This might be due to insufficient funds or a
temporary issue with your payment method.
</AlertDescription>
</Alert>
)}
</CardContent>

<CardFooter className="flex flex-col sm:flex-row gap-3 justify-center">
<Button
variant="outline"
size="lg"
onClick={() => router.push('/')}
className="w-full sm:w-auto"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back Home
</Button>
<Button
size="lg"
onClick={() => router.push(status === 'success' ? '/dashboard' : '/pricing')}
className={`w-full sm:w-auto ${
status === 'success'
? 'bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700'
: 'bg-primary hover:bg-primary/90'
}`}
>
{status === 'success' ? 'Go to Dashboard' : 'Retry Payment'}
</Button>
</CardFooter>
</Card>
</div>
);
}
99 changes: 99 additions & 0 deletions app/dashboard/pricing/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
'use client';

import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { PricingCard } from '@/shared/PricingCard';
import type { PricingPlan } from '@/types/interface';
import axios from 'axios';

const fadeIn = {
hidden: { opacity: 0, y: 30 },
visible: { opacity: 1, y: 0, transition: { duration: 0.5 } },
};

export default function PricingPage() {
const [plans, setPlans] = useState<PricingPlan[]>([]);


useEffect(() => {
const fetchPricing = async () => {
try {
const res = await axios.get('/api/user/pricing');
const json = res.data;

if (json.status === 'success' && json.data) {
let fetchedPlans = Array.isArray(json.data) ? json.data : json.data.plans || [];

fetchedPlans = fetchedPlans.map((plan: { price: string; }) => ({
...plan,
price: `₦${parseInt(plan.price, 10).toLocaleString()}`,
}));

const businessPlanIndex = fetchedPlans.findIndex(
(plan: { name: string; }) => plan.name.toLowerCase() === 'business plan'
);

if (businessPlanIndex !== -1) {
const businessPlan = fetchedPlans.splice(businessPlanIndex, 1)[0];
fetchedPlans.splice(1, 0, businessPlan);
}

setPlans(fetchedPlans);
} else {
console.error('API response error:', json);
}
} catch (error) {
console.error('Error fetching pricing plans:', error);
}
};

fetchPricing();
}, []);

if (!plans.length) {
return (
<motion.div
className="flex min-h-[400px] items-center justify-center"
initial="hidden"
animate="visible"
variants={fadeIn}
>
<p className="text-muted-foreground">Loading pricing plans...</p>
</motion.div>
);
}

return (
<motion.div
className="relative mx-auto max-w-7xl px-4 py-20"
initial="hidden"
animate="visible"
variants={fadeIn}
>
<div className="absolute inset-0 -z-10 h-full w-full bg-white dark:bg-gray-950">
<div className="absolute inset-0 bg-[linear-gradient(to_right,#4f4f4f2e_1px,transparent_1px),linear-gradient(to_bottom,#4f4f4f2e_1px,transparent_1px)] bg-[size:14px_24px]" />
</div>
<motion.div className="text-center space-y-4 mb-16" variants={fadeIn}>
<h1 className="text-4xl font-bold tracking-tight">Simple, Transparent Pricing</h1>
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
Choose the perfect plan for your needs. Always know what you'll pay.
</p>
</motion.div>
<motion.div
className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 xl:gap-10"
initial="hidden"
animate="visible"
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { staggerChildren: 0.2 } },
}}
>
{plans.map((plan, index) => (
<motion.div key={plan.id} variants={fadeIn}>
<PricingCard plan={plan} isPopular={index === 1} />
</motion.div>
))}
</motion.div>
</motion.div>
);
}
40 changes: 40 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,43 @@
.animate-auto-scroll {
animation: auto-scroll 10s linear infinite;
}



@keyframes scale-in {
from { transform: scale(0); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}

@keyframes bounce-in {
0% { transform: scale(0); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}

@keyframes shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}

@keyframes fade-in-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}

.animate-scale-in {
animation: scale-in 0.5s ease-out forwards;
}

.animate-bounce-in {
animation: bounce-in 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}

.animate-shake {
animation: shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) forwards;
}

.animate-fade-in-up {
animation: fade-in-up 0.5s ease-out forwards;
}
Loading

0 comments on commit f52112c

Please sign in to comment.