Skip to content
Draft
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
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"editor.formatOnSave": true
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
}
"editor.formatOnSave": true
}
4 changes: 4 additions & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const nextConfig = {
pathname: "/pw/**",
protocol: "https",
},
{
hostname: "files.stripe.com",
protocol: "https",
},
],
},
reactStrictMode: true,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@
"react": "^18",
"react-countup": "^6.5.3",
"react-dom": "^18",
"react-icons": "^5.5.0",
"react-intersection-observer": "^9.13.1",
"react-lazy-load-image-component": "^1.6.2",
"react-multi-carousel": "^2.8.5",
"react-scroll": "^1.9.0",
"react-tsparticles": "^2.9.3",
"react-visibility-sensor": "^5.1.1",
"stripe": "^17.7.0",
"styled-components": "^6.1.13",
"tsparticles": "^2.9.3"
},
Expand Down
Binary file added public/default-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 31 additions & 28 deletions src/components/Merch/MerchItems.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
import Image from "next/image";
import Link from "next/link";
import PaymentButton from "./PaymentButton";

export const MerchItems = ({
clothingImg,
link,
price,
priceId,
title,
}: {
clothingImg: string;
link: string;
price: string;
price: number;
priceId: string;
title: string;
}) => {
return (
<Link className="group w-full max-w-xs cursor-pointer" href={link}>
<div className="relative mx-auto mt-5 h-auto max-w-sm rounded-lg border-4 border-black p-5 shadow-lg transition-all duration-300 ease-in-out group-hover:scale-105 group-hover:opacity-90">
<div className="relative w-full overflow-hidden rounded-t-lg border-b-4 border-black">
<Image
alt="Merch Image"
className="object-contain transition-all duration-300 group-hover:scale-110"
height={533} // Matches aspect ratio (3:4)
src={clothingImg}
width={400} // Adjust based on aspect ratio
/>
</div>
<div className="flex items-center justify-center">
<div className="group w-full max-w-xs">
<div className="relative mx-auto mt-5 h-auto max-w-sm rounded-lg border-4 border-black p-5 shadow-lg transition-all duration-300 ease-in-out group-hover:scale-105 group-hover:opacity-90">
<div className="relative w-full overflow-hidden rounded-t-lg border-b-4 border-black">
<Image
alt={title} // Improved accessibility
className="object-contain transition-all duration-300 group-hover:scale-110"
height={533} // Matches aspect ratio (3:4)
priority // Faster loading
src={clothingImg || "/default-image.png"} // Fixed Image Issue
width={400}
/>
</div>

{/* Title and Price Section */}
<div className="rounded-b-lg bg-white py-3">
<h1 className="line-clamp-2 h-16 overflow-hidden text-center text-xl font-bold text-gray-800">
{title}
</h1>
<p className="mt-2 text-center text-lg text-gray-700">${price} CAD</p>
</div>
{/* Title and Price Section */}
<div className="rounded-b-lg bg-white py-3">
<h1 className="line-clamp-2 h-16 overflow-hidden text-center text-xl font-bold text-gray-800">
{title}
</h1>
<p className="mt-2 text-center text-lg text-gray-700">
{price?.toFixed(2)} CAD
</p>
</div>

{/* Button Section */}
<div className="mb-3 flex justify-center">
<button className="rounded-md bg-black px-10 py-3 text-white transition-all duration-200 ease-in-out hover:bg-gradient-to-l hover:from-[#00B7FF] hover:via-[#64CDB2] hover:to-[#C9E265]">
Buy Now
</button>
<div className="mb-3 flex justify-center">
<PaymentButton priceId={priceId} />
</div>
</div>
</div>
</Link>
</div>
);
};

export default MerchItems;
74 changes: 58 additions & 16 deletions src/components/Merch/MerchPageContent.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,90 @@
"use client";

import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { NewlineText } from "../../utility/Helpers";
import { merchPageLottieOptions } from "../../utility/LottieOptions";
import { MerchItems } from "./MerchItems";
import { MerchItemsData } from "../../lib/data/MerchData";
import dynamic from "next/dynamic";

const Lottie = dynamic(() => import("lottie-react"), { ssr: false });

const MerchPageContent = () => {
const [merchItems, setMerchItems] = useState<
{
id: string;
price: number;
priceId: string;
title: string;
clothingImg: string;
active: boolean;
}[]
>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

// ✅ Fetch merch data when the component loads
useEffect(() => {
async function fetchMerchData() {
try {
const response = await fetch("/api/getProducts");

const data = await response.json();
if (!response.ok)
throw new Error(data.error || "Failed to load merch data");
setMerchItems(data.prices);
} catch (err) {
setError((err as Error).message);
} finally {
setLoading(false);
}
}
fetchMerchData();
}, []); // Runs only once when component mounts

if (loading) return <p>Loading merch...</p>;
if (error) return <p>Error: {error}</p>;

return (
<div
className="flex flex-col items-center justify-center"
id="merchPageTop"
>
{/* Header Section */}
<div className="mb-8 flex w-full flex-col items-center bg-black pb-32">
<div className="flex w-full items-center justify-center">
<div className="w-full max-w-xl">
<Lottie {...merchPageLottieOptions} />
</div>
</div>

<motion.div
<motion.h1
animate={{ opacity: 1 }}
className="mt-4 text-center text-8xl font-bold text-white"
className="mt-4 text-center text-5xl font-bold text-white sm:text-7xl lg:text-8xl"
initial={{ opacity: 0 }}
transition={{ delay: 0.75 }}
>
{NewlineText("Our Merch")}
</motion.div>
</motion.h1>
</div>
<h1 className="m-5 w-[400px] break-words p-4 px-10 text-black">
{JSON.stringify(merchItems)}
</h1>

{/* Merch Items Grid - Max 3 per row */}
{/* Merch Items Grid */}
<div className="my-6 flex w-full flex-wrap justify-center gap-6 px-4">
{MerchItemsData.map((merchItem) => (
<div className="w-full p-2 sm:w-1/2 lg:w-1/3" key={merchItem.id}>
<MerchItems
clothingImg={merchItem.clothingImg}
link={merchItem.link}
price={merchItem.price}
title={merchItem.title}
/>
</div>
))}
{merchItems.map(
(merchItem) =>
merchItem.active ? ( // Check if `active` is true
<div className="w-full p-2 sm:w-1/2 lg:w-1/3" key={merchItem.id}>
<MerchItems
clothingImg={merchItem.clothingImg}
price={merchItem.price}
priceId={merchItem.id}
title={merchItem.title}
/>
</div>
) : null, // If not active, don't render anything
)}
</div>
</div>
);
Expand Down
42 changes: 42 additions & 0 deletions src/components/Merch/PaymentButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"use client";

import { useState } from "react";

export default function PaymentButton({ priceId }: { priceId: string }) {
const [loading, setLoading] = useState(false);

const handleCheckout = async () => {
setLoading(true);
try {
const res = await fetch("/api/checkout", {
body: JSON.stringify({ priceId }), // Send the priceId to the API
headers: {
"Content-Type": "application/json",
},
method: "POST",
});
const data = await res.json();

if (data.url) {
window.location.href = data.url; // Redirect user to Stripe Checkout
} else {
alert("Failed to create checkout session.");
}
} catch (error) {
console.error("Error:", error);
alert("Something went wrong.");
} finally {
setLoading(false);
}
};

return (
<button
className="rounded bg-green-600 px-10 py-3 font-bold text-white hover:bg-green-500"
disabled={loading}
onClick={handleCheckout}
>
{loading ? "Processing..." : "Pay Now"}
</button>
);
}
3 changes: 3 additions & 0 deletions src/lib/data/MerchData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type MerchItem = {
link: string;
price: string;
title: string;
priceId: string;
};

export const MerchItemsData: MerchItem[] = [
Expand All @@ -12,13 +13,15 @@ export const MerchItemsData: MerchItem[] = [
id: 0,
link: "https://docs.google.com/forms/d/e/1FAIpQLSfpXS4hisen7IBvMGZnrfYWH600W_vpJwW0-b7blsA-D5Dq2w/viewform",
price: "29.95",
priceId: "price_1QuK3UIJ8lAYncJXcSjd0Hkl",
title: "Classic Crew Neck",
},
{
clothingImg: "/image/merch/BasicCrewneckWithSleevePrint.jpg",
id: 1,
link: "https://docs.google.com/forms/d/e/1FAIpQLSfpXS4hisen7IBvMGZnrfYWH600W_vpJwW0-b7blsA-D5Dq2w/viewform",
price: "49.99",
priceId: "price_1QuKSpIJ8lAYncJXBja1aLhU",
title: "Crew Neck with Custom Sleeve Print",
},
];
47 changes: 47 additions & 0 deletions src/pages/api/checkout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { NextApiRequest, NextApiResponse } from "next";
import Stripe from "stripe";

// Initialize Stripe
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(405).json({ error: "❌ Method Not Allowed" });
}

try {
// Extract the priceId from the request body
const { priceId } = req.body;
if (!priceId) {
return res.status(400).json({ error: "Price ID is required" });
}

// Create a Stripe Checkout Session using the provided price ID
const session = await stripe.checkout.sessions.create({
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/merch`,
custom_fields: [
{
key: "name",
label: {
custom: "Full Name",
type: "custom",
},
optional: false,
type: "text",
},
],
line_items: [{ price: priceId, quantity: 1 }],
mode: "payment",
payment_method_types: ["card"],
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/success`,
});

return res.status(200).json({ url: session.url });
} catch (error) {
console.error("Error creating checkout session:", error);
return res.status(500).json({ error: "Internal Server Error" });
}
}
Loading