Skip to content

Commit

Permalink
Add checkout and confirmation pages, implement order service, and upd…
Browse files Browse the repository at this point in the history
…ate routing
  • Loading branch information
gkhaavik committed Dec 1, 2024
1 parent 56160f4 commit b794744
Show file tree
Hide file tree
Showing 11 changed files with 388 additions and 3 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.2",
"axios": "^1.7.7",
Expand Down
161 changes: 161 additions & 0 deletions src/app/(client)/checkout/_components/CheckoutPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"use client";

import React, { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useCart } from '@/context/CartContext';
import { useAuth } from '@/context/AuthContext';
import { orderService } from '@/services/orderService';
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { useToast } from '@/hooks/use-toast';
import { Separator } from '@/components/ui/separator';

export function CheckoutPage() {
const { cart, totalPrice, clearCart } = useCart();
const { user } = useAuth();
const { toast } = useToast();
const router = useRouter();
const [isProcessing, setIsProcessing] = useState(false);

const handleCheckout = async () => {
if (!user) {
toast({
title: "Please log in",
description: "You need to be logged in to checkout",
variant: "destructive",
});
router.push('/auth/login');
return;
}

try {
setIsProcessing(true);

// Create order lines from cart
const orderLines = cart.map(item => ({
productId: item.id,
quantity: item.quantity,
...(item.selectedVariant && { variantId: item.selectedVariant.id }),
}));

// Create order
const orderResponse = await orderService.createOrder(user.id, {
currency: "DKK",
orderLines,
});

// Create MobilePay payment
const paymentResponse = await orderService.createMobilePayPayment({
orderId: orderResponse.order.id.toString(),
currency: "DKK",
paymentMethod: "WALLET",
returnUrl: `${window.location.origin}/checkout/confirmation`,
});

// Clear cart and redirect to MobilePay
clearCart();
window.location.href = paymentResponse.redirectUrl;

} catch (error) {
toast({
title: "Checkout failed",
description: error instanceof Error ? error.message : "Please try again",
variant: "destructive",
});
} finally {
setIsProcessing(false);
}
};

const formatPrice = (amount: number) => {
return new Intl.NumberFormat('da-DK', {
style: 'currency',
currency: 'DKK',
}).format(amount);
};

if (cart.length === 0) {
return (
<div className="container mx-auto py-8 text-center">
<h1 className="text-2xl font-bold mb-4">Your cart is empty</h1>
<Button onClick={() => router.push('/')}>
Continue Shopping
</Button>
</div>
);
}

return (
<div className="container mx-auto py-8">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Order Summary */}
<div>
<Card>
<CardHeader>
<CardTitle>Order Summary</CardTitle>
<CardDescription>Review your items</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{cart.map((item) => (
<div key={item.cartItemId} className="flex justify-between">
<div>
<p className="font-medium">{item.name}</p>
{item.selectedVariant && (
<p className="text-sm text-muted-foreground">
Size: {item.selectedVariant.options[0].value}
</p>
)}
<p className="text-sm text-muted-foreground">
Quantity: {item.quantity}
</p>
</div>
<p className="font-medium">
{formatPrice(item.price.amount * item.quantity)}
</p>
</div>
))}
</CardContent>
<CardFooter className="flex flex-col">
<Separator className="my-4" />
<div className="flex justify-between w-full">
<p className="font-bold">Total</p>
<p className="font-bold">{formatPrice(totalPrice)}</p>
</div>
</CardFooter>
</Card>
</div>

{/* Payment */}
<div>
<Card>
<CardHeader>
<CardTitle>Payment</CardTitle>
<CardDescription>Choose your payment method</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
You will be redirected to MobilePay to complete your payment
</p>
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={handleCheckout}
disabled={isProcessing}
>
{isProcessing ? "Processing..." : "Pay with MobilePay"}
</Button>
</CardFooter>
</Card>
</div>
</div>
</div>
);
}
61 changes: 61 additions & 0 deletions src/app/(client)/checkout/_components/ConfirmationPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use client";

import React from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { CheckCircle, XCircle } from "lucide-react";

export function ConfirmationPage() {
const router = useRouter();
const searchParams = useSearchParams();
const success = searchParams.get('success') === 'true';
const orderId = searchParams.get('orderId');

return (
<div className="container mx-auto py-16">
<Card className="max-w-md mx-auto text-center">
<CardHeader>
<div className="flex justify-center mb-4">
{success ? (
<CheckCircle className="h-12 w-12 text-green-500" />
) : (
<XCircle className="h-12 w-12 text-red-500" />
)}
</div>
<CardTitle className="text-2xl">
{success ? 'Order Confirmed' : 'Payment Failed'}
</CardTitle>
{orderId && success && (
<p className="text-sm text-muted-foreground mt-2">
Order #{orderId}
</p>
)}
</CardHeader>
<CardContent className="space-y-4">
<p className="text-muted-foreground">
{success
? 'Thank you for your order. You will receive a confirmation email shortly.'
: 'Sorry, your payment was not successful. Please try again.'}
</p>
<div className="flex flex-col gap-2">
{!success && (
<Button
variant="default"
onClick={() => router.push('/checkout')}
>
Try Again
</Button>
)}
<Button
variant={success ? "default" : "secondary"}
onClick={() => router.push('/')}
>
Continue Shopping
</Button>
</div>
</CardContent>
</Card>
</div>
);
}
5 changes: 5 additions & 0 deletions src/app/(client)/checkout/confirmation/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ConfirmationPage } from "../_components/ConfirmationPage";

export default function Page() {
return <ConfirmationPage />;
}
5 changes: 5 additions & 0 deletions src/app/(client)/checkout/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { CheckoutPage } from "./_components/CheckoutPage";

export default function Page() {
return <CheckoutPage />;
}
31 changes: 31 additions & 0 deletions src/components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"use client"

import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"

import { cn } from "@/lib/utils"

const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName

export { Separator }
32 changes: 30 additions & 2 deletions src/components/webshop/CartSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use client";

import React from 'react';
import { useRouter } from 'next/navigation';
import { useCart } from '@/context/CartContext';
import { Button } from "@/components/ui/button";
import {
Expand All @@ -9,15 +10,22 @@ import {
SheetHeader,
SheetTitle,
SheetTrigger,
SheetClose,
} from "@/components/ui/sheet";
import { ShoppingCart, Minus, Plus, Trash2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { useAuth } from '@/context/AuthContext';
import { useToast } from '@/hooks/use-toast';
import { CartItem } from '@/types/product';
import Image from 'next/image';
import placeholderImage from '@/public/placeholder.webp';
import { CartItem } from '@/types/product';

export const CartSheet = () => {
const router = useRouter();
const { cart, totalItems, totalPrice, updateCartItemQuantity, removeFromCart } = useCart();
const { isAuthenticated } = useAuth();
const { toast } = useToast();
const sheetCloseRef = React.useRef<HTMLButtonElement>(null);

const formatPrice = (amount: number, currency: string) => {
return new Intl.NumberFormat('da-DK', {
Expand All @@ -26,6 +34,22 @@ export const CartSheet = () => {
}).format(amount);
};

const handleCheckout = () => {
if (!isAuthenticated) {
toast({
title: "Login Required",
description: "Please log in to proceed with checkout",
variant: "destructive",
});
sheetCloseRef.current?.click(); // Close the cart sheet
router.push('/auth/login');
return;
}

sheetCloseRef.current?.click(); // Close the cart sheet
router.push('/checkout');
};

const getVariantDisplay = (item: CartItem) => {
if (!item.selectedVariant) return null;

Expand Down Expand Up @@ -150,11 +174,15 @@ export const CartSheet = () => {
<span>{formatPrice(totalPrice, cart[0].price.currency)}</span>
</div>
</div>
<Button className="w-full">
<Button
className="w-full"
onClick={handleCheckout}
>
Checkout ({totalItems} {totalItems === 1 ? 'item' : 'items'})
</Button>
</div>
)}
<SheetClose ref={sheetCloseRef} className="hidden" />
</SheetContent>
</Sheet>
);
Expand Down
2 changes: 1 addition & 1 deletion src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
title: "Logged out",
description: "You have been successfully logged out",
});
router.push('/login');
router.push('/auth/login');
};

return (
Expand Down
Loading

0 comments on commit b794744

Please sign in to comment.