Merge version_1 into main

Merge version_1 into main
This commit was merged in pull request #2.
This commit is contained in:
2026-02-04 11:23:05 +00:00
2 changed files with 181 additions and 0 deletions

108
src/app/shop/page.tsx Normal file
View File

@@ -0,0 +1,108 @@
'use client';
import { useEffect, useState } from 'react';
import ProductCard from '@/components/ProductCard';
import { ThemeProvider } from '@/components/theme-provider';
import Navbar from '@/components/Navbar';
import Footer from '@/components/Footer';
interface StripeProduct {
id: string;
name: string;
description: string | null;
images: string[];
default_price: {
id: string;
unit_amount: number;
currency: string;
} | null;
metadata: Record<string, string>;
}
export default function ShopPage() {
const [products, setProducts] = useState<StripeProduct[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchProducts = async () => {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || ''}/stripe/project/products?projectId=32a65411-f209-4b18-ba90-6f24fb6d2dcb&expandDefaultPrice=true`
);
if (!response.ok) throw new Error('Failed to fetch products');
const data = await response.json();
setProducts(data.data?.data || []);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load products');
} finally {
setLoading(false);
}
};
fetchProducts();
}, []);
const handleCheckout = async (productId: string, priceId: string) => {
try {
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL || ''}/stripe/project/checkout-session`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
projectId: '32a65411-f209-4b18-ba90-6f24fb6d2dcb',
items: [{ priceId, quantity: 1 }],
successUrl: window.location.origin + '/shop?success=true',
cancelUrl: window.location.origin + '/shop?canceled=true',
}),
}
);
if (!response.ok) throw new Error('Failed to create checkout session');
const data = await response.json();
if (data.data?.url) {
window.location.href = data.data.url;
}
} catch (err) {
alert(err instanceof Error ? err.message : 'Checkout failed');
}
};
return (
<ThemeProvider attribute="class" defaultTheme="light" enableSystem disableTransitionOnChange>
<div className="min-h-screen flex flex-col">
<Navbar />
<main className="flex-1 container mx-auto px-4 py-12">
<h1 className="text-4xl font-bold mb-8 text-center">Our Products</h1>
{loading && (
<div className="flex justify-center items-center min-h-[200px]">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
)}
{error && (
<div className="text-center text-red-500 py-8">{error}</div>
)}
{!loading && !error && products.length === 0 && (
<div className="text-center text-muted-foreground py-8">
No products available yet.
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map((product) => (
<ProductCard
key={product.id}
product={product}
onCheckout={handleCheckout}
/>
))}
</div>
</main>
<Footer />
</div>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,73 @@
'use client';
import Image from 'next/image';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
interface StripeProduct {
id: string;
name: string;
description: string | null;
images: string[];
default_price: {
id: string;
unit_amount: number;
currency: string;
} | null;
metadata: Record<string, string>;
}
interface ProductCardProps {
product: StripeProduct;
onCheckout: (productId: string, priceId: string) => void;
}
export default function ProductCard({ product, onCheckout }: ProductCardProps) {
const formatPrice = (amount: number, currency: string) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency.toUpperCase(),
}).format(amount / 100);
};
return (
<Card className="flex flex-col h-full overflow-hidden hover:shadow-lg transition-shadow">
{product.images[0] && (
<div className="relative aspect-square overflow-hidden">
<Image
src={product.images[0]}
alt={product.name}
fill
className="object-cover hover:scale-105 transition-transform duration-300"
/>
</div>
)}
<CardHeader className="flex-none">
<CardTitle className="line-clamp-1">{product.name}</CardTitle>
{product.default_price && (
<p className="text-2xl font-bold text-primary">
{formatPrice(product.default_price.unit_amount, product.default_price.currency)}
</p>
)}
</CardHeader>
<CardContent className="flex-1">
{product.description && (
<CardDescription className="line-clamp-3">{product.description}</CardDescription>
)}
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={() => {
if (product.default_price) {
onCheckout(product.id, product.default_price.id);
}
}}
disabled={!product.default_price}
>
{product.default_price ? 'Buy Now' : 'Unavailable'}
</Button>
</CardFooter>
</Card>
);
}