diff --git a/.vscode/settings.json b/.vscode/settings.json index e11b984..09c7543 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,5 +20,5 @@ "editor.formatOnSave": true }, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true, -} \ No newline at end of file + "editor.formatOnSave": true +} diff --git a/next.config.mjs b/next.config.mjs index a14c397..0a38198 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -10,6 +10,10 @@ const nextConfig = { pathname: "/pw/**", protocol: "https", }, + { + hostname: "files.stripe.com", + protocol: "https", + }, ], }, reactStrictMode: true, diff --git a/package.json b/package.json index 7cd1b20..c7740f2 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/public/default-image.png b/public/default-image.png new file mode 100644 index 0000000..587284b Binary files /dev/null and b/public/default-image.png differ diff --git a/src/components/Merch/MerchItems.tsx b/src/components/Merch/MerchItems.tsx index ddea7d3..c63cdf5 100644 --- a/src/components/Merch/MerchItems.tsx +++ b/src/components/Merch/MerchItems.tsx @@ -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 ( - -
-
- Merch Image -
+
+
+
+
+ {title} +
- {/* Title and Price Section */} -
-

- {title} -

-

${price} CAD

-
+ {/* Title and Price Section */} +
+

+ {title} +

+

+ {price?.toFixed(2)} CAD +

+
- {/* Button Section */} -
- +
+ +
- +
); }; + export default MerchItems; diff --git a/src/components/Merch/MerchPageContent.tsx b/src/components/Merch/MerchPageContent.tsx index 3c1b2e9..2c9596d 100644 --- a/src/components/Merch/MerchPageContent.tsx +++ b/src/components/Merch/MerchPageContent.tsx @@ -1,19 +1,55 @@ +"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(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

Loading merch...

; + if (error) return

Error: {error}

; + return (
- {/* Header Section */}
@@ -21,28 +57,34 @@ const MerchPageContent = () => {
- {NewlineText("Our Merch")} - +
+

+ {JSON.stringify(merchItems)} +

- {/* Merch Items Grid - Max 3 per row */} + {/* Merch Items Grid */}
- {MerchItemsData.map((merchItem) => ( -
- -
- ))} + {merchItems.map( + (merchItem) => + merchItem.active ? ( // Check if `active` is true +
+ +
+ ) : null, // If not active, don't render anything + )}
); diff --git a/src/components/Merch/PaymentButton.tsx b/src/components/Merch/PaymentButton.tsx new file mode 100644 index 0000000..b6f32e2 --- /dev/null +++ b/src/components/Merch/PaymentButton.tsx @@ -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 ( + + ); +} diff --git a/src/lib/data/MerchData.ts b/src/lib/data/MerchData.ts index 908a970..dc018e9 100644 --- a/src/lib/data/MerchData.ts +++ b/src/lib/data/MerchData.ts @@ -4,6 +4,7 @@ type MerchItem = { link: string; price: string; title: string; + priceId: string; }; export const MerchItemsData: MerchItem[] = [ @@ -12,6 +13,7 @@ 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", }, { @@ -19,6 +21,7 @@ export const MerchItemsData: MerchItem[] = [ 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", }, ]; diff --git a/src/pages/api/checkout.ts b/src/pages/api/checkout.ts new file mode 100644 index 0000000..a2dba5e --- /dev/null +++ b/src/pages/api/checkout.ts @@ -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" }); + } +} diff --git a/src/pages/api/getProducts.ts b/src/pages/api/getProducts.ts new file mode 100644 index 0000000..413ef19 --- /dev/null +++ b/src/pages/api/getProducts.ts @@ -0,0 +1,69 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import Stripe from "stripe"; + +if (!process.env.STRIPE_SECRET_KEY) { + throw new Error("STRIPE_SECRET_KEY is not set in environment variables."); +} + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { + apiVersion: "2025-02-24.acacia", +}); +export default async function handler( + req: NextApiRequest, + res: NextApiResponse, +) { + if (req.method !== "GET") { + return res + .setHeader("Cache-Control", "no-store") + .status(405) + .json({ error: "Method Not Allowed" }); + } + + try { + res.setHeader("Cache-Control", "no-store, max-age=0, must-revalidate"); + + // Fetch prices from Stripe + const prices = await stripe.prices.search({ + query: 'active:"true"', + }); + + // Fetch product details for each price entry + const productDetails = await Promise.all( + prices.data.map(async (price) => { + try { + const product = await stripe.products.retrieve( + price.product as string, + ); + + return { + active: product.active ?? price.active, // Ensure active status is retrieved + clothingImg: product.images?.[0] || "/default-image.png", + currency: price.currency, + id: price.id, + price: (price.unit_amount ?? 0) / 100, // Convert cents to dollars + priceId: price.id, + productId: product.id, + title: product.name || "Unknown Product", + }; + } catch (err) { + console.error(`Error fetching product ${price.product}:`, err); + return { + active: price.active, // Ensure active status is still included + clothingImg: "/default-image.png", + currency: price.currency, + id: price.id, + price: (price.unit_amount ?? 0) / 100, // Fix incorrect unit amount + priceId: price.id, + productId: price.product, + title: "Error Fetching Product", + }; + } + }), + ); + + return res.status(200).json({ prices: productDetails }); + } catch (error) { + console.error("Stripe API error:", error); + return res.status(500).json({ error: "Internal Server Error" }); + } +} diff --git a/src/pages/api/test.ts b/src/pages/api/test.ts new file mode 100644 index 0000000..0fb6b7d --- /dev/null +++ b/src/pages/api/test.ts @@ -0,0 +1,8 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + res.status(200).json({ + stripeKey: process.env.STRIPE_SECRET_KEY || "Not found", + webhookKey: process.env.STRIPE_WEBHOOK_SECRET || "Not found", + }); +} diff --git a/src/pages/success/index.tsx b/src/pages/success/index.tsx new file mode 100644 index 0000000..5da21a5 --- /dev/null +++ b/src/pages/success/index.tsx @@ -0,0 +1,62 @@ +import { motion } from "framer-motion"; +import { FiCheckCircle } from "react-icons/fi"; +import Link from "next/link"; + +// Common transition properties for all animations +const commonTransition = { + duration: 1, + ease: "easeOut", +}; + +const SuccessPage = () => { + return ( +
+ {/* Animated Icon Intro */} + + + + + {/* Success Message */} + + Payment Successful! + + + {/* Personalized Thank-You Message */} + + Thank you for supporting TechStart UCalgary. Your transaction has been + completed. + + + {/* Return to Merch Button */} + + + + + +
+ ); +}; + +export default SuccessPage; diff --git a/yarn.lock b/yarn.lock index fe18c5e..ff76088 100644 --- a/yarn.lock +++ b/yarn.lock @@ -832,6 +832,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:>=8.1.0": + version: 22.13.4 + resolution: "@types/node@npm:22.13.4" + dependencies: + undici-types: "npm:~6.20.0" + checksum: 10c0/3a234fa7766a3efc382cf81f66f474c26cdab2f54f43f757634c81c0444eb2160c2dabbde9741e4983078a318a88515b65416b5f1ab5478548579d7b3ead1d95 + languageName: node + linkType: hard + "@types/node@npm:^20": version: 20.16.11 resolution: "@types/node@npm:20.16.11" @@ -1589,6 +1598,16 @@ __metadata: languageName: node linkType: hard +"call-bind-apply-helpers@npm:^1.0.1": + version: 1.0.2 + resolution: "call-bind-apply-helpers@npm:1.0.2" + dependencies: + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 + languageName: node + linkType: hard + "call-bind@npm:^1.0.2, call-bind@npm:^1.0.5, call-bind@npm:^1.0.6, call-bind@npm:^1.0.7": version: 1.0.7 resolution: "call-bind@npm:1.0.7" @@ -1602,6 +1621,16 @@ __metadata: languageName: node linkType: hard +"call-bound@npm:^1.0.2": + version: 1.0.3 + resolution: "call-bound@npm:1.0.3" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + get-intrinsic: "npm:^1.2.6" + checksum: 10c0/45257b8e7621067304b30dbd638e856cac913d31e8e00a80d6cf172911acd057846572d0b256b45e652d515db6601e2974a1b1a040e91b4fc36fb3dd86fa69cf + languageName: node + linkType: hard + "callsites@npm:^3.0.0": version: 3.1.0 resolution: "callsites@npm:3.1.0" @@ -2060,6 +2089,17 @@ __metadata: languageName: node linkType: hard +"dunder-proto@npm:^1.0.1": + version: 1.0.1 + resolution: "dunder-proto@npm:1.0.1" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + gopd: "npm:^1.2.0" + checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -2207,6 +2247,13 @@ __metadata: languageName: node linkType: hard +"es-define-property@npm:^1.0.1": + version: 1.0.1 + resolution: "es-define-property@npm:1.0.1" + checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c + languageName: node + linkType: hard + "es-errors@npm:^1.2.1, es-errors@npm:^1.3.0": version: 1.3.0 resolution: "es-errors@npm:1.3.0" @@ -2971,6 +3018,34 @@ __metadata: languageName: node linkType: hard +"get-intrinsic@npm:^1.2.5, get-intrinsic@npm:^1.2.6": + version: 1.2.7 + resolution: "get-intrinsic@npm:1.2.7" + dependencies: + call-bind-apply-helpers: "npm:^1.0.1" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.0.0" + function-bind: "npm:^1.1.2" + get-proto: "npm:^1.0.0" + gopd: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + math-intrinsics: "npm:^1.1.0" + checksum: 10c0/b475dec9f8bff6f7422f51ff4b7b8d0b68e6776ee83a753c1d627e3008c3442090992788038b37eff72e93e43dceed8c1acbdf2d6751672687ec22127933080d + languageName: node + linkType: hard + +"get-proto@npm:^1.0.0": + version: 1.0.1 + resolution: "get-proto@npm:1.0.1" + dependencies: + dunder-proto: "npm:^1.0.1" + es-object-atoms: "npm:^1.0.0" + checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c + languageName: node + linkType: hard + "get-stream@npm:^8.0.1": version: 8.0.1 resolution: "get-stream@npm:8.0.1" @@ -3110,6 +3185,13 @@ __metadata: languageName: node linkType: hard +"gopd@npm:^1.2.0": + version: 1.2.0 + resolution: "gopd@npm:1.2.0" + checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead + languageName: node + linkType: hard + "graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -3168,6 +3250,13 @@ __metadata: languageName: node linkType: hard +"has-symbols@npm:^1.1.0": + version: 1.1.0 + resolution: "has-symbols@npm:1.1.0" + checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e + languageName: node + linkType: hard + "has-tostringtag@npm:^1.0.0, has-tostringtag@npm:^1.0.2": version: 1.0.2 resolution: "has-tostringtag@npm:1.0.2" @@ -3954,6 +4043,13 @@ __metadata: languageName: node linkType: hard +"math-intrinsics@npm:^1.1.0": + version: 1.1.0 + resolution: "math-intrinsics@npm:1.1.0" + checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -4314,6 +4410,13 @@ __metadata: languageName: node linkType: hard +"object-inspect@npm:^1.13.3": + version: 1.13.4 + resolution: "object-inspect@npm:1.13.4" + checksum: 10c0/d7f8711e803b96ea3191c745d6f8056ce1f2496e530e6a19a0e92d89b0fa3c76d910c31f0aa270432db6bd3b2f85500a376a83aaba849a8d518c8845b3211692 + languageName: node + linkType: hard + "object-is@npm:^1.1.5": version: 1.1.6 resolution: "object-is@npm:1.1.6" @@ -4808,6 +4911,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.11.0": + version: 6.14.0 + resolution: "qs@npm:6.14.0" + dependencies: + side-channel: "npm:^1.1.0" + checksum: 10c0/8ea5d91bf34f440598ee389d4a7d95820e3b837d3fd9f433871f7924801becaa0cd3b3b4628d49a7784d06a8aea9bc4554d2b6d8d584e2d221dc06238a42909c + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -4838,6 +4950,15 @@ __metadata: languageName: node linkType: hard +"react-icons@npm:^5.5.0": + version: 5.5.0 + resolution: "react-icons@npm:5.5.0" + peerDependencies: + react: "*" + checksum: 10c0/a24309bfc993c19cbcbfc928157e53a137851822779977b9588f6dd41ffc4d11ebc98b447f4039b0d309a858f0a42980f6bfb4477fb19f9f2d1bc2e190fcf79c + languageName: node + linkType: hard + "react-intersection-observer@npm:^9.13.1": version: 9.13.1 resolution: "react-intersection-observer@npm:9.13.1" @@ -5149,12 +5270,14 @@ __metadata: react: "npm:^18" react-countup: "npm:^6.5.3" react-dom: "npm:^18" + react-icons: "npm:^5.5.0" react-intersection-observer: "npm:^9.13.1" react-lazy-load-image-component: "npm:^1.6.2" react-multi-carousel: "npm:^2.8.5" react-scroll: "npm:^1.9.0" react-tsparticles: "npm:^2.9.3" react-visibility-sensor: "npm:^5.1.1" + stripe: "npm:^17.7.0" styled-components: "npm:^6.1.13" tailwindcss: "npm:^3.4.4" tsparticles: "npm:^2.9.3" @@ -5277,6 +5400,41 @@ __metadata: languageName: node linkType: hard +"side-channel-list@npm:^1.0.0": + version: 1.0.0 + resolution: "side-channel-list@npm:1.0.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + checksum: 10c0/644f4ac893456c9490ff388bf78aea9d333d5e5bfc64cfb84be8f04bf31ddc111a8d4b83b85d7e7e8a7b845bc185a9ad02c052d20e086983cf59f0be517d9b3d + languageName: node + linkType: hard + +"side-channel-map@npm:^1.0.1": + version: 1.0.1 + resolution: "side-channel-map@npm:1.0.1" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + checksum: 10c0/010584e6444dd8a20b85bc926d934424bd809e1a3af941cace229f7fdcb751aada0fb7164f60c2e22292b7fa3c0ff0bce237081fd4cdbc80de1dc68e95430672 + languageName: node + linkType: hard + +"side-channel-weakmap@npm:^1.0.2": + version: 1.0.2 + resolution: "side-channel-weakmap@npm:1.0.2" + dependencies: + call-bound: "npm:^1.0.2" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.5" + object-inspect: "npm:^1.13.3" + side-channel-map: "npm:^1.0.1" + checksum: 10c0/71362709ac233e08807ccd980101c3e2d7efe849edc51455030327b059f6c4d292c237f94dc0685031dd11c07dd17a68afde235d6cf2102d949567f98ab58185 + languageName: node + linkType: hard + "side-channel@npm:^1.0.4, side-channel@npm:^1.0.6": version: 1.0.6 resolution: "side-channel@npm:1.0.6" @@ -5289,6 +5447,19 @@ __metadata: languageName: node linkType: hard +"side-channel@npm:^1.1.0": + version: 1.1.0 + resolution: "side-channel@npm:1.1.0" + dependencies: + es-errors: "npm:^1.3.0" + object-inspect: "npm:^1.13.3" + side-channel-list: "npm:^1.0.0" + side-channel-map: "npm:^1.0.1" + side-channel-weakmap: "npm:^1.0.2" + checksum: 10c0/cb20dad41eb032e6c24c0982e1e5a24963a28aa6122b4f05b3f3d6bf8ae7fd5474ef382c8f54a6a3ab86e0cac4d41a23bd64ede3970e5bfb50326ba02a7996e6 + languageName: node + linkType: hard + "signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" @@ -5550,6 +5721,16 @@ __metadata: languageName: node linkType: hard +"stripe@npm:^17.7.0": + version: 17.7.0 + resolution: "stripe@npm:17.7.0" + dependencies: + "@types/node": "npm:>=8.1.0" + qs: "npm:^6.11.0" + checksum: 10c0/df67c6d455bd0dd87140640924c220fa9581fc00c3267d171f407c8d088f946f61e3ae7e88a89e7dd705b10fd5254630fc943222eb6f003390ebafbd391f81b2 + languageName: node + linkType: hard + "styled-components@npm:^6.1.13": version: 6.1.13 resolution: "styled-components@npm:6.1.13" @@ -6329,6 +6510,13 @@ __metadata: languageName: node linkType: hard +"undici-types@npm:~6.20.0": + version: 6.20.0 + resolution: "undici-types@npm:6.20.0" + checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0"