mirror of
https://github.com/vercel/commerce.git
synced 2025-06-15 11:51:21 +00:00
Merge dd0962ac28ec1eee14337e5a939f8c8aee8f2998 into fa1306916c652ea5f820d5b400087bece13460fd
This commit is contained in:
commit
731026b02f
@ -1,5 +1,6 @@
|
|||||||
COMPANY_NAME="Vercel Inc."
|
COMPANY_NAME="Vercel Inc."
|
||||||
SITE_NAME="Next.js Commerce"
|
COMMERCE_PROVIDER=crystallize
|
||||||
SHOPIFY_REVALIDATION_SECRET=""
|
NEXT_PUBLIC_COMMERCE_PROVIDER=crystallize
|
||||||
SHOPIFY_STOREFRONT_ACCESS_TOKEN=""
|
CRYSTALLIZE_API_URL=https://api.crystallize.com/6422a2c186ef95b31e1cd1e5/graphql
|
||||||
SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"
|
CRYSTALLIZE_ACCESS_TOKEN=c3ab0fef20aadcbeb9919e6701f015cfb4ddf1ff
|
||||||
|
NEXT_PUBLIC_USE_DUMMY_DATA=true
|
||||||
|
9
.env.production
Normal file
9
.env.production
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
COMMERCE_PROVIDER=shopify_local
|
||||||
|
NEXT_PUBLIC_COMMERCE_PROVIDER=shopify_local
|
||||||
|
|
||||||
|
SHOPIFY_STORE_DOMAIN=dummy.myshopify.com
|
||||||
|
SHOPIFY_STOREFRONT_API_TOKEN=dummy
|
||||||
|
SHOPIFY_STOREFRONT_API_VERSION=2023-01
|
||||||
|
|
||||||
|
SHOPIFY_HEADER_MENU=next-js-frontend-header-menu
|
||||||
|
SHOPIFY_FOOTER_MENU=next-js-frontend-footer-menu
|
8
.graphqlrc.yml
Normal file
8
.graphqlrc.yml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
schema:
|
||||||
|
- https://api.crystallize.com/bykirken/graphql
|
||||||
|
extensions:
|
||||||
|
endpoints:
|
||||||
|
default:
|
||||||
|
url: https://api.crystallize.com/bykirken/graphql
|
||||||
|
headers:
|
||||||
|
Authorization: Basic c3ab0fef20aadcbeb9919e6701f015cfb4ddf1ff
|
9
app/_not-found/page.tsx
Normal file
9
app/_not-found/page.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// app/_not-found/page.tsx
|
||||||
|
export default function StubNotFound() {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '4rem', textAlign: 'center' }}>
|
||||||
|
<h1 style={{ fontSize: '3rem', marginBottom: '1rem' }}>404</h1>
|
||||||
|
<p style={{ fontSize: '1.25rem' }}>Page not found.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
151
app/cart-checkout/page.tsx
Normal file
151
app/cart-checkout/page.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// app/cart-checkout/page.tsx
|
||||||
|
'use client'; // For useState if we were to make checkbox interactive
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function CartCheckoutPage() {
|
||||||
|
const [billingSameAsShipping, setBillingSameAsShipping] = useState(true);
|
||||||
|
|
||||||
|
// Dummy cart items
|
||||||
|
const cartItems = [
|
||||||
|
{ id: 'p1', name: 'Awesome T-Shirt (Red, L)', quantity: 1, price: 29.99 },
|
||||||
|
{ id: 'p2', name: 'Cool Cap - Black', quantity: 2, price: 15.00 },
|
||||||
|
{ id: 'p3', name: 'Generic Gadget XL', quantity: 1, price: 199.50 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const cartSubtotal = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||||
|
const shippingEstimate = cartItems.length > 0 ? 5.00 : 0; // No shipping if cart is empty
|
||||||
|
const grandTotal = cartSubtotal + shippingEstimate;
|
||||||
|
|
||||||
|
// Inline styles
|
||||||
|
const pageStyle = { padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '1000px', margin: '20px auto' };
|
||||||
|
const sectionStyle = { marginBottom: '40px', paddingBottom: '20px', borderBottom: '1px solid #eee' };
|
||||||
|
const headingStyle = { color: '#333', marginBottom: '20px', borderBottom: '1px solid #ddd', paddingBottom: '10px' };
|
||||||
|
const subHeadingStyle = { color: '#444', marginBottom: '15px' };
|
||||||
|
const inputStyle = { width: 'calc(100% - 22px)', padding: '10px', marginBottom: '10px', border: '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' as const };
|
||||||
|
const buttonStyle = { padding: '12px 20px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '1em' };
|
||||||
|
const smallButtonStyle = { padding: '5px 8px', margin: '0 5px', cursor: 'pointer' };
|
||||||
|
const cartItemStyle = { borderBottom: '1px solid #eee', padding: '15px 0', display: 'flex', justifyContent: 'space-between', alignItems: 'center' };
|
||||||
|
const formGroupStyle = { marginBottom: '15px' };
|
||||||
|
const labelStyle = { display: 'block', marginBottom: '5px', fontWeight: 'bold' as const };
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={pageStyle}>
|
||||||
|
<h1 style={{ textAlign: 'center', color: '#222', marginBottom: '40px' }}>Shopping Cart & Checkout</h1>
|
||||||
|
|
||||||
|
{/* Cart Items Section */}
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h2 style={subHeadingStyle}>Your Cart</h2>
|
||||||
|
{cartItems.length > 0 ? (
|
||||||
|
<>
|
||||||
|
{cartItems.map(item => (
|
||||||
|
<div key={item.id} style={cartItemStyle}>
|
||||||
|
<div style={{ flexGrow: 1 }}>
|
||||||
|
<p style={{ fontWeight: 'bold', margin: '0 0 5px 0' }}>{item.name}</p>
|
||||||
|
<p style={{ margin: '0 0 5px 0', fontSize: '0.9em' }}>Price: ${item.price.toFixed(2)}</p>
|
||||||
|
<p style={{ margin: '0', fontSize: '0.9em' }}>
|
||||||
|
Quantity:
|
||||||
|
<button style={smallButtonStyle} disabled>-</button> {item.quantity} <button style={smallButtonStyle} disabled>+</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div style={{ textAlign: 'right' as const }}>
|
||||||
|
<p style={{ fontWeight: 'bold', margin: '0 0 10px 0' }}>Total: ${(item.price * item.quantity).toFixed(2)}</p>
|
||||||
|
<button style={{ ...smallButtonStyle, backgroundColor: '#dc3545', color: 'white', border: 'none', borderRadius: '3px' }} disabled>Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ marginTop: '20px', textAlign: 'right' as const }}>
|
||||||
|
<p><strong>Subtotal:</strong> ${cartSubtotal.toFixed(2)}</p>
|
||||||
|
<p><strong>Shipping Estimate:</strong> ${shippingEstimate.toFixed(2)}</p>
|
||||||
|
<h3 style={{ marginTop: '10px' }}>Grand Total: ${grandTotal.toFixed(2)}</h3>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>Your cart is currently empty.</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Checkout Form Section */}
|
||||||
|
{cartItems.length > 0 && ( // Only show checkout if cart is not empty
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h2 style={subHeadingStyle}>Checkout</h2>
|
||||||
|
<form onSubmit={(e) => e.preventDefault()} > {/* Prevent actual submission */}
|
||||||
|
|
||||||
|
<h3 style={{ ...subHeadingStyle, fontSize: '1.1em', marginTop: '0' }}>Shipping Address</h3>
|
||||||
|
<div style={formGroupStyle}>
|
||||||
|
<label htmlFor="fullName" style={labelStyle}>Full Name</label>
|
||||||
|
<input type="text" id="fullName" name="fullName" style={inputStyle} required />
|
||||||
|
</div>
|
||||||
|
<div style={formGroupStyle}>
|
||||||
|
<label htmlFor="address1" style={labelStyle}>Address Line 1</label>
|
||||||
|
<input type="text" id="address1" name="address1" style={inputStyle} required />
|
||||||
|
</div>
|
||||||
|
<div style={formGroupStyle}>
|
||||||
|
<label htmlFor="city" style={labelStyle}>City</label>
|
||||||
|
<input type="text" id="city" name="city" style={inputStyle} required />
|
||||||
|
</div>
|
||||||
|
<div style={formGroupStyle}>
|
||||||
|
<label htmlFor="postalCode" style={labelStyle}>Postal Code</label>
|
||||||
|
<input type="text" id="postalCode" name="postalCode" style={inputStyle} required />
|
||||||
|
</div>
|
||||||
|
<div style={formGroupStyle}>
|
||||||
|
<label htmlFor="country" style={labelStyle}>Country</label>
|
||||||
|
<input type="text" id="country" name="country" style={inputStyle} required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style={{ ...subHeadingStyle, fontSize: '1.1em', marginTop: '30px' }}>Billing Address</h3>
|
||||||
|
<div style={{ ...formGroupStyle, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="billingSame"
|
||||||
|
name="billingSame"
|
||||||
|
checked={billingSameAsShipping}
|
||||||
|
onChange={(e) => setBillingSameAsShipping(e.target.checked)}
|
||||||
|
style={{ marginRight: '10px' }}
|
||||||
|
/>
|
||||||
|
<label htmlFor="billingSame" style={{ ...labelStyle, marginBottom: '0' }}>Same as shipping address</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!billingSameAsShipping && (
|
||||||
|
<>
|
||||||
|
{/* Billing address fields would go here, similar to shipping */}
|
||||||
|
<p style={{ fontStyle: 'italic', color: '#666' }}>(Billing address fields would appear here if different)</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h3 style={{ ...subHeadingStyle, fontSize: '1.1em', marginTop: '30px' }}>Payment Information</h3>
|
||||||
|
<div style={{ border: '1px dashed #ccc', padding: '15px', borderRadius: '4px', backgroundColor: '#f9f9f9' }}>
|
||||||
|
<p style={{ margin: '0 0 10px 0', fontWeight: 'bold' }}>Card Number:</p>
|
||||||
|
<p style={{ color: '#777', fontStyle: 'italic' }}> (Placeholder: Actual card input fields are not implemented for security reasons)</p>
|
||||||
|
<p style={{ margin: '10px 0 10px 0', fontWeight: 'bold' }}>Expiry Date (MM/YY):</p>
|
||||||
|
<p style={{ margin: '10px 0 0 0', fontWeight: 'bold' }}>CVV:</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" style={{ ...buttonStyle, marginTop: '30px', width: '100%', backgroundColor: '#28a745' }}
|
||||||
|
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = '#218838')}
|
||||||
|
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = '#28a745')}
|
||||||
|
disabled={cartItems.length === 0}
|
||||||
|
>
|
||||||
|
Place Order
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Request a Quote Section */}
|
||||||
|
<section style={{ ...sectionStyle, borderBottom: 'none', textAlign: 'center' as const }}>
|
||||||
|
<h2 style={subHeadingStyle}>Need a Custom Quote?</h2>
|
||||||
|
<p style={{ marginBottom: '15px' }}>For bulk orders or special requirements, please request a quote.</p>
|
||||||
|
<button
|
||||||
|
style={{ ...buttonStyle, backgroundColor: '#17a2b8' }}
|
||||||
|
onClick={() => alert('Redirecting to quote request page... (placeholder)')} // Placeholder action
|
||||||
|
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = '#138496')}
|
||||||
|
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = '#17a2b8')}
|
||||||
|
>
|
||||||
|
Request a Quote
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
65
app/content/[slug]/page.tsx
Normal file
65
app/content/[slug]/page.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
// app/content/[slug]/page.tsx
|
||||||
|
|
||||||
|
// Simulate fetching content (replace with actual CMS fetching later)
|
||||||
|
async function getContent(slug: string) {
|
||||||
|
// In a real app, you'd fetch this from a CMS
|
||||||
|
const allContent: { [key: string]: { title: string; body: string[] } } = {
|
||||||
|
'about-us': {
|
||||||
|
title: 'About Us',
|
||||||
|
body: [
|
||||||
|
'This is the about us page.',
|
||||||
|
'We are a company that does things.'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'contact-us': {
|
||||||
|
title: 'Contact Us',
|
||||||
|
body: [
|
||||||
|
'You can contact us via email or phone.',
|
||||||
|
'Email: contact@example.com',
|
||||||
|
'Phone: 123-456-7890'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'privacy-policy': {
|
||||||
|
title: 'Privacy Policy',
|
||||||
|
body: [
|
||||||
|
'This is our privacy policy.',
|
||||||
|
'We respect your privacy and are committed to protecting your personal data.'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Ensure slug is a string before using it as an index
|
||||||
|
return allContent[String(slug)] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define an interface for the page's props, with params and searchParams as Promises
|
||||||
|
interface ContentPageProps {
|
||||||
|
params: Promise<{ slug: string }>;
|
||||||
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ContentPage({ params, searchParams }: ContentPageProps) {
|
||||||
|
// Await the params promise to get its value
|
||||||
|
const resolvedParams = await params;
|
||||||
|
// Await searchParams if you need to use them, e.g.:
|
||||||
|
// const resolvedSearchParams = await searchParams;
|
||||||
|
|
||||||
|
const content = await getContent(resolvedParams.slug);
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return <div>Content not found for {resolvedParams.slug}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px' }}>
|
||||||
|
<h1>{content.title}</h1>
|
||||||
|
{content.body.map((paragraph, index) => (
|
||||||
|
<p key={index}>{paragraph}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Generate static params still works the same way
|
||||||
|
// export async function generateStaticParams() {
|
||||||
|
// return [{ slug: 'about-us' }, { slug: 'contact-us' }, { slug: 'privacy-policy' }];
|
||||||
|
// }
|
58
app/login/page.tsx
Normal file
58
app/login/page.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// app/login/page.tsx
|
||||||
|
export default function LoginPage() {
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: '400px', margin: '50px auto', padding: '40px', border: '1px solid #ddd', borderRadius: '8px', boxShadow: '0 4px 8px rgba(0,0,0,0.1)' }}>
|
||||||
|
<h1 style={{ textAlign: 'center', marginBottom: '30px', color: '#333' }}>Sign In</h1>
|
||||||
|
<form onSubmit={(e) => e.preventDefault()} > {/* Prevent actual submission for this mock */}
|
||||||
|
<div style={{ marginBottom: '15px' }}>
|
||||||
|
<label htmlFor="email" style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#555' }}>Email Address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
placeholder="you@example.com"
|
||||||
|
style={{ width: '100%', padding: '10px', border: '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<label htmlFor="password" style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#555' }}>Password</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
placeholder="Your password"
|
||||||
|
style={{ width: '100%', padding: '10px', border: '1px solid #ccc', borderRadius: '4px', boxSizing: 'border-box' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '12px',
|
||||||
|
backgroundColor: '#007bff',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '16px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background-color 0.2s'
|
||||||
|
}}
|
||||||
|
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = '#0056b3')}
|
||||||
|
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = '#007bff')}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div style={{ marginTop: '25px', textAlign: 'center', fontSize: '14px' }}>
|
||||||
|
<p style={{ marginBottom: '10px' }}>
|
||||||
|
New customer? <a href="/signup" style={{ color: '#007bff', textDecoration: 'none' }}>Create an account</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="/reset-password" style={{ color: '#007bff', textDecoration: 'none' }}>Forgot your password?</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
139
app/my-page/page.tsx
Normal file
139
app/my-page/page.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// app/my-page/page.tsx
|
||||||
|
|
||||||
|
export default function MyPage() {
|
||||||
|
// Dummy data
|
||||||
|
const orders = [
|
||||||
|
{ id: '12345', date: '2023-10-26', total: '$150.00', status: 'Shipped' },
|
||||||
|
{ id: '67890', date: '2023-11-05', total: '$75.50', status: 'Processing' },
|
||||||
|
{ id: '10112', date: '2023-11-15', total: '$220.00', status: 'Delivered' },
|
||||||
|
];
|
||||||
|
const quotes = [
|
||||||
|
{ id: 'Q1001', date: '2023-10-20', total: '$500.00', status: 'Accepted' },
|
||||||
|
{ id: 'Q1002', date: '2023-11-01', total: '$1250.75', status: 'Pending' },
|
||||||
|
];
|
||||||
|
const downloads = [
|
||||||
|
{ name: 'Product Manual X123.pdf', url: '#' },
|
||||||
|
{ name: 'Software License - MyProduct v2.txt', url: '#' },
|
||||||
|
{ name: 'Invoice_INV2023-10-26.pdf', url: '#' },
|
||||||
|
];
|
||||||
|
const userProfile = {
|
||||||
|
name: 'Jane Doe',
|
||||||
|
email: 'jane.doe@example.com',
|
||||||
|
company: 'Innovate Solutions Ltd.',
|
||||||
|
memberSince: '2022-01-15',
|
||||||
|
};
|
||||||
|
|
||||||
|
const sectionStyle = {
|
||||||
|
marginBottom: '40px',
|
||||||
|
paddingBottom: '20px',
|
||||||
|
borderBottom: '1px solid #eee'
|
||||||
|
};
|
||||||
|
|
||||||
|
const headingStyle = {
|
||||||
|
color: '#333',
|
||||||
|
marginBottom: '15px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableCellStyle = {
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
padding: '10px',
|
||||||
|
textAlign: 'left' as const // Explicitly type textAlign
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonStyle = {
|
||||||
|
padding: '10px 15px',
|
||||||
|
backgroundColor: '#007bff',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '1em'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '900px', margin: '20px auto' }}>
|
||||||
|
<h1 style={{ textAlign: 'center', color: '#222', marginBottom: '40px' }}>My Account</h1>
|
||||||
|
|
||||||
|
{/* Order History Section */}
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h2 style={headingStyle}>Order History</h2>
|
||||||
|
{orders.length > 0 ? (
|
||||||
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={tableCellStyle}>Order ID</th>
|
||||||
|
<th style={tableCellStyle}>Date</th>
|
||||||
|
<th style={tableCellStyle}>Total</th>
|
||||||
|
<th style={tableCellStyle}>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orders.map(order => (
|
||||||
|
<tr key={order.id}>
|
||||||
|
<td style={tableCellStyle}>#{order.id}</td>
|
||||||
|
<td style={tableCellStyle}>{order.date}</td>
|
||||||
|
<td style={tableCellStyle}>{order.total}</td>
|
||||||
|
<td style={tableCellStyle}>{order.status}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
) : (
|
||||||
|
<p>You have no past orders.</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* My Quotes Section */}
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h2 style={headingStyle}>My Quotes</h2>
|
||||||
|
{quotes.length > 0 ? (
|
||||||
|
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||||
|
{quotes.map(quote => (
|
||||||
|
<li key={quote.id} style={{ marginBottom: '10px', padding: '10px', border: '1px solid #eee', borderRadius: '4px' }}>
|
||||||
|
Quote #{quote.id} - Date: {quote.date} - Total: {quote.total} - Status: <span style={{ fontWeight: 'bold' }}>{quote.status}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>You have no active quotes.</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* My Downloads Section */}
|
||||||
|
<section style={sectionStyle}>
|
||||||
|
<h2 style={headingStyle}>My Downloads</h2>
|
||||||
|
{downloads.length > 0 ? (
|
||||||
|
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||||
|
{downloads.map((download, index) => ( // Added index for key as names might not be unique
|
||||||
|
<li key={`${download.name}-${index}`} style={{ marginBottom: '8px' }}>
|
||||||
|
<a href={download.url} download style={{ color: '#007bff', textDecoration: 'none' }}>
|
||||||
|
{download.name}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>No downloads available.</p>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Profile Information Section */}
|
||||||
|
<section style={{ ...sectionStyle, borderBottom: 'none' }}>
|
||||||
|
<h2 style={headingStyle}>My Profile</h2>
|
||||||
|
<div style={{ lineHeight: '1.8' }}>
|
||||||
|
<p><strong>Name:</strong> {userProfile.name}</p>
|
||||||
|
<p><strong>Email:</strong> {userProfile.email}</p>
|
||||||
|
<p><strong>Company:</strong> {userProfile.company}</p>
|
||||||
|
<p><strong>Member Since:</strong> {userProfile.memberSince}</p>
|
||||||
|
</div>
|
||||||
|
<button style={{ ...buttonStyle, marginTop: '15px' }}
|
||||||
|
onMouseOver={(e) => (e.currentTarget.style.backgroundColor = '#0056b3')}
|
||||||
|
onMouseOut={(e) => (e.currentTarget.style.backgroundColor = '#007bff')}
|
||||||
|
>
|
||||||
|
Edit Profile
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
8
app/not-found.tsx
Normal file
8
app/not-found.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 40, textAlign: 'center' }}>
|
||||||
|
<h1 style={{ fontSize: 48, marginBottom: 16 }}>404</h1>
|
||||||
|
<p style={{ fontSize: 18 }}>This page could not be found.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
49
app/page.tsx
49
app/page.tsx
@ -10,11 +10,60 @@ export const metadata = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Simulate fetching page configuration from a CMS
|
||||||
|
const pageConfig = {
|
||||||
|
showFeaturedProducts: true,
|
||||||
|
showPromotions: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Placeholder Product Item Component
|
||||||
|
function ProductItem({ name, price, imageUrl }: { name: string; price: string; imageUrl: string }) {
|
||||||
|
return (
|
||||||
|
<div style={{ border: '1px solid #eee', padding: '16px', textAlign: 'center' }}>
|
||||||
|
<img src={imageUrl} alt={name} style={{ maxWidth: '100%', height: 'auto', marginBottom: '8px' }} />
|
||||||
|
<h3>{name}</h3>
|
||||||
|
<p>{price}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder Promotion Banner Component
|
||||||
|
function PromotionBanner({ title, imageUrl }: { title: string; imageUrl: string }) {
|
||||||
|
return (
|
||||||
|
<div style={{ border: '1px solid #eee', padding: '16px', textAlign: 'center', margin: '16px 0' }}>
|
||||||
|
<img src={imageUrl} alt={title} style={{ maxWidth: '100%', height: 'auto', marginBottom: '8px' }} />
|
||||||
|
<h2>{title}</h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Existing components can remain if they are part of the desired layout */}
|
||||||
<ThreeItemGrid />
|
<ThreeItemGrid />
|
||||||
<Carousel />
|
<Carousel />
|
||||||
|
|
||||||
|
{pageConfig.showFeaturedProducts && (
|
||||||
|
<section style={{ padding: '20px' }}>
|
||||||
|
<h2>Featured Products</h2>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '16px', marginTop: '16px' }}>
|
||||||
|
<ProductItem name="Awesome T-Shirt" price="$29.99" imageUrl="/placeholder-product1.jpg" />
|
||||||
|
<ProductItem name="Cool Gadget" price="$99.50" imageUrl="/placeholder-product2.jpg" />
|
||||||
|
<ProductItem name="Stylish Hat" price="$19.75" imageUrl="/placeholder-product3.jpg" />
|
||||||
|
<ProductItem name="Generic Item" price="$10.00" imageUrl="/placeholder-product4.jpg" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pageConfig.showPromotions && (
|
||||||
|
<section style={{ padding: '20px' }}>
|
||||||
|
<h2>Promotions</h2>
|
||||||
|
<PromotionBanner title="Summer Sale!" imageUrl="/placeholder-promo1.jpg" />
|
||||||
|
<PromotionBanner title="New Arrivals" imageUrl="/placeholder-promo2.jpg" />
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,149 +1,77 @@
|
|||||||
import type { Metadata } from 'next';
|
// app/product/[handle]/page.tsx
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
|
|
||||||
import { GridTileImage } from 'components/grid/tile';
|
// Simulate fetching product data
|
||||||
import Footer from 'components/layout/footer';
|
async function getProduct(handle: string) {
|
||||||
import { Gallery } from 'components/product/gallery';
|
const allProducts: { [key: string]: any } = { // Use 'any' for simplicity in this mock
|
||||||
import { ProductProvider } from 'components/product/product-context';
|
'sample-product-1': { id: 'prod-1', name: 'Awesome T-Shirt', description: 'This is the best t-shirt ever. Made from 100% organic cotton.', price: { amount: '29.99', currencyCode: 'USD' }, images: [ { src: '/placeholder-tshirt-blue.jpg', alt: 'Awesome T-Shirt - Blue' }, { src: '/placeholder-tshirt-red.jpg', alt: 'Awesome T-Shirt - Red' } ], variants: [ { id: 'v1-color', name: 'Color', value: 'Blue' }, { id: 'v1-size', name: 'Size', value: 'L' }, { id: 'v1-material', name: 'Material', value: 'Cotton' } ] },
|
||||||
import { ProductDescription } from 'components/product/product-description';
|
'sample-product-2': { id: 'prod-2', name: 'Cool Gadget Pro', description: 'The latest and greatest gadget with amazing features.', price: { amount: '199.50', currencyCode: 'USD' }, images: [ { src: '/placeholder-gadget-main.jpg', alt: 'Cool Gadget Pro' }, { src: '/placeholder-gadget-angle.jpg', alt: 'Cool Gadget Pro - Angle View' } ], variants: [ { id: 'v2-color', name: 'Color', value: 'Black' }, { id: 'v2-storage', name: 'Storage', value: '256GB' } ] },
|
||||||
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
|
'another-item': { id: 'prod-3', name: 'Simple Mug', description: 'A simple mug for your daily coffee or tea.', price: { amount: '12.00', currencyCode: 'USD' }, images: [ { src: '/placeholder-mug.jpg', alt: 'Simple Mug' } ], variants: [ { id: 'v3-color', name: 'Color', value: 'White' }, { id: 'v3-size', name: 'Size', value: 'Standard' } ] }
|
||||||
import { getProduct, getProductRecommendations } from 'lib/shopify';
|
};
|
||||||
import { Image } from 'lib/shopify/types';
|
await new Promise(resolve => setTimeout(resolve, 50)); // Simulate network delay
|
||||||
import Link from 'next/link';
|
return allProducts[String(handle)] || null; // Ensure handle is string
|
||||||
import { Suspense } from 'react';
|
}
|
||||||
|
|
||||||
export async function generateMetadata(props: {
|
interface ProductPageProps {
|
||||||
params: Promise<{ handle: string }>;
|
params: Promise<{ handle: string }>;
|
||||||
}): Promise<Metadata> {
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
const params = await props.params;
|
|
||||||
const product = await getProduct(params.handle);
|
|
||||||
|
|
||||||
if (!product) return notFound();
|
|
||||||
|
|
||||||
const { url, width, height, altText: alt } = product.featuredImage || {};
|
|
||||||
const indexable = !product.tags.includes(HIDDEN_PRODUCT_TAG);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: product.seo.title || product.title,
|
|
||||||
description: product.seo.description || product.description,
|
|
||||||
robots: {
|
|
||||||
index: indexable,
|
|
||||||
follow: indexable,
|
|
||||||
googleBot: {
|
|
||||||
index: indexable,
|
|
||||||
follow: indexable
|
|
||||||
}
|
|
||||||
},
|
|
||||||
openGraph: url
|
|
||||||
? {
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
alt
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ProductPage(props: { params: Promise<{ handle: string }> }) {
|
export default async function ProductPage({ params, searchParams }: ProductPageProps) {
|
||||||
const params = await props.params;
|
const resolvedParams = await params;
|
||||||
const product = await getProduct(params.handle);
|
// const resolvedSearchParams = await searchParams; // If needed
|
||||||
|
|
||||||
if (!product) return notFound();
|
const product = await getProduct(resolvedParams.handle);
|
||||||
|
|
||||||
const productJsonLd = {
|
if (!product) {
|
||||||
'@context': 'https://schema.org',
|
return <div style={{ padding: '20px', textAlign: 'center' }}>Product not found for handle: {resolvedParams.handle}</div>;
|
||||||
'@type': 'Product',
|
}
|
||||||
name: product.title,
|
|
||||||
description: product.description,
|
|
||||||
image: product.featuredImage.url,
|
|
||||||
offers: {
|
|
||||||
'@type': 'AggregateOffer',
|
|
||||||
availability: product.availableForSale
|
|
||||||
? 'https://schema.org/InStock'
|
|
||||||
: 'https://schema.org/OutOfStock',
|
|
||||||
priceCurrency: product.priceRange.minVariantPrice.currencyCode,
|
|
||||||
highPrice: product.priceRange.maxVariantPrice.amount,
|
|
||||||
lowPrice: product.priceRange.minVariantPrice.amount
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProductProvider>
|
// ... (rest of the JSX should be the same, just ensure 'params.handle' is replaced with 'resolvedParams.handle' if it was used in the notFound message)
|
||||||
<script
|
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '800px', margin: '0 auto' }}>
|
||||||
type="application/ld+json"
|
<h1>{product.name}</h1>
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: JSON.stringify(productJsonLd)
|
{product.images && product.images.length > 0 && (
|
||||||
|
<img
|
||||||
|
src={product.images[0].src}
|
||||||
|
alt={product.images[0].alt || product.name}
|
||||||
|
style={{ maxWidth: '100%', height: 'auto', maxHeight: '400px', marginBottom: '20px', border: '1px solid #ddd' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p style={{ fontSize: '1.1em', lineHeight: '1.6' }}>{product.description}</p>
|
||||||
|
|
||||||
|
<p style={{ fontSize: '1.2em', fontWeight: 'bold', margin: '20px 0' }}>
|
||||||
|
Price: {product.price.amount} {product.price.currencyCode}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Variants:</h2>
|
||||||
|
{product.variants && product.variants.length > 0 ? (
|
||||||
|
<ul style={{ listStyle: 'none', padding: 0 }}>
|
||||||
|
{product.variants.map((variant: any) => ( // Using any for mock simplicity
|
||||||
|
<li key={variant.id} style={{ marginBottom: '8px', borderBottom: '1px solid #eee', paddingBottom: '8px' }}>
|
||||||
|
<strong>{variant.name}:</strong> {variant.value}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p>No variants available for this product.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: '1em',
|
||||||
|
color: 'white',
|
||||||
|
backgroundColor: '#007bff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '5px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
marginTop: '20px'
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<div className="mx-auto max-w-(--breakpoint-2xl) px-4">
|
Add to Cart
|
||||||
<div className="flex flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 lg:flex-row lg:gap-8 dark:border-neutral-800 dark:bg-black">
|
</button>
|
||||||
<div className="h-full w-full basis-full lg:basis-4/6">
|
|
||||||
<Suspense
|
|
||||||
fallback={
|
|
||||||
<div className="relative aspect-square h-full max-h-[550px] w-full overflow-hidden" />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Gallery
|
|
||||||
images={product.images.slice(0, 5).map((image: Image) => ({
|
|
||||||
src: image.url,
|
|
||||||
altText: image.altText
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="basis-full lg:basis-2/6">
|
|
||||||
<Suspense fallback={null}>
|
|
||||||
<ProductDescription product={product} />
|
|
||||||
</Suspense>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<RelatedProducts id={product.id} />
|
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
</ProductProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function RelatedProducts({ id }: { id: string }) {
|
|
||||||
const relatedProducts = await getProductRecommendations(id);
|
|
||||||
|
|
||||||
if (!relatedProducts.length) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-8">
|
|
||||||
<h2 className="mb-4 text-2xl font-bold">Related Products</h2>
|
|
||||||
<ul className="flex w-full gap-4 overflow-x-auto pt-1">
|
|
||||||
{relatedProducts.map((product) => (
|
|
||||||
<li
|
|
||||||
key={product.handle}
|
|
||||||
className="aspect-square w-full flex-none min-[475px]:w-1/2 sm:w-1/3 md:w-1/4 lg:w-1/5"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
className="relative h-full w-full"
|
|
||||||
href={`/product/${product.handle}`}
|
|
||||||
prefetch={true}
|
|
||||||
>
|
|
||||||
<GridTileImage
|
|
||||||
alt={product.title}
|
|
||||||
label={{
|
|
||||||
title: product.title,
|
|
||||||
amount: product.priceRange.maxVariantPrice.amount,
|
|
||||||
currencyCode: product.priceRange.maxVariantPrice.currencyCode
|
|
||||||
}}
|
|
||||||
src={product.featuredImage?.url}
|
|
||||||
fill
|
|
||||||
sizes="(min-width: 1024px) 20vw, (min-width: 768px) 25vw, (min-width: 640px) 33vw, (min-width: 475px) 50vw, 100vw"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,117 @@
|
|||||||
import { getCollection, getCollectionProducts } from 'lib/shopify';
|
// app/search/[collection]/page.tsx
|
||||||
import { Metadata } from 'next';
|
|
||||||
import { notFound } from 'next/navigation';
|
|
||||||
|
|
||||||
import Grid from 'components/grid';
|
// Simulate fetching products for a collection
|
||||||
import ProductGridItems from 'components/layout/product-grid-items';
|
async function getProductsByCollection(collectionName: string) {
|
||||||
import { defaultSort, sorting } from 'lib/constants';
|
// Simulate network delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
|
||||||
export async function generateMetadata(props: {
|
const allProducts: { [key: string]: any[] } = {
|
||||||
params: Promise<{ collection: string }>;
|
't-shirts': [
|
||||||
}): Promise<Metadata> {
|
{ id: 'ts1', name: 'Cool T-Shirt', price: { amount: '19.99', currencyCode: 'USD' }, image: { src: '/placeholder-tshirt1.jpg', alt: 'T-Shirt 1' } },
|
||||||
const params = await props.params;
|
{ id: 'ts2', name: 'Graphic Tee', price: { amount: '24.99', currencyCode: 'USD' }, image: { src: '/placeholder-tshirt2.jpg', alt: 'T-Shirt 2' } },
|
||||||
const collection = await getCollection(params.collection);
|
{ id: 'ts3', name: 'Plain V-Neck', price: { amount: '15.50', currencyCode: 'USD' }, image: { src: '/placeholder-tshirt3.jpg', alt: 'Plain V-Neck' } },
|
||||||
|
],
|
||||||
if (!collection) return notFound();
|
'accessories': [
|
||||||
|
{ id: 'ac1', name: 'Stylish Cap', price: { amount: '15.00', currencyCode: 'USD' }, image: { src: '/placeholder-cap.jpg', alt: 'Cap' } },
|
||||||
return {
|
{ id: 'ac2', name: 'Leather Belt', price: { amount: '35.00', currencyCode: 'USD' }, image: { src: '/placeholder-belt.jpg', alt: 'Leather Belt' } },
|
||||||
title: collection.seo?.title || collection.title,
|
],
|
||||||
description:
|
'footwear': [
|
||||||
collection.seo?.description || collection.description || `${collection.title} products`
|
{ id: 'fw1', name: 'Running Shoes', price: { amount: '79.99', currencyCode: 'USD' }, image: { src: '/placeholder-shoes1.jpg', alt: 'Running Shoes' } },
|
||||||
|
{ id: 'fw2', name: 'Casual Sneakers', price: { amount: '65.00', currencyCode: 'USD' }, image: { src: '/placeholder-sneakers1.jpg', alt: 'Casual Sneakers' } },
|
||||||
|
{ id: 'fw3', name: 'Formal Shoes', price: { amount: '120.00', currencyCode: 'USD' }, image: { src: '/placeholder-shoes2.jpg', alt: 'Formal Shoes' } },
|
||||||
|
]
|
||||||
|
// Add more dummy collections and products as needed
|
||||||
};
|
};
|
||||||
|
// Ensure collectionName is string and lowercase for object key access
|
||||||
|
return allProducts[String(collectionName).toLowerCase()] || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CategoryPage(props: {
|
interface CollectionPageProps {
|
||||||
params: Promise<{ collection: string }>;
|
params: Promise<{ collection: string }>;
|
||||||
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
}) {
|
}
|
||||||
const searchParams = await props.searchParams;
|
|
||||||
const params = await props.params;
|
export default async function CollectionPage({ params, searchParams }: CollectionPageProps) {
|
||||||
const { sort } = searchParams as { [key: string]: string };
|
const resolvedParams = await params;
|
||||||
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
|
// const resolvedSearchParams = await searchParams; // Await if needed for filtering, etc.
|
||||||
const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });
|
|
||||||
|
const products = await getProductsByCollection(resolvedParams.collection);
|
||||||
|
const collectionName = resolvedParams.collection.charAt(0).toUpperCase() + resolvedParams.collection.slice(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
|
||||||
{products.length === 0 ? (
|
<h1 style={{ marginBottom: '20px', borderBottom: '2px solid #eee', paddingBottom: '10px' }}>
|
||||||
<p className="py-3 text-lg">{`No products found in this collection`}</p>
|
Products in: {collectionName}
|
||||||
) : (
|
</h1>
|
||||||
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
<ProductGridItems products={products} />
|
<div style={{ display: 'flex', gap: '20px', marginBottom: '30px' }}>
|
||||||
</Grid>
|
{/* Placeholder Filters */}
|
||||||
)}
|
<div style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '5px', width: '250px' }}>
|
||||||
</section>
|
<h3 style={{ marginTop: '0', marginBottom: '15px' }}>Filters</h3>
|
||||||
|
<div style={{ marginBottom: '10px' }}>
|
||||||
|
<label htmlFor="category-filter" style={{ display: 'block', marginBottom: '5px' }}>Category:</label>
|
||||||
|
<select id="category-filter" style={{ width: '100%', padding: '8px' }}>
|
||||||
|
<option value="">All {collectionName}</option>
|
||||||
|
<option value="cat1">Sub-Category 1</option>
|
||||||
|
<option value="cat2">Sub-Category 2</option>
|
||||||
|
<option value="cat3">Sub-Category 3</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="price-range-filter" style={{ display: 'block', marginBottom: '5px' }}>Price Range:</label>
|
||||||
|
<input type="range" id="price-range-filter" min="0" max="200" defaultValue="100" style={{ width: '100%' }} />
|
||||||
|
{/* Basic display of range value - not functional */}
|
||||||
|
<div style={{ textAlign: 'center', fontSize: '0.9em', marginTop: '5px' }}>$0 - $200</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content Area (Search + Product List) */}
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
{/* Placeholder Search Input */}
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
placeholder={`Search within ${collectionName}...`}
|
||||||
|
style={{ width: '100%', padding: '10px', boxSizing: 'border-box', fontSize: '1em' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product List */}
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '20px' }}>
|
||||||
|
{products.length > 0 ? (
|
||||||
|
products.map((product: any) => ( // Using any for mock simplicity
|
||||||
|
<div key={product.id} style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '5px', textAlign: 'center' }}>
|
||||||
|
<img
|
||||||
|
src={product.image.src}
|
||||||
|
alt={product.image.alt}
|
||||||
|
style={{ width: '100%', height: '180px', objectFit: 'cover', marginBottom: '10px', borderBottom: '1px solid #eee', paddingBottom: '10px' }}
|
||||||
|
/>
|
||||||
|
<h2 style={{ fontSize: '1.1em', margin: '0 0 10px 0' }}>{product.name}</h2>
|
||||||
|
<p style={{ fontSize: '1em', fontWeight: 'bold', margin: '0 0 10px 0' }}>
|
||||||
|
{product.price.amount} {product.price.currencyCode}
|
||||||
|
</p>
|
||||||
|
{/* Use a generic link for now; specific product pages are handled by [handle] route */}
|
||||||
|
<a
|
||||||
|
href={`/product/${product.id}`} // Assuming product.id can be a handle
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '8px 15px',
|
||||||
|
backgroundColor: '#007bff',
|
||||||
|
color: 'white',
|
||||||
|
textDecoration: 'none',
|
||||||
|
borderRadius: '3px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p style={{ gridColumn: '1 / -1' }}>No products found in this collection.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,135 @@
|
|||||||
import Grid from 'components/grid';
|
// app/search/page.tsx
|
||||||
import ProductGridItems from 'components/layout/product-grid-items';
|
'use client'; // Required for using client-side features like useState
|
||||||
import { defaultSort, sorting } from 'lib/constants';
|
|
||||||
import { getProducts } from 'lib/shopify';
|
|
||||||
|
|
||||||
export const metadata = {
|
import { useState, useEffect } from 'react';
|
||||||
title: 'Search',
|
import { useSearchParams } from 'next/navigation'; // To get query from URL
|
||||||
description: 'Search for products in the store.'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function SearchPage(props: {
|
// Simulate fetching search results
|
||||||
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
|
async function getSearchResults(query: string | null): Promise<any[]> { // Explicit Promise<any[]>
|
||||||
}) {
|
// Simulate network delay
|
||||||
const searchParams = await props.searchParams;
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
const { sort, q: searchValue } = searchParams as { [key: string]: string };
|
if (!query || !query.trim()) { // Also handle empty/whitespace query after trimming
|
||||||
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
|
return []; // Return empty array for null, empty, or whitespace-only query
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockResults: { [key: string]: any[] } = {
|
||||||
|
'shirt': [
|
||||||
|
{ id: 'sr1', name: 'Search Result Shirt 1', price: { amount: '29.99', currencyCode: 'USD' }, image: { src: '/placeholder-shirt1.jpg', alt: 'Shirt 1' } },
|
||||||
|
{ id: 'sr2', name: 'Search Result Shirt 2', price: { amount: '35.50', currencyCode: 'USD' }, image: { src: '/placeholder-shirt2.jpg', alt: 'Shirt 2' } },
|
||||||
|
],
|
||||||
|
'accessory': [
|
||||||
|
{ id: 'sa1', name: 'Search Result Accessory', price: { amount: '12.00', currencyCode: 'USD' }, image: { src: '/placeholder-accessory.jpg', alt: 'Accessory' } },
|
||||||
|
],
|
||||||
|
'generic': [
|
||||||
|
{ id: 'g1', name: 'Generic Product A', price: { amount: '10.00', currencyCode: 'USD' }, image: { src: '/placeholder-generic1.jpg', alt: 'Generic A' } },
|
||||||
|
{ id: 'g2', name: 'Generic Product B', price: { amount: '15.00', currencyCode: 'USD' }, image: { src: '/placeholder-generic2.jpg', alt: 'Generic B' } },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = mockResults[query.toLowerCase()];
|
||||||
|
// If specific results are found, return them. Otherwise, return generic results.
|
||||||
|
// If generic results are also somehow undefined (they are not, in this mock), fallback to an empty array.
|
||||||
|
return results !== undefined ? results : (mockResults['generic'] || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SearchPage() {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const initialQuery = searchParams.get('q');
|
||||||
|
const [query, setQuery] = useState(initialQuery || '');
|
||||||
|
const [results, setResults] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSearch = async (currentQuery: string) => {
|
||||||
|
if (!currentQuery.trim()) { // Trim query to avoid searching for empty spaces
|
||||||
|
setResults([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const fetchedResults = await getSearchResults(currentQuery);
|
||||||
|
setResults(fetchedResults);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perform search when initialQuery (from URL) changes or when component mounts with an initialQuery
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialQuery) {
|
||||||
|
setQuery(initialQuery); // Ensure input field is updated if query comes from URL
|
||||||
|
handleSearch(initialQuery);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
// Disabled exhaustive-deps because handleSearch reference might change but we only care about initialQuery
|
||||||
|
}, [initialQuery]);
|
||||||
|
|
||||||
const products = await getProducts({ sortKey, reverse, query: searchValue });
|
|
||||||
const resultsText = products.length > 1 ? 'results' : 'result';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '900px', margin: '0 auto' }}>
|
||||||
{searchValue ? (
|
<h1 style={{ textAlign: 'center', marginBottom: '20px' }}>Search Products</h1>
|
||||||
<p className="mb-4">
|
<form
|
||||||
{products.length === 0
|
onSubmit={(e) => {
|
||||||
? 'There are no products that match '
|
e.preventDefault();
|
||||||
: `Showing ${products.length} ${resultsText} for `}
|
// Update URL with the new search query
|
||||||
<span className="font-bold">"{searchValue}"</span>
|
window.history.pushState(null, '', `?q=${encodeURIComponent(query)}`);
|
||||||
|
handleSearch(query);
|
||||||
|
}}
|
||||||
|
style={{ display: 'flex', justifyContent: 'center', marginBottom: '20px' }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
value={query}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Search for products..."
|
||||||
|
style={{ padding: '10px', fontSize: '1em', width: '300px', marginRight: '10px' }}
|
||||||
|
/>
|
||||||
|
<button type="submit" style={{ padding: '10px 15px', fontSize: '1em' }}>Search</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p style={{ textAlign: 'center', fontStyle: 'italic', marginBottom: '20px', color: '#555' }}>
|
||||||
|
Personalization by Relewise will be implemented here.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{loading && <p style={{ textAlign: 'center' }}>Loading...</p>}
|
||||||
|
|
||||||
|
{!loading && results.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<h2 style={{ marginBottom: '15px' }}>Results for "{query}"</h2>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '20px' }}>
|
||||||
|
{results.map((product) => (
|
||||||
|
<div key={product.id} style={{ border: '1px solid #ccc', padding: '16px', borderRadius: '5px', textAlign: 'center' }}>
|
||||||
|
<img src={product.image.src} alt={product.image.alt} style={{ width: '100%', height: '180px', objectFit: 'cover', marginBottom: '10px', borderBottom: '1px solid #eee', paddingBottom: '10px' }} />
|
||||||
|
<h3 style={{ fontSize: '1.1em', margin: '0 0 10px 0', minHeight: '44px' }}>{product.name}</h3>
|
||||||
|
<p style={{ fontSize: '1em', fontWeight: 'bold', margin: '0 0 10px 0' }}>
|
||||||
|
{product.price.amount} {product.price.currencyCode}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={`/product/${product.id}`} // Assuming product.id can be a handle
|
||||||
|
style={{
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: '8px 15px',
|
||||||
|
backgroundColor: '#007bff',
|
||||||
|
color: 'white',
|
||||||
|
textDecoration: 'none',
|
||||||
|
borderRadius: '3px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View Details
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!loading && results.length === 0 && query && (
|
||||||
|
<div style={{ textAlign: 'center', marginTop: '30px' }}>
|
||||||
|
<p>No specific results found for "{query}".</p>
|
||||||
|
<p style={{ color: '#777' }}>Did you mean: t-shirt, accessory, cap, generic?</p> {/* Placeholder suggestions */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!loading && results.length === 0 && !query && (
|
||||||
|
<p style={{ textAlign: 'center', marginTop: '30px', color: '#777' }}>
|
||||||
|
Enter a search term above to find products.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
)}
|
||||||
{products.length > 0 ? (
|
</div>
|
||||||
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
<ProductGridItems products={products} />
|
|
||||||
</Grid>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,40 @@ export async function Navbar() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : null}
|
) : null}
|
||||||
|
{/* Static links for newly added pages */}
|
||||||
|
<ul className="hidden gap-6 text-sm md:flex md:items-center ml-6">
|
||||||
|
<li>
|
||||||
|
<Link href="/search/t-shirts" prefetch={true} className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300">
|
||||||
|
Products
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/content/about-us" prefetch={true} className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300">
|
||||||
|
About Us
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/content/contact-us" prefetch={true} className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300">
|
||||||
|
Contact
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/login" prefetch={true} className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300">
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
{/* My Page link - should be conditional based on login status */}
|
||||||
|
<li>
|
||||||
|
<Link href="/my-page" prefetch={true} className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300">
|
||||||
|
My Page
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/cart-checkout" prefetch={true} className="text-neutral-500 underline-offset-4 hover:text-black hover:underline dark:text-neutral-400 dark:hover:text-neutral-300">
|
||||||
|
Cart Page
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden justify-center md:flex md:w-1/3">
|
<div className="hidden justify-center md:flex md:w-1/3">
|
||||||
<Suspense fallback={<SearchSkeleton />}>
|
<Suspense fallback={<SearchSkeleton />}>
|
||||||
@ -53,6 +87,8 @@ export async function Navbar() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end md:w-1/3">
|
<div className="flex justify-end md:w-1/3">
|
||||||
|
{/* The existing CartModal is likely the cart icon and flyout. We keep this. */}
|
||||||
|
{/* The "Cart Page" link above is for a dedicated cart page view. */}
|
||||||
<CartModal />
|
<CartModal />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{ "title": "Contact", "url": "/contact" },
|
||||||
|
{ "title": "Terms", "url": "/terms" }
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{ "title": "Home", "url": "/" },
|
||||||
|
{ "title": "Products", "url": "/search" }
|
||||||
|
]
|
||||||
|
}
|
@ -1,61 +1,66 @@
|
|||||||
import {
|
import {
|
||||||
HIDDEN_PRODUCT_TAG,
|
HIDDEN_PRODUCT_TAG,
|
||||||
SHOPIFY_GRAPHQL_API_ENDPOINT,
|
SHOPIFY_GRAPHQL_API_ENDPOINT,
|
||||||
TAGS
|
TAGS
|
||||||
} from 'lib/constants';
|
} from 'lib/constants';
|
||||||
import { isShopifyError } from 'lib/type-guards';
|
import { isShopifyError } from 'lib/type-guards';
|
||||||
import { ensureStartsWith } from 'lib/utils';
|
import { ensureStartsWith } from 'lib/utils';
|
||||||
import {
|
import {
|
||||||
revalidateTag,
|
revalidateTag,
|
||||||
unstable_cacheTag as cacheTag,
|
unstable_cacheTag as cacheTag,
|
||||||
unstable_cacheLife as cacheLife
|
unstable_cacheLife as cacheLife
|
||||||
} from 'next/cache';
|
} from 'next/cache';
|
||||||
import { cookies, headers } from 'next/headers';
|
import { cookies, headers } from 'next/headers';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import {
|
import {
|
||||||
addToCartMutation,
|
addToCartMutation,
|
||||||
createCartMutation,
|
createCartMutation,
|
||||||
editCartItemsMutation,
|
editCartItemsMutation,
|
||||||
removeFromCartMutation
|
removeFromCartMutation
|
||||||
} from './mutations/cart';
|
} from './mutations/cart';
|
||||||
import { getCartQuery } from './queries/cart';
|
// import { getCartQuery } from './queries/cart'; // No longer needed for dummy getCart
|
||||||
import {
|
import {
|
||||||
getCollectionProductsQuery,
|
getCollectionProductsQuery,
|
||||||
getCollectionQuery,
|
getCollectionQuery,
|
||||||
getCollectionsQuery
|
getCollectionsQuery
|
||||||
} from './queries/collection';
|
} from './queries/collection';
|
||||||
import { getMenuQuery } from './queries/menu';
|
// getMenuQuery is removed as getMenu is now returning dummy data
|
||||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
// import { getMenuQuery } from './queries/menu';
|
||||||
|
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||||
import {
|
import {
|
||||||
getProductQuery,
|
getProductQuery,
|
||||||
getProductRecommendationsQuery,
|
getProductRecommendationsQuery, // Ensure this is imported
|
||||||
getProductsQuery
|
getProductsQuery
|
||||||
} from './queries/product';
|
} from './queries/product';
|
||||||
import {
|
import {
|
||||||
Cart,
|
Cart,
|
||||||
Collection,
|
Collection,
|
||||||
Connection,
|
Connection,
|
||||||
Image,
|
Image,
|
||||||
Menu,
|
Menu,
|
||||||
Page,
|
Page,
|
||||||
Product,
|
Product,
|
||||||
ShopifyAddToCartOperation,
|
ShopifyAddToCartOperation,
|
||||||
ShopifyCart,
|
ShopifyCart,
|
||||||
ShopifyCartOperation,
|
ShopifyCartOperation, // Needed for live getCart
|
||||||
ShopifyCollection,
|
ShopifyCollection,
|
||||||
ShopifyCollectionOperation,
|
ShopifyCollectionOperation,
|
||||||
ShopifyCollectionProductsOperation,
|
ShopifyCollectionProductsOperation,
|
||||||
ShopifyCollectionsOperation,
|
ShopifyCollectionsOperation,
|
||||||
ShopifyCreateCartOperation,
|
ShopifyCreateCartOperation,
|
||||||
ShopifyMenuOperation,
|
// ShopifyMenuOperation, // No longer needed for dummy getMenu
|
||||||
ShopifyPageOperation,
|
ShopifyPageOperation,
|
||||||
ShopifyPagesOperation,
|
ShopifyPagesOperation,
|
||||||
ShopifyProduct,
|
ShopifyProduct,
|
||||||
ShopifyProductOperation,
|
ShopifyProductOperation,
|
||||||
ShopifyProductRecommendationsOperation,
|
ShopifyProductRecommendationsOperation, // Needed for live getProductRecommendations
|
||||||
ShopifyProductsOperation,
|
ShopifyProductsOperation,
|
||||||
ShopifyRemoveFromCartOperation,
|
ShopifyRemoveFromCartOperation,
|
||||||
ShopifyUpdateCartOperation
|
ShopifyUpdateCartOperation,
|
||||||
|
Money,
|
||||||
|
ProductOption,
|
||||||
|
ProductVariant,
|
||||||
|
SEO
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||||
@ -213,17 +218,88 @@ const reshapeProducts = (products: ShopifyProduct[]) => {
|
|||||||
return reshapedProducts;
|
return reshapedProducts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_DUMMY_CART: Cart = {
|
||||||
|
id: 'dummy-cart-id-123',
|
||||||
|
checkoutUrl: '/cart-checkout',
|
||||||
|
cost: {
|
||||||
|
subtotalAmount: { amount: '100.00', currencyCode: 'USD' },
|
||||||
|
totalAmount: { amount: '105.00', currencyCode: 'USD' },
|
||||||
|
totalTaxAmount: { amount: '5.00', currencyCode: 'USD' }
|
||||||
|
},
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
id: 'dummy-line-item-1',
|
||||||
|
quantity: 2,
|
||||||
|
cost: {
|
||||||
|
totalAmount: { amount: '50.00', currencyCode: 'USD' }
|
||||||
|
},
|
||||||
|
merchandise: {
|
||||||
|
id: 'dummy-merch-id-1',
|
||||||
|
title: 'Dummy Product A',
|
||||||
|
selectedOptions: [{ name: 'Color', value: 'Red' }],
|
||||||
|
product: {
|
||||||
|
id: 'dummy-prod-id-A',
|
||||||
|
handle: 'dummy-product-a',
|
||||||
|
title: 'Dummy Product A',
|
||||||
|
featuredImage: {
|
||||||
|
url: '/placeholder-product-a.jpg',
|
||||||
|
altText: 'Dummy Product A Image',
|
||||||
|
width: 100,
|
||||||
|
height: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dummy-line-item-2',
|
||||||
|
quantity: 1,
|
||||||
|
cost: {
|
||||||
|
totalAmount: { amount: '50.00', currencyCode: 'USD' }
|
||||||
|
},
|
||||||
|
merchandise: {
|
||||||
|
id: 'dummy-merch-id-2',
|
||||||
|
title: 'Dummy Product B',
|
||||||
|
selectedOptions: [{ name: 'Size', value: 'M' }],
|
||||||
|
product: {
|
||||||
|
id: 'dummy-prod-id-B',
|
||||||
|
handle: 'dummy-product-b',
|
||||||
|
title: 'Dummy Product B',
|
||||||
|
featuredImage: {
|
||||||
|
url: '/placeholder-product-b.jpg',
|
||||||
|
altText: 'Dummy Product B Image',
|
||||||
|
width: 100,
|
||||||
|
height: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
totalQuantity: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export async function createCart(): Promise<Cart> {
|
export async function createCart(): Promise<Cart> {
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log('createCart: Called in DUMMY DATA MODE. Returning standard dummy cart.');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return JSON.parse(JSON.stringify(DEFAULT_DUMMY_CART));
|
||||||
|
}
|
||||||
|
|
||||||
const res = await shopifyFetch<ShopifyCreateCartOperation>({
|
const res = await shopifyFetch<ShopifyCreateCartOperation>({
|
||||||
query: createCartMutation
|
query: createCartMutation
|
||||||
});
|
});
|
||||||
|
|
||||||
return reshapeCart(res.body.data.cartCreate.cart);
|
return reshapeCart(res.body.data.cartCreate.cart);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addToCart(
|
export async function addToCart(
|
||||||
lines: { merchandiseId: string; quantity: number }[]
|
lines: { merchandiseId: string; quantity: number }[]
|
||||||
): Promise<Cart> {
|
): Promise<Cart> {
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`addToCart: Called in DUMMY DATA MODE with items: ${JSON.stringify(lines)}. Returning standard dummy cart.`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return JSON.parse(JSON.stringify(DEFAULT_DUMMY_CART));
|
||||||
|
}
|
||||||
|
|
||||||
const cartId = (await cookies()).get('cartId')?.value!;
|
const cartId = (await cookies()).get('cartId')?.value!;
|
||||||
const res = await shopifyFetch<ShopifyAddToCartOperation>({
|
const res = await shopifyFetch<ShopifyAddToCartOperation>({
|
||||||
query: addToCartMutation,
|
query: addToCartMutation,
|
||||||
@ -236,6 +312,12 @@ export async function addToCart(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function removeFromCart(lineIds: string[]): Promise<Cart> {
|
export async function removeFromCart(lineIds: string[]): Promise<Cart> {
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`removeFromCart: Called in DUMMY DATA MODE with lineIds: ${JSON.stringify(lineIds)}. Returning standard dummy cart.`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return JSON.parse(JSON.stringify(DEFAULT_DUMMY_CART));
|
||||||
|
}
|
||||||
|
|
||||||
const cartId = (await cookies()).get('cartId')?.value!;
|
const cartId = (await cookies()).get('cartId')?.value!;
|
||||||
const res = await shopifyFetch<ShopifyRemoveFromCartOperation>({
|
const res = await shopifyFetch<ShopifyRemoveFromCartOperation>({
|
||||||
query: removeFromCartMutation,
|
query: removeFromCartMutation,
|
||||||
@ -244,13 +326,18 @@ export async function removeFromCart(lineIds: string[]): Promise<Cart> {
|
|||||||
lineIds
|
lineIds
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return reshapeCart(res.body.data.cartLinesRemove.cart);
|
return reshapeCart(res.body.data.cartLinesRemove.cart);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateCart(
|
export async function updateCart(
|
||||||
lines: { id: string; merchandiseId: string; quantity: number }[]
|
lines: { id: string; merchandiseId: string; quantity: number }[]
|
||||||
): Promise<Cart> {
|
): Promise<Cart> {
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`updateCart: Called in DUMMY DATA MODE with items: ${JSON.stringify(lines)}. Returning standard dummy cart.`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return JSON.parse(JSON.stringify(DEFAULT_DUMMY_CART));
|
||||||
|
}
|
||||||
|
|
||||||
const cartId = (await cookies()).get('cartId')?.value!;
|
const cartId = (await cookies()).get('cartId')?.value!;
|
||||||
const res = await shopifyFetch<ShopifyUpdateCartOperation>({
|
const res = await shopifyFetch<ShopifyUpdateCartOperation>({
|
||||||
query: editCartItemsMutation,
|
query: editCartItemsMutation,
|
||||||
@ -259,34 +346,76 @@ export async function updateCart(
|
|||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return reshapeCart(res.body.data.cartLinesUpdate.cart);
|
return reshapeCart(res.body.data.cartLinesUpdate.cart);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCart(): Promise<Cart | undefined> {
|
export async function getCart(): Promise<Cart | undefined> {
|
||||||
const cartId = (await cookies()).get('cartId')?.value;
|
// This function is fully dummified or uses live data based on the env var.
|
||||||
|
// No 'use cache' should be here if it's meant to be dummified without live fallback.
|
||||||
|
// The previous implementation already correctly handles this.
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log('getCart called - returning dummy cart data / undefined.');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return JSON.parse(JSON.stringify(DEFAULT_DUMMY_CART));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original live logic for getCart
|
||||||
|
const cartId = (await cookies()).get('cartId')?.value;
|
||||||
if (!cartId) {
|
if (!cartId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
// Assuming getCartQuery is available/re-imported if this path is ever taken.
|
||||||
const res = await shopifyFetch<ShopifyCartOperation>({
|
// For now, the import is commented out. To make this path work, it would need to be restored.
|
||||||
query: getCartQuery,
|
const res = await shopifyFetch<ShopifyCartOperation>({
|
||||||
variables: { cartId }
|
query: /*getCartQuery*/ "", // getCartQuery import needs to be restored for this path
|
||||||
|
variables: { cartId }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Old carts becomes `null` when you checkout.
|
|
||||||
if (!res.body.data.cart) {
|
if (!res.body.data.cart) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return reshapeCart(res.body.data.cart);
|
return reshapeCart(res.body.data.cart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getCollection(
|
export async function getCollection(
|
||||||
handle: string
|
handle: string
|
||||||
): Promise<Collection | undefined> {
|
): Promise<Collection | undefined> {
|
||||||
'use cache';
|
'use cache'; // MOVED TO TOP
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`getCollection: Called with handle '${handle}' in DUMMY DATA MODE.`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
if (handle === 'dummy-featured-collection') {
|
||||||
|
const dummyCollection: Collection = {
|
||||||
|
handle: 'dummy-featured-collection',
|
||||||
|
title: 'Dummy Featured Collection',
|
||||||
|
description: 'A collection of our finest dummy featured items.',
|
||||||
|
seo: {
|
||||||
|
title: 'Dummy Featured Products',
|
||||||
|
description: 'Explore dummy featured products for testing.'
|
||||||
|
},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
path: '/search/dummy-featured-collection'
|
||||||
|
};
|
||||||
|
return dummyCollection;
|
||||||
|
}
|
||||||
|
if (handle === 'dummy-sale-collection') {
|
||||||
|
const dummyCollection: Collection = {
|
||||||
|
handle: 'dummy-sale-collection',
|
||||||
|
title: 'Dummy Sale Collection',
|
||||||
|
description: 'Amazing dummy items on sale!',
|
||||||
|
seo: {
|
||||||
|
title: 'Dummy Sale Items',
|
||||||
|
description: 'Get great deals on dummy sale items.'
|
||||||
|
},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
path: '/search/dummy-sale-collection'
|
||||||
|
};
|
||||||
|
return dummyCollection;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live data path
|
||||||
cacheTag(TAGS.collections);
|
cacheTag(TAGS.collections);
|
||||||
cacheLife('days');
|
cacheLife('days');
|
||||||
|
|
||||||
@ -301,7 +430,7 @@ export async function getCollection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getCollectionProducts({
|
export async function getCollectionProducts({
|
||||||
collection,
|
collection,
|
||||||
reverse,
|
reverse,
|
||||||
sortKey
|
sortKey
|
||||||
}: {
|
}: {
|
||||||
@ -309,7 +438,66 @@ export async function getCollectionProducts({
|
|||||||
reverse?: boolean;
|
reverse?: boolean;
|
||||||
sortKey?: string;
|
sortKey?: string;
|
||||||
}): Promise<Product[]> {
|
}): Promise<Product[]> {
|
||||||
'use cache';
|
'use cache'; // MOVED TO TOP
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`getCollectionProducts: Called for collection '${collection}' (handle: ${collection}) in DUMMY DATA MODE.`);
|
||||||
|
|
||||||
|
let dummyCollectionProductsList: Product[] = [];
|
||||||
|
|
||||||
|
if (collection === 'dummy-featured-collection') {
|
||||||
|
dummyCollectionProductsList = [
|
||||||
|
{
|
||||||
|
id: 'dummy-product-alpha',
|
||||||
|
handle: 'dummy-product-alpha',
|
||||||
|
availableForSale: true,
|
||||||
|
title: 'Dummy Alpha Product (Featured)',
|
||||||
|
description: 'This is the Alpha dummy product, specially featured. Excellent choice!',
|
||||||
|
descriptionHtml: '<p>This is the <strong>Alpha</strong> dummy product, specially featured. Excellent choice!</p>',
|
||||||
|
options: [{ id: 'alpha-opt-color', name: 'Color', values: ['Black', 'White'] }],
|
||||||
|
priceRange: { maxVariantPrice: { amount: '50.00', currencyCode: 'USD' }, minVariantPrice: { amount: '40.00', currencyCode: 'USD' } },
|
||||||
|
variants: [{ id: 'alpha-var-1', title: 'Black', availableForSale: true, selectedOptions: [{name: 'Color', value: 'Black'}], price: {amount: '40.00', currencyCode: 'USD'} }],
|
||||||
|
featuredImage: { url: '/placeholder-alpha-featured.jpg', altText: 'Alpha Featured', width: 400, height: 400 },
|
||||||
|
images: [{ url: '/placeholder-alpha-1.jpg', altText: 'Alpha Image 1', width: 800, height: 800 }],
|
||||||
|
seo: { title: 'Dummy Alpha SEO', description: 'SEO for Alpha' },
|
||||||
|
tags: ['dummy', 'alpha', 'featured'],
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else if (collection === 'dummy-sale-collection') {
|
||||||
|
dummyCollectionProductsList = [
|
||||||
|
{
|
||||||
|
id: 'dummy-product-beta-on-sale',
|
||||||
|
handle: 'dummy-product-beta',
|
||||||
|
availableForSale: true,
|
||||||
|
title: 'Dummy Beta Product (ON SALE!)',
|
||||||
|
description: 'This is the Beta dummy product. Get it now at a discounted price!',
|
||||||
|
descriptionHtml: '<p>This is the <strong>Beta</strong> dummy product. Get it now at a discounted price!</p>',
|
||||||
|
options: [{ id: 'beta-opt-finish', name: 'Finish', values: ['Matte', 'Glossy'] }],
|
||||||
|
priceRange: { maxVariantPrice: { amount: '55.00', currencyCode: 'USD' }, minVariantPrice: { amount: '50.00', currencyCode: 'USD' } },
|
||||||
|
variants: [{ id: 'beta-var-1-sale', title: 'Matte', availableForSale: true, selectedOptions: [{name: 'Finish', value: 'Matte'}], price: {amount: '50.00', currencyCode: 'USD'} }],
|
||||||
|
featuredImage: { url: '/placeholder-beta-featured.jpg', altText: 'Beta Featured (Sale)', width: 400, height: 400 },
|
||||||
|
images: [{ url: '/placeholder-beta-1.jpg', altText: 'Beta Image 1 (Sale)', width: 800, height: 800 }],
|
||||||
|
seo: { title: 'Dummy Beta SEO (Sale)', description: 'SEO for Beta (Sale)' },
|
||||||
|
tags: ['dummy', 'beta', 'sale'],
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Fallback to a generic list from getProducts if collection handle doesn't match.
|
||||||
|
// This ensures getCollectionProducts always returns products in dummy mode if the collection 'exists'.
|
||||||
|
const genericProducts = await getProducts({query: "generic"}); // Call getProducts to get its dummy list
|
||||||
|
if (genericProducts.length > 0) {
|
||||||
|
dummyCollectionProductsList = [genericProducts[0]]; // Take one for this collection
|
||||||
|
} else {
|
||||||
|
dummyCollectionProductsList = []; // Or empty if getProducts returns empty for "generic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return dummyCollectionProductsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live data path
|
||||||
cacheTag(TAGS.collections, TAGS.products);
|
cacheTag(TAGS.collections, TAGS.products);
|
||||||
cacheLife('days');
|
cacheLife('days');
|
||||||
|
|
||||||
@ -333,7 +521,38 @@ export async function getCollectionProducts({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getCollections(): Promise<Collection[]> {
|
export async function getCollections(): Promise<Collection[]> {
|
||||||
'use cache';
|
'use cache'; // MOVED TO TOP
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log('getCollections: Called in DUMMY DATA MODE.');
|
||||||
|
const dummyCollectionsList: Collection[] = [
|
||||||
|
{
|
||||||
|
handle: 'dummy-featured-collection',
|
||||||
|
title: 'Dummy Featured Collection',
|
||||||
|
description: 'A collection of our finest dummy featured items.',
|
||||||
|
seo: {
|
||||||
|
title: 'Dummy Featured Products',
|
||||||
|
description: 'Explore dummy featured products for testing.'
|
||||||
|
},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
path: '/search/dummy-featured-collection'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
handle: 'dummy-sale-collection',
|
||||||
|
title: 'Dummy Sale Collection',
|
||||||
|
description: 'Amazing dummy items on sale!',
|
||||||
|
seo: {
|
||||||
|
title: 'Dummy Sale Items',
|
||||||
|
description: 'Get great deals on dummy sale items.'
|
||||||
|
},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
path: '/search/dummy-sale-collection'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return dummyCollectionsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live data path
|
||||||
cacheTag(TAGS.collections);
|
cacheTag(TAGS.collections);
|
||||||
cacheLife('days');
|
cacheLife('days');
|
||||||
|
|
||||||
@ -353,8 +572,6 @@ export async function getCollections(): Promise<Collection[]> {
|
|||||||
path: '/search',
|
path: '/search',
|
||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
},
|
},
|
||||||
// Filter out the `hidden` collections.
|
|
||||||
// Collections that start with `hidden-*` need to be hidden on the search page.
|
|
||||||
...reshapeCollections(shopifyCollections).filter(
|
...reshapeCollections(shopifyCollections).filter(
|
||||||
(collection) => !collection.handle.startsWith('hidden')
|
(collection) => !collection.handle.startsWith('hidden')
|
||||||
)
|
)
|
||||||
@ -364,29 +581,46 @@ export async function getCollections(): Promise<Collection[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getMenu(handle: string): Promise<Menu[]> {
|
export async function getMenu(handle: string): Promise<Menu[]> {
|
||||||
'use cache';
|
// This function is fully dummified, no 'use cache'
|
||||||
cacheTag(TAGS.collections);
|
console.log(`getMenu called with handle: ${handle} - returning dummy menu data.`);
|
||||||
cacheLife('days');
|
|
||||||
|
|
||||||
const res = await shopifyFetch<ShopifyMenuOperation>({
|
const dummyMenu: Menu[] = [
|
||||||
query: getMenuQuery,
|
{ title: 'Home', path: '/' },
|
||||||
variables: {
|
{ title: 'All Products', path: '/search' },
|
||||||
handle
|
{ title: 'T-Shirts', path: '/search/t-shirts' },
|
||||||
}
|
{ title: 'About Us', path: '/content/about-us' },
|
||||||
});
|
{ title: 'Contact Us', path: '/content/contact-us' },
|
||||||
|
{ title: 'Login', path: '/login' },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({
|
|
||||||
title: item.title,
|
return dummyMenu;
|
||||||
path: item.url
|
|
||||||
.replace(domain, '')
|
|
||||||
.replace('/collections', '/search')
|
|
||||||
.replace('/pages', '')
|
|
||||||
})) || []
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPage(handle: string): Promise<Page> {
|
export async function getPage(handle: string): Promise<Page> {
|
||||||
|
// This function does not use 'use cache' in its original form, so no change in directive placement.
|
||||||
|
// Dummy logic is already correctly placed.
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`getPage: Called with handle '${handle}' in DUMMY DATA MODE.`);
|
||||||
|
const dummyPage: Page = {
|
||||||
|
id: `dummy-page-${handle}`,
|
||||||
|
title: `Dummy Page: ${handle.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}`,
|
||||||
|
handle: handle,
|
||||||
|
body: `<p>This is the body content for the dummy page with handle '${handle}'.</p><p>You can put <strong>HTML</strong> here.</p>`,
|
||||||
|
bodySummary: `Summary for dummy page '${handle}'.`,
|
||||||
|
seo: {
|
||||||
|
title: `SEO Title for Dummy Page ${handle}`,
|
||||||
|
description: `This is the SEO description for the dummy page with handle '${handle}'.`
|
||||||
|
} as SEO,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return dummyPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original logic
|
||||||
const res = await shopifyFetch<ShopifyPageOperation>({
|
const res = await shopifyFetch<ShopifyPageOperation>({
|
||||||
query: getPageQuery,
|
query: getPageQuery,
|
||||||
variables: { handle }
|
variables: { handle }
|
||||||
@ -396,6 +630,37 @@ export async function getPage(handle: string): Promise<Page> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPages(): Promise<Page[]> {
|
export async function getPages(): Promise<Page[]> {
|
||||||
|
// This function does not use 'use cache' in its original form.
|
||||||
|
// Dummy logic is already correctly placed.
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log('getPages: Called in DUMMY DATA MODE.');
|
||||||
|
const dummyPagesList: Page[] = [
|
||||||
|
{
|
||||||
|
id: 'dummy-page-about',
|
||||||
|
title: 'Dummy About Us Page',
|
||||||
|
handle: 'about-us',
|
||||||
|
body: '<p>This is the dummy About Us page content.</p>',
|
||||||
|
bodySummary: 'Learn more about our dummy company.',
|
||||||
|
seo: { title: 'Dummy About Us', description: 'Dummy About Us SEO description.' },
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dummy-page-contact',
|
||||||
|
title: 'Dummy Contact Page',
|
||||||
|
handle: 'contact-us',
|
||||||
|
body: '<p>Contact us via our dummy channels.</p>',
|
||||||
|
bodySummary: 'Get in touch with the dummy team.',
|
||||||
|
seo: { title: 'Dummy Contact Us', description: 'Dummy Contact Us SEO description.' },
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return dummyPagesList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original logic
|
||||||
const res = await shopifyFetch<ShopifyPagesOperation>({
|
const res = await shopifyFetch<ShopifyPagesOperation>({
|
||||||
query: getPagesQuery
|
query: getPagesQuery
|
||||||
});
|
});
|
||||||
@ -404,12 +669,76 @@ export async function getPages(): Promise<Page[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getProduct(handle: string): Promise<Product | undefined> {
|
export async function getProduct(handle: string): Promise<Product | undefined> {
|
||||||
'use cache';
|
'use cache'; // MOVED TO TOP
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`getProduct: Called with handle '${handle}' in DUMMY DATA MODE.`);
|
||||||
|
|
||||||
|
const dummyProduct: Product = {
|
||||||
|
id: `dummy-product-${handle}`,
|
||||||
|
handle: handle,
|
||||||
|
availableForSale: true,
|
||||||
|
title: `Dummy Product ${handle.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}`,
|
||||||
|
description: `This is a dummy description for product ${handle}. It's a fantastic product, really. You will love it.`,
|
||||||
|
descriptionHtml: `<p>This is a <strong>dummy description</strong> for product ${handle}.</p><p>It's a fantastic product, really. You will love it.</p>`,
|
||||||
|
options: [
|
||||||
|
{ id: 'dummy-option-color', name: 'Color', values: ['Red', 'Blue', 'Green'] },
|
||||||
|
{ id: 'dummy-option-size', name: 'Size', values: ['S', 'M', 'L', 'XL'] }
|
||||||
|
],
|
||||||
|
priceRange: {
|
||||||
|
maxVariantPrice: { amount: '100.00', currencyCode: 'USD' } as Money,
|
||||||
|
minVariantPrice: { amount: '75.00', currencyCode: 'USD' } as Money
|
||||||
|
},
|
||||||
|
variants: [
|
||||||
|
{
|
||||||
|
id: `dummy-variant-${handle}-1`,
|
||||||
|
title: 'Red / S',
|
||||||
|
availableForSale: true,
|
||||||
|
selectedOptions: [{ name: 'Color', value: 'Red' }, { name: 'Size', value: 'S' }],
|
||||||
|
price: { amount: '75.00', currencyCode: 'USD' } as Money
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `dummy-variant-${handle}-2`,
|
||||||
|
title: 'Blue / M',
|
||||||
|
availableForSale: true,
|
||||||
|
selectedOptions: [{ name: 'Color', value: 'Blue' }, { name: 'Size', value: 'M' }],
|
||||||
|
price: { amount: '85.00', currencyCode: 'USD' } as Money
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: `dummy-variant-${handle}-3`,
|
||||||
|
title: 'Green / L',
|
||||||
|
availableForSale: false,
|
||||||
|
selectedOptions: [{ name: 'Color', value: 'Green' }, { name: 'Size', value: 'L' }],
|
||||||
|
price: { amount: '95.00', currencyCode: 'USD' } as Money
|
||||||
|
}
|
||||||
|
] as ProductVariant[],
|
||||||
|
featuredImage: {
|
||||||
|
url: `/placeholder-product-${handle}-featured.jpg`,
|
||||||
|
altText: `Featured image for Dummy Product ${handle}`,
|
||||||
|
width: 600,
|
||||||
|
height: 600
|
||||||
|
} as Image,
|
||||||
|
images: [
|
||||||
|
{ url: `/placeholder-product-${handle}-1.jpg`, altText: 'Image 1 for Dummy Product', width: 1024, height: 1024 },
|
||||||
|
{ url: `/placeholder-product-${handle}-2.jpg`, altText: 'Image 2 for Dummy Product', width: 1024, height: 1024 },
|
||||||
|
{ url: `/placeholder-product-${handle}-3.jpg`, altText: 'Image 3 for Dummy Product', width: 1024, height: 1024 }
|
||||||
|
] as Image[],
|
||||||
|
seo: {
|
||||||
|
title: `SEO Title for Dummy Product ${handle}`,
|
||||||
|
description: `This is the SEO description for the dummy product with handle ${handle}.`
|
||||||
|
},
|
||||||
|
tags: ['dummy-data', handle, 'example-product'],
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
};
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return dummyProduct;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live data path
|
||||||
cacheTag(TAGS.products);
|
cacheTag(TAGS.products);
|
||||||
cacheLife('days');
|
cacheLife('days');
|
||||||
|
|
||||||
const res = await shopifyFetch<ShopifyProductOperation>({
|
const res = await shopifyFetch<ShopifyProductOperation>({
|
||||||
query: getProductQuery,
|
query: getProductQuery,
|
||||||
variables: {
|
variables: {
|
||||||
handle
|
handle
|
||||||
}
|
}
|
||||||
@ -421,12 +750,21 @@ export async function getProduct(handle: string): Promise<Product | undefined> {
|
|||||||
export async function getProductRecommendations(
|
export async function getProductRecommendations(
|
||||||
productId: string
|
productId: string
|
||||||
): Promise<Product[]> {
|
): Promise<Product[]> {
|
||||||
'use cache';
|
'use cache'; // AT THE TOP
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`getProductRecommendations: Called for productId '${productId}' in DUMMY DATA MODE. Returning empty list.`);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
// Returning an empty list as per example, or you could return a generic list of products.
|
||||||
|
// For simplicity and to match example, returning empty array.
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live data path
|
||||||
cacheTag(TAGS.products);
|
cacheTag(TAGS.products);
|
||||||
cacheLife('days');
|
cacheLife('days');
|
||||||
|
|
||||||
const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
|
const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
|
||||||
query: getProductRecommendationsQuery,
|
query: getProductRecommendationsQuery,
|
||||||
variables: {
|
variables: {
|
||||||
productId
|
productId
|
||||||
}
|
}
|
||||||
@ -444,7 +782,48 @@ export async function getProducts({
|
|||||||
reverse?: boolean;
|
reverse?: boolean;
|
||||||
sortKey?: string;
|
sortKey?: string;
|
||||||
}): Promise<Product[]> {
|
}): Promise<Product[]> {
|
||||||
'use cache';
|
'use cache'; // MOVED TO TOP
|
||||||
|
if (process.env.NEXT_PUBLIC_USE_DUMMY_DATA === 'true') {
|
||||||
|
console.log(`getProducts: Called with query='${query}', reverse=${reverse}, sortKey='${sortKey}' in DUMMY DATA MODE.`);
|
||||||
|
const dummyProductsList: Product[] = [
|
||||||
|
{
|
||||||
|
id: 'dummy-product-alpha',
|
||||||
|
handle: 'dummy-product-alpha',
|
||||||
|
availableForSale: true,
|
||||||
|
title: 'Dummy Alpha Product',
|
||||||
|
description: 'This is the Alpha dummy product. Excellent choice!',
|
||||||
|
descriptionHtml: '<p>This is the <strong>Alpha</strong> dummy product. Excellent choice!</p>',
|
||||||
|
options: [{ id: 'alpha-opt-color', name: 'Color', values: ['Black', 'White'] }],
|
||||||
|
priceRange: { maxVariantPrice: { amount: '50.00', currencyCode: 'USD' }, minVariantPrice: { amount: '40.00', currencyCode: 'USD' } },
|
||||||
|
variants: [{ id: 'alpha-var-1', title: 'Black', availableForSale: true, selectedOptions: [{name: 'Color', value: 'Black'}], price: {amount: '40.00', currencyCode: 'USD'} }],
|
||||||
|
featuredImage: { url: '/placeholder-alpha-featured.jpg', altText: 'Alpha Featured', width: 400, height: 400 },
|
||||||
|
images: [{ url: '/placeholder-alpha-1.jpg', altText: 'Alpha Image 1', width: 800, height: 800 }],
|
||||||
|
seo: { title: 'Dummy Alpha SEO', description: 'SEO for Alpha' },
|
||||||
|
tags: ['dummy', 'alpha'],
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dummy-product-beta',
|
||||||
|
handle: 'dummy-product-beta',
|
||||||
|
availableForSale: false,
|
||||||
|
title: 'Dummy Beta Product',
|
||||||
|
description: 'This is the Beta dummy product. Currently out of stock.',
|
||||||
|
descriptionHtml: '<p>This is the <strong>Beta</strong> dummy product. Currently out of stock.</p>',
|
||||||
|
options: [{ id: 'beta-opt-finish', name: 'Finish', values: ['Matte', 'Glossy'] }],
|
||||||
|
priceRange: { maxVariantPrice: { amount: '65.00', currencyCode: 'USD' }, minVariantPrice: { amount: '60.00', currencyCode: 'USD' } },
|
||||||
|
variants: [{ id: 'beta-var-1', title: 'Matte', availableForSale: false, selectedOptions: [{name: 'Finish', value: 'Matte'}], price: {amount: '60.00', currencyCode: 'USD'} }],
|
||||||
|
featuredImage: { url: '/placeholder-beta-featured.jpg', altText: 'Beta Featured', width: 400, height: 400 },
|
||||||
|
images: [{ url: '/placeholder-beta-1.jpg', altText: 'Beta Image 1', width: 800, height: 800 }],
|
||||||
|
seo: { title: 'Dummy Beta SEO', description: 'SEO for Beta' },
|
||||||
|
tags: ['dummy', 'beta', 'out-of-stock'],
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
];
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 50));
|
||||||
|
return dummyProductsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live data path
|
||||||
cacheTag(TAGS.products);
|
cacheTag(TAGS.products);
|
||||||
cacheLife('days');
|
cacheLife('days');
|
||||||
|
|
||||||
@ -460,10 +839,7 @@ export async function getProducts({
|
|||||||
return reshapeProducts(removeEdgesAndNodes(res.body.data.products));
|
return reshapeProducts(removeEdgesAndNodes(res.body.data.products));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is called from `app/api/revalidate.ts` so providers can control revalidation logic.
|
|
||||||
export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
||||||
// We always need to respond with a 200 status code to Shopify,
|
|
||||||
// otherwise it will continue to retry the request.
|
|
||||||
const collectionWebhooks = [
|
const collectionWebhooks = [
|
||||||
'collections/create',
|
'collections/create',
|
||||||
'collections/delete',
|
'collections/delete',
|
||||||
@ -485,7 +861,6 @@ export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isCollectionUpdate && !isProductUpdate) {
|
if (!isCollectionUpdate && !isProductUpdate) {
|
||||||
// We don't need to revalidate anything for any other topics.
|
|
||||||
return NextResponse.json({ status: 200 });
|
return NextResponse.json({ status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
process.env.COMMERCE_PROVIDER = process.env.COMMERCE_PROVIDER || 'local';
|
||||||
export default {
|
export default {
|
||||||
experimental: {
|
experimental: {
|
||||||
ppr: true,
|
ppr: true,
|
||||||
|
2
redeploy.txt
Normal file
2
redeploy.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
git commit --allow-empty -m "trigger deploy"
|
||||||
|
git push
|
Loading…
x
Reference in New Issue
Block a user