Merge dd0962ac28ec1eee14337e5a939f8c8aee8f2998 into fa1306916c652ea5f820d5b400087bece13460fd

This commit is contained in:
basstian-ai 2025-05-23 08:10:55 +00:00 committed by GitHub
commit 731026b02f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1310 additions and 290 deletions

View File

@ -1,5 +1,6 @@
COMPANY_NAME="Vercel Inc."
SITE_NAME="Next.js Commerce"
SHOPIFY_REVALIDATION_SECRET=""
SHOPIFY_STOREFRONT_ACCESS_TOKEN=""
SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com"
COMMERCE_PROVIDER=crystallize
NEXT_PUBLIC_COMMERCE_PROVIDER=crystallize
CRYSTALLIZE_API_URL=https://api.crystallize.com/6422a2c186ef95b31e1cd1e5/graphql
CRYSTALLIZE_ACCESS_TOKEN=c3ab0fef20aadcbeb9919e6701f015cfb4ddf1ff
NEXT_PUBLIC_USE_DUMMY_DATA=true

9
.env.production Normal file
View 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
View 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
View 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
View 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>
);
}

View 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
View 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
View 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
View 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>
);
}

View File

@ -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() {
return (
<>
{/* Existing components can remain if they are part of the desired layout */}
<ThreeItemGrid />
<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 />
</>
);

View File

@ -1,149 +1,77 @@
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
// app/product/[handle]/page.tsx
import { GridTileImage } from 'components/grid/tile';
import Footer from 'components/layout/footer';
import { Gallery } from 'components/product/gallery';
import { ProductProvider } from 'components/product/product-context';
import { ProductDescription } from 'components/product/product-description';
import { HIDDEN_PRODUCT_TAG } from 'lib/constants';
import { getProduct, getProductRecommendations } from 'lib/shopify';
import { Image } from 'lib/shopify/types';
import Link from 'next/link';
import { Suspense } from 'react';
// Simulate fetching product data
async function getProduct(handle: string) {
const allProducts: { [key: string]: any } = { // Use 'any' for simplicity in this mock
'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' } ] },
'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' } ] },
'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' } ] }
};
await new Promise(resolve => setTimeout(resolve, 50)); // Simulate network delay
return allProducts[String(handle)] || null; // Ensure handle is string
}
export async function generateMetadata(props: {
interface ProductPageProps {
params: Promise<{ handle: string }>;
}): Promise<Metadata> {
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
};
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function ProductPage(props: { params: Promise<{ handle: string }> }) {
const params = await props.params;
const product = await getProduct(params.handle);
export default async function ProductPage({ params, searchParams }: ProductPageProps) {
const resolvedParams = await params;
// const resolvedSearchParams = await searchParams; // If needed
if (!product) return notFound();
const product = await getProduct(resolvedParams.handle);
const productJsonLd = {
'@context': 'https://schema.org',
'@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
if (!product) {
return <div style={{ padding: '20px', textAlign: 'center' }}>Product not found for handle: {resolvedParams.handle}</div>;
}
};
return (
<ProductProvider>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(productJsonLd)
}}
// ... (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)
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '800px', margin: '0 auto' }}>
<h1>{product.name}</h1>
{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' }}
/>
<div className="mx-auto max-w-(--breakpoint-2xl) px-4">
<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">
<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>
);
}
<p style={{ fontSize: '1.1em', lineHeight: '1.6' }}>{product.description}</p>
async function RelatedProducts({ id }: { id: string }) {
const relatedProducts = await getProductRecommendations(id);
<p style={{ fontSize: '1.2em', fontWeight: 'bold', margin: '20px 0' }}>
Price: {product.price.amount} {product.price.currencyCode}
</p>
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>
<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'
}}
>
Add to Cart
</button>
</div>
);
}

View File

@ -1,45 +1,117 @@
import { getCollection, getCollectionProducts } from 'lib/shopify';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
// app/search/[collection]/page.tsx
import Grid from 'components/grid';
import ProductGridItems from 'components/layout/product-grid-items';
import { defaultSort, sorting } from 'lib/constants';
// Simulate fetching products for a collection
async function getProductsByCollection(collectionName: string) {
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 50));
export async function generateMetadata(props: {
params: Promise<{ collection: string }>;
}): Promise<Metadata> {
const params = await props.params;
const collection = await getCollection(params.collection);
if (!collection) return notFound();
return {
title: collection.seo?.title || collection.title,
description:
collection.seo?.description || collection.description || `${collection.title} products`
const allProducts: { [key: string]: any[] } = {
't-shirts': [
{ id: 'ts1', name: 'Cool T-Shirt', price: { amount: '19.99', currencyCode: 'USD' }, image: { src: '/placeholder-tshirt1.jpg', alt: 'T-Shirt 1' } },
{ id: 'ts2', name: 'Graphic Tee', price: { amount: '24.99', currencyCode: 'USD' }, image: { src: '/placeholder-tshirt2.jpg', alt: 'T-Shirt 2' } },
{ id: 'ts3', name: 'Plain V-Neck', price: { amount: '15.50', currencyCode: 'USD' }, image: { src: '/placeholder-tshirt3.jpg', alt: 'Plain V-Neck' } },
],
'accessories': [
{ id: 'ac1', name: 'Stylish Cap', price: { amount: '15.00', currencyCode: 'USD' }, image: { src: '/placeholder-cap.jpg', alt: 'Cap' } },
{ id: 'ac2', name: 'Leather Belt', price: { amount: '35.00', currencyCode: 'USD' }, image: { src: '/placeholder-belt.jpg', alt: 'Leather Belt' } },
],
'footwear': [
{ 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 }>;
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
const params = await props.params;
const { sort } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
const products = await getCollectionProducts({ collection: params.collection, sortKey, reverse });
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
export default async function CollectionPage({ params, searchParams }: CollectionPageProps) {
const resolvedParams = await params;
// const resolvedSearchParams = await searchParams; // Await if needed for filtering, etc.
const products = await getProductsByCollection(resolvedParams.collection);
const collectionName = resolvedParams.collection.charAt(0).toUpperCase() + resolvedParams.collection.slice(1);
return (
<section>
{products.length === 0 ? (
<p className="py-3 text-lg">{`No products found in this collection`}</p>
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif' }}>
<h1 style={{ marginBottom: '20px', borderBottom: '2px solid #eee', paddingBottom: '10px' }}>
Products in: {collectionName}
</h1>
<div style={{ display: 'flex', gap: '20px', marginBottom: '30px' }}>
{/* Placeholder Filters */}
<div style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '5px', width: '250px' }}>
<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>
))
) : (
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<ProductGridItems products={products} />
</Grid>
<p style={{ gridColumn: '1 / -1' }}>No products found in this collection.</p>
)}
</section>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,38 +1,135 @@
import Grid from 'components/grid';
import ProductGridItems from 'components/layout/product-grid-items';
import { defaultSort, sorting } from 'lib/constants';
import { getProducts } from 'lib/shopify';
// app/search/page.tsx
'use client'; // Required for using client-side features like useState
export const metadata = {
title: 'Search',
description: 'Search for products in the store.'
import { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation'; // To get query from URL
// Simulate fetching search results
async function getSearchResults(query: string | null): Promise<any[]> { // Explicit Promise<any[]>
// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 50));
if (!query || !query.trim()) { // Also handle empty/whitespace query after trimming
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' } },
]
};
export default async function SearchPage(props: {
searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const searchParams = await props.searchParams;
const { sort, q: searchValue } = searchParams as { [key: string]: string };
const { sortKey, reverse } = sorting.find((item) => item.slug === sort) || defaultSort;
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 (
<>
{searchValue ? (
<p className="mb-4">
{products.length === 0
? 'There are no products that match '
: `Showing ${products.length} ${resultsText} for `}
<span className="font-bold">&quot;{searchValue}&quot;</span>
<div style={{ padding: '20px', fontFamily: 'Arial, sans-serif', maxWidth: '900px', margin: '0 auto' }}>
<h1 style={{ textAlign: 'center', marginBottom: '20px' }}>Search Products</h1>
<form
onSubmit={(e) => {
e.preventDefault();
// Update URL with the new search query
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>
) : null}
{products.length > 0 ? (
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<ProductGridItems products={products} />
</Grid>
) : null}
</>
{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>
)}
</div>
);
}

View File

@ -46,6 +46,40 @@ export async function Navbar() {
))}
</ul>
) : 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 className="hidden justify-center md:flex md:w-1/3">
<Suspense fallback={<SearchSkeleton />}>
@ -53,6 +87,8 @@ export async function Navbar() {
</Suspense>
</div>
<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 />
</div>
</div>

View File

@ -0,0 +1,6 @@
{
"items": [
{ "title": "Contact", "url": "/contact" },
{ "title": "Terms", "url": "/terms" }
]
}

View File

@ -0,0 +1,6 @@
{
"items": [
{ "title": "Home", "url": "/" },
{ "title": "Products", "url": "/search" }
]
}

View File

@ -18,17 +18,18 @@ import {
editCartItemsMutation,
removeFromCartMutation
} from './mutations/cart';
import { getCartQuery } from './queries/cart';
// import { getCartQuery } from './queries/cart'; // No longer needed for dummy getCart
import {
getCollectionProductsQuery,
getCollectionQuery,
getCollectionsQuery
} from './queries/collection';
import { getMenuQuery } from './queries/menu';
// getMenuQuery is removed as getMenu is now returning dummy data
// import { getMenuQuery } from './queries/menu';
import { getPageQuery, getPagesQuery } from './queries/page';
import {
getProductQuery,
getProductRecommendationsQuery,
getProductRecommendationsQuery, // Ensure this is imported
getProductsQuery
} from './queries/product';
import {
@ -41,21 +42,25 @@ import {
Product,
ShopifyAddToCartOperation,
ShopifyCart,
ShopifyCartOperation,
ShopifyCartOperation, // Needed for live getCart
ShopifyCollection,
ShopifyCollectionOperation,
ShopifyCollectionProductsOperation,
ShopifyCollectionsOperation,
ShopifyCreateCartOperation,
ShopifyMenuOperation,
// ShopifyMenuOperation, // No longer needed for dummy getMenu
ShopifyPageOperation,
ShopifyPagesOperation,
ShopifyProduct,
ShopifyProductOperation,
ShopifyProductRecommendationsOperation,
ShopifyProductRecommendationsOperation, // Needed for live getProductRecommendations
ShopifyProductsOperation,
ShopifyRemoveFromCartOperation,
ShopifyUpdateCartOperation
ShopifyUpdateCartOperation,
Money,
ProductOption,
ProductVariant,
SEO
} from './types';
const domain = process.env.SHOPIFY_STORE_DOMAIN
@ -213,17 +218,88 @@ const reshapeProducts = (products: ShopifyProduct[]) => {
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> {
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>({
query: createCartMutation
});
return reshapeCart(res.body.data.cartCreate.cart);
}
export async function addToCart(
lines: { merchandiseId: string; quantity: number }[]
): 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 res = await shopifyFetch<ShopifyAddToCartOperation>({
query: addToCartMutation,
@ -236,6 +312,12 @@ export async function addToCart(
}
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 res = await shopifyFetch<ShopifyRemoveFromCartOperation>({
query: removeFromCartMutation,
@ -244,13 +326,18 @@ export async function removeFromCart(lineIds: string[]): Promise<Cart> {
lineIds
}
});
return reshapeCart(res.body.data.cartLinesRemove.cart);
}
export async function updateCart(
lines: { id: string; merchandiseId: string; quantity: number }[]
): 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 res = await shopifyFetch<ShopifyUpdateCartOperation>({
query: editCartItemsMutation,
@ -259,34 +346,76 @@ export async function updateCart(
lines
}
});
return reshapeCart(res.body.data.cartLinesUpdate.cart);
}
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) {
return undefined;
}
// Assuming getCartQuery is available/re-imported if this path is ever taken.
// For now, the import is commented out. To make this path work, it would need to be restored.
const res = await shopifyFetch<ShopifyCartOperation>({
query: getCartQuery,
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) {
return undefined;
}
return reshapeCart(res.body.data.cart);
}
export async function getCollection(
handle: string
): 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);
cacheLife('days');
@ -309,7 +438,66 @@ export async function getCollectionProducts({
reverse?: boolean;
sortKey?: string;
}): 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);
cacheLife('days');
@ -333,7 +521,38 @@ export async function getCollectionProducts({
}
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);
cacheLife('days');
@ -353,8 +572,6 @@ export async function getCollections(): Promise<Collection[]> {
path: '/search',
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(
(collection) => !collection.handle.startsWith('hidden')
)
@ -364,29 +581,46 @@ export async function getCollections(): Promise<Collection[]> {
}
export async function getMenu(handle: string): Promise<Menu[]> {
'use cache';
cacheTag(TAGS.collections);
cacheLife('days');
// This function is fully dummified, no 'use cache'
console.log(`getMenu called with handle: ${handle} - returning dummy menu data.`);
const res = await shopifyFetch<ShopifyMenuOperation>({
query: getMenuQuery,
variables: {
handle
}
});
const dummyMenu: Menu[] = [
{ title: 'Home', path: '/' },
{ title: 'All Products', path: '/search' },
{ 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 (
res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({
title: item.title,
path: item.url
.replace(domain, '')
.replace('/collections', '/search')
.replace('/pages', '')
})) || []
);
await new Promise(resolve => setTimeout(resolve, 50));
return dummyMenu;
}
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>({
query: getPageQuery,
variables: { handle }
@ -396,6 +630,37 @@ export async function getPage(handle: string): 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>({
query: getPagesQuery
});
@ -404,7 +669,71 @@ export async function getPages(): Promise<Page[]> {
}
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);
cacheLife('days');
@ -421,7 +750,16 @@ export async function getProduct(handle: string): Promise<Product | undefined> {
export async function getProductRecommendations(
productId: string
): 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);
cacheLife('days');
@ -444,7 +782,48 @@ export async function getProducts({
reverse?: boolean;
sortKey?: string;
}): 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);
cacheLife('days');
@ -460,10 +839,7 @@ export async function getProducts({
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> {
// We always need to respond with a 200 status code to Shopify,
// otherwise it will continue to retry the request.
const collectionWebhooks = [
'collections/create',
'collections/delete',
@ -485,7 +861,6 @@ export async function revalidate(req: NextRequest): Promise<NextResponse> {
}
if (!isCollectionUpdate && !isProductUpdate) {
// We don't need to revalidate anything for any other topics.
return NextResponse.json({ status: 200 });
}

View File

@ -1,3 +1,4 @@
process.env.COMMERCE_PROVIDER = process.env.COMMERCE_PROVIDER || 'local';
export default {
experimental: {
ppr: true,

2
redeploy.txt Normal file
View File

@ -0,0 +1,2 @@
git commit --allow-empty -m "trigger deploy"
git push