-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #81 from 0xdevcollins/feat/pricing
feat: add new animations and pricing section in sidebar
- Loading branch information
Showing
9 changed files
with
448 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.