Initialized repository for project Auntysparkles.com clone
Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
|
|
@ -0,0 +1,27 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Auntysparkles.com clone
|
||||
|
||||
*Automatically synced with your [v0.app](https://v0.app) deployments*
|
||||
|
||||
[](https://vercel.com/jeff-emmetts-projects/v0-auntysparkles-com-clone)
|
||||
[](https://v0.app/chat/projects/kBpDOC0alDo)
|
||||
|
||||
## Overview
|
||||
|
||||
This repository will stay in sync with your deployed chats on [v0.app](https://v0.app).
|
||||
Any changes you make to your deployed app will be automatically pushed to this repository from [v0.app](https://v0.app).
|
||||
|
||||
## Deployment
|
||||
|
||||
Your project is live at:
|
||||
|
||||
**[https://vercel.com/jeff-emmetts-projects/v0-auntysparkles-com-clone](https://vercel.com/jeff-emmetts-projects/v0-auntysparkles-com-clone)**
|
||||
|
||||
## Build your app
|
||||
|
||||
Continue building your app on:
|
||||
|
||||
**[https://v0.app/chat/projects/kBpDOC0alDo](https://v0.app/chat/projects/kBpDOC0alDo)**
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Create and modify your project using [v0.app](https://v0.app)
|
||||
2. Deploy your chats from the v0 interface
|
||||
3. Changes are automatically pushed to this repository
|
||||
4. Vercel deploys the latest version from this repository
|
||||
|
|
@ -0,0 +1,212 @@
|
|||
import Image from "next/image"
|
||||
import SiteFooter from "@/components/site-footer"
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Header */}
|
||||
<header className="bg-[#e8e4d3] py-4">
|
||||
<div className="container mx-auto px-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<a href="/" className="flex items-center">
|
||||
<Image
|
||||
src="/images/logo-black-white.png"
|
||||
alt="Aunty Sparkles Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className="mr-4"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-8">
|
||||
<nav>
|
||||
<ul className="flex space-x-8 text-xl font-bold">
|
||||
<li>
|
||||
<a href="/about" className="text-blue-600 hover:text-blue-800 transition-colors">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/gallery" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<a
|
||||
href="/shop"
|
||||
className="bg-pink-500 text-white px-6 py-2 font-semibold hover:bg-pink-400 transition-colors rounded-full shadow-lg"
|
||||
>
|
||||
SHOP NOW
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="hero-bg py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-2xl">
|
||||
<h1 className="text-5xl font-bold text-white mb-4">About Aunty Sparkles</h1>
|
||||
<div className="w-16 h-1 bg-yellow-400 mb-6"></div>
|
||||
<p className="text-lg text-gray-300">The heart behind Aunty Sparkles</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main Content */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-start max-w-6xl">
|
||||
<div className="w-2/3 pr-12">
|
||||
<p className="text-gray-300 leading-relaxed mb-6">
|
||||
Hi, I'm Kiersten, the creator behind Aunty Sparkles! I've always been drawn to nature, creativity, and
|
||||
the little details that make life feel magical. I love wandering through thrift stores and vintage
|
||||
shops, finding inspiration in the textures, colors, and shapes of the natural world. I'm also fascinated
|
||||
by handmade fashion—the freedom, the playfulness, and the way people express themselves as boldly and
|
||||
beautifully.
|
||||
</p>
|
||||
|
||||
<p className="text-gray-300 leading-relaxed mb-6">
|
||||
Aunty Sparkles grew out of my love for giving forgotten things new life. I adore hunting for thrifted
|
||||
treasures, repurposing materials, and turning them into something completely unique, whether it's a
|
||||
hand-painted denim jacket, a piece of whimsical mushroom art, or a watercolor painting that captures a
|
||||
bit of woodland wonder. Each creation is made with intention and care.
|
||||
</p>
|
||||
|
||||
<p className="text-gray-300 leading-relaxed mb-12">
|
||||
This brand is my way of blending sustainability, creativity, and a love for the earth into something
|
||||
that can be shared. My hope is that when you wear or see an Aunty Sparkles piece, you remember you are
|
||||
unique and beautiful, and that beauty can be found everywhere—especially in the things we choose to give
|
||||
a second life.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<Image
|
||||
src="/images/about-photo.jpeg"
|
||||
alt="Person sitting with decorative mushrooms in forest"
|
||||
width={400}
|
||||
height={500}
|
||||
className="rounded-lg w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Why Upcycling Matters */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-start">
|
||||
<div className="w-1/3 pr-8">
|
||||
<Image
|
||||
src="/images/textile-waste.webp"
|
||||
alt="Mountain of textile waste showing environmental impact"
|
||||
width={300}
|
||||
height={300}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-2/3">
|
||||
<h2 className="text-3xl font-bold text-white mb-6">Why Upcycling Matters</h2>
|
||||
<p className="text-gray-300 leading-relaxed mb-8">
|
||||
The fashion industry produces over 100 billion garments annually, yet we wear a million tonnes of
|
||||
clothing and up to landfills annually. Most of these textiles take decades—or even centuries—to break
|
||||
down, releasing harmful chemicals and microplastics into the environment. Upcycling helps change that by
|
||||
giving forgotten items a second chance at life, and cutting down the demand for new production. Every
|
||||
upcycled piece keeps fabric out of a landfill and turns it into something beautiful and
|
||||
meaningful—proving that sustainability and style can go hand in hand.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm text-gray-300 mb-1">
|
||||
<span>60% of fast fashion is discarded within the year</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div className="bg-pink-500 h-2 rounded-full" style={{ width: "60%" }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-sm text-gray-300 mb-1">
|
||||
<span>85% of all discarded clothing ends up in landfills</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div className="bg-pink-500 h-2 rounded-full" style={{ width: "85%" }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-sm text-gray-300 mb-1">
|
||||
<span>35% of microplastics in the ocean are from textiles</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div className="bg-pink-500 h-2 rounded-full" style={{ width: "35%" }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex justify-between text-sm text-gray-300 mb-1">
|
||||
<span>20% of global wastewater is from dying textiles</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2">
|
||||
<div className="bg-pink-500 h-2 rounded-full" style={{ width: "20%" }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Finding Treasure */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-start">
|
||||
<div className="w-2/3 pr-8">
|
||||
<h2 className="text-3xl font-bold text-white mb-6">Finding Treasure in the Forgotten</h2>
|
||||
<p className="text-gray-300 leading-relaxed mb-8">
|
||||
Aunty Sparkles creations start with a hunt through thrift stores and vintage shops, where I search for
|
||||
overlooked and discarded items just for a second chance. Denim jackets have become my favorite
|
||||
canvas—they're durable, timeless, and full of character. I look for pieces that have lived a life: faded
|
||||
denim, vintage cuts, or forgotten prints. Even pieces that are stained or damaged can make beautiful
|
||||
fabrics and textures worth saving. By rescuing these forgotten textiles, they are transformed into
|
||||
something new, unique, and full of character, keeping them out of landfills and giving them a second
|
||||
life.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<Image
|
||||
src="/images/thrift-store.webp"
|
||||
alt="Thrift store exterior showing secondhand shopping"
|
||||
width={300}
|
||||
height={300}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map */}
|
||||
<div className="mt-16">
|
||||
<Image
|
||||
src="/placeholder.svg?height=400&width=800&text=Location+Map"
|
||||
alt="Location map"
|
||||
width={800}
|
||||
height={400}
|
||||
className="rounded-lg mx-auto"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<SiteFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { email } = await request.json()
|
||||
|
||||
if (!email || !email.includes('@')) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Please provide a valid email address'
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const MAILCHIMP_API_KEY = process.env.MAILCHIMP_API_KEY || '6951b8172cace4f040d8717c2ff9d77d-us4'
|
||||
const AUDIENCE_ID = process.env.MAILCHIMP_AUDIENCE_ID || '28f1945bb4'
|
||||
const DATACENTER = MAILCHIMP_API_KEY.split('-')[1] // Extract 'us4' from API key
|
||||
|
||||
if (!MAILCHIMP_API_KEY || !AUDIENCE_ID) {
|
||||
console.error('Missing Mailchimp configuration')
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Mailing list service is not configured'
|
||||
}, { status: 500 })
|
||||
}
|
||||
|
||||
const url = `https://${DATACENTER}.api.mailchimp.com/3.0/lists/${AUDIENCE_ID}/members`
|
||||
|
||||
console.log('Subscribing email to Mailchimp:', email)
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${MAILCHIMP_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email_address: email,
|
||||
status: 'subscribed',
|
||||
tags: ['website-footer'],
|
||||
merge_fields: {
|
||||
SOURCE: 'Website Footer'
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Successfully subscribed:', email)
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: 'Successfully subscribed to mailing list!'
|
||||
})
|
||||
} else {
|
||||
console.error('Mailchimp API error:', data)
|
||||
|
||||
// Handle specific Mailchimp errors
|
||||
if (data.title === 'Member Exists') {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'This email is already subscribed to our mailing list!'
|
||||
})
|
||||
} else if (data.title === 'Invalid Resource') {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Please provide a valid email address.'
|
||||
})
|
||||
} else if (data.title === 'Forbidden') {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Invalid API credentials. Please contact support.'
|
||||
})
|
||||
} else {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: data.detail || 'Failed to subscribe. Please try again.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Mailchimp subscription error:', error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: 'Something went wrong. Please try again later.'
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { getSquareClient } from "@/lib/square-client"
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: { imageId: string } }
|
||||
) {
|
||||
try {
|
||||
const client = await getSquareClient()
|
||||
const catalogApi = client.catalogApi
|
||||
const response = await catalogApi.retrieveCatalogObject(params.imageId, true)
|
||||
|
||||
if (response.result.object?.type === "IMAGE" && response.result.object.imageData?.url) {
|
||||
// Redirect to the actual Square image URL
|
||||
return Response.redirect(response.result.object.imageData.url, 302)
|
||||
}
|
||||
|
||||
// Fallback to placeholder
|
||||
return Response.redirect("/placeholder.svg?height=400&width=400&text=No+Image", 302)
|
||||
|
||||
} catch (error) {
|
||||
console.error("Square image retrieval error:", error)
|
||||
return Response.redirect("/placeholder.svg?height=400&width=400&text=Error", 302)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
import { getSquareClient, getSquareLocationId, checkSquareConnection } from "@/lib/square-client"
|
||||
import type { SquareProduct, SquareCategory } from "@/lib/square-types"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
console.log("=== Square Catalog API Request ===")
|
||||
|
||||
// First, check if Square is properly configured
|
||||
const connectionCheck = await checkSquareConnection()
|
||||
if (!connectionCheck.success) {
|
||||
console.error("Square connection failed:", connectionCheck.error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: "Square API connection failed",
|
||||
details: connectionCheck.error,
|
||||
products: [],
|
||||
categories: []
|
||||
}, { status: 500 })
|
||||
}
|
||||
|
||||
console.log("Square connection verified:", connectionCheck.message)
|
||||
|
||||
const { searchParams } = new URL(request.url)
|
||||
const categoryId = searchParams.get('categoryId')
|
||||
const includeInventory = searchParams.get('includeInventory') === 'true'
|
||||
|
||||
console.log("Category filter:", categoryId)
|
||||
console.log("Include inventory:", includeInventory)
|
||||
|
||||
const client = await getSquareClient()
|
||||
const locationId = getSquareLocationId()
|
||||
const catalogApi = client.catalogApi
|
||||
|
||||
// Build search request
|
||||
const searchRequest: any = {
|
||||
objectTypes: ["ITEM"],
|
||||
includeDeletedObjects: false,
|
||||
includeRelatedObjects: true,
|
||||
}
|
||||
|
||||
// Add category filter if specified
|
||||
if (categoryId) {
|
||||
searchRequest.query = {
|
||||
exactQuery: {
|
||||
attributeName: "category_id",
|
||||
attributeValue: categoryId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Searching catalog with request:", searchRequest)
|
||||
|
||||
// Search catalog items
|
||||
const catalogResponse = await catalogApi.searchCatalogObjects(searchRequest)
|
||||
|
||||
console.log("Catalog response received:", {
|
||||
objectCount: catalogResponse.result.objects?.length || 0,
|
||||
hasObjects: !!catalogResponse.result.objects
|
||||
})
|
||||
|
||||
if (!catalogResponse.result.objects || catalogResponse.result.objects.length === 0) {
|
||||
return Response.json({
|
||||
success: true,
|
||||
products: [],
|
||||
categories: [],
|
||||
message: "No products found in Square catalog. Add products in your Square Dashboard."
|
||||
})
|
||||
}
|
||||
|
||||
// Separate items and categories
|
||||
const items = catalogResponse.result.objects.filter(obj => obj.type === "ITEM")
|
||||
const categories = catalogResponse.result.objects.filter(obj => obj.type === "CATEGORY")
|
||||
|
||||
console.log("Found items:", items.length, "categories:", categories.length)
|
||||
|
||||
// Process categories
|
||||
const processedCategories: SquareCategory[] = categories.map(cat => ({
|
||||
id: cat.id!,
|
||||
name: cat.categoryData?.name || "Unnamed Category",
|
||||
imageUrl: cat.categoryData?.imageIds?.[0]
|
||||
? `/api/square/catalog/image/${cat.categoryData.imageIds[0]}`
|
||||
: undefined
|
||||
}))
|
||||
|
||||
// Get inventory data if requested
|
||||
let inventoryData: Record<string, number> = {}
|
||||
if (includeInventory) {
|
||||
try {
|
||||
const variationIds = items.flatMap(item =>
|
||||
item.itemData?.variations?.map(v => v.id!) || []
|
||||
).filter(Boolean)
|
||||
|
||||
if (variationIds.length > 0) {
|
||||
console.log("Fetching inventory for", variationIds.length, "variations")
|
||||
const inventoryApi = client.inventoryApi
|
||||
const inventoryResponse = await inventoryApi.batchRetrieveInventoryCounts({
|
||||
catalogObjectIds: variationIds,
|
||||
locationIds: [locationId]
|
||||
})
|
||||
|
||||
inventoryResponse.result.counts?.forEach(count => {
|
||||
if (count.catalogObjectId) {
|
||||
inventoryData[count.catalogObjectId] = parseInt(count.quantity || "0")
|
||||
}
|
||||
})
|
||||
console.log("Inventory data loaded for", Object.keys(inventoryData).length, "items")
|
||||
}
|
||||
} catch (inventoryError) {
|
||||
console.warn("Failed to fetch inventory:", inventoryError)
|
||||
// Continue without inventory data
|
||||
}
|
||||
}
|
||||
|
||||
// Process items into products
|
||||
const products: SquareProduct[] = items
|
||||
.filter(item => {
|
||||
const hasVariations = item.itemData?.variations && item.itemData.variations.length > 0
|
||||
if (!hasVariations) {
|
||||
console.warn("Skipping item without variations:", item.id, item.itemData?.name)
|
||||
}
|
||||
return hasVariations
|
||||
})
|
||||
.map(item => {
|
||||
const itemData = item.itemData!
|
||||
const primaryVariation = itemData.variations![0]
|
||||
const variationData = primaryVariation.itemVariationData!
|
||||
const price = variationData.priceMoney
|
||||
|
||||
// Get inventory for primary variation
|
||||
const inventory = inventoryData[primaryVariation.id!] || 0
|
||||
|
||||
// Process all variations
|
||||
const variations = itemData.variations!.map(variation => {
|
||||
const vData = variation.itemVariationData!
|
||||
const vPrice = vData.priceMoney
|
||||
return {
|
||||
id: variation.id!,
|
||||
name: vData.name || itemData.name || "Default",
|
||||
price: vPrice ? Number(vPrice.amount) / 100 : 0,
|
||||
currency: vPrice?.currency || "USD",
|
||||
inventory: inventoryData[variation.id!] || 0,
|
||||
sku: vData.sku
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
id: item.id!,
|
||||
variationId: primaryVariation.id!,
|
||||
name: itemData.name || "Unnamed Product",
|
||||
description: itemData.description || "",
|
||||
price: price ? Number(price.amount) / 100 : 0,
|
||||
currency: price?.currency || "USD",
|
||||
imageUrl: itemData.imageIds?.[0]
|
||||
? `/api/square/catalog/image/${itemData.imageIds[0]}`
|
||||
: "/placeholder.svg?height=400&width=400&text=Product+Image",
|
||||
inventory,
|
||||
category: itemData.categoryId || "uncategorized",
|
||||
categoryName: categories.find(c => c.id === itemData.categoryId)?.categoryData?.name,
|
||||
isAvailable: inventory > 0,
|
||||
variations
|
||||
}
|
||||
})
|
||||
|
||||
console.log("Successfully processed", products.length, "products")
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
products,
|
||||
categories: processedCategories,
|
||||
message: `Found ${products.length} products and ${processedCategories.length} categories`
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error("=== Square Catalog Error ===")
|
||||
console.error("Error type:", error?.constructor?.name)
|
||||
console.error("Error message:", error instanceof Error ? error.message : String(error))
|
||||
console.error("Stack trace:", error instanceof Error ? error.stack : "No stack")
|
||||
|
||||
// Return detailed error information
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown catalog error",
|
||||
details: {
|
||||
type: error?.constructor?.name || "Unknown",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
suggestion: "Check your Square API credentials and ensure products exist in your Square catalog"
|
||||
},
|
||||
products: [],
|
||||
categories: []
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { Client, Environment } from "squareup"
|
||||
|
||||
const client = new Client({
|
||||
accessToken: process.env.SQUARE_ACCESS_TOKEN,
|
||||
environment: process.env.SQUARE_ENVIRONMENT === "production" ? Environment.Production : Environment.Sandbox,
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { items, customerInfo } = await request.json()
|
||||
|
||||
const ordersApi = client.ordersApi
|
||||
|
||||
const orderRequest = {
|
||||
order: {
|
||||
locationId: process.env.SQUARE_LOCATION_ID,
|
||||
lineItems: items.map((item: any) => ({
|
||||
name: item.name,
|
||||
quantity: item.quantity.toString(),
|
||||
basePriceMoney: {
|
||||
amount: BigInt(item.price * 100),
|
||||
currency: "USD",
|
||||
},
|
||||
note: item.description || "",
|
||||
})),
|
||||
metadata: {
|
||||
customerName: customerInfo.name,
|
||||
customerEmail: customerInfo.email,
|
||||
customerPhone: customerInfo.phone || "",
|
||||
shippingAddress: JSON.stringify(customerInfo.address || {}),
|
||||
},
|
||||
},
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
}
|
||||
|
||||
const response = await ordersApi.createOrder(orderRequest)
|
||||
|
||||
if (response.result.order) {
|
||||
return Response.json({
|
||||
success: true,
|
||||
order: response.result.order,
|
||||
})
|
||||
} else {
|
||||
throw new Error("Failed to create order")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Square order creation error:", error)
|
||||
return Response.json({ success: false, error: "Failed to create order" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { Client, Environment } from "squareup"
|
||||
|
||||
const client = new Client({
|
||||
accessToken: process.env.SQUARE_ACCESS_TOKEN,
|
||||
environment: process.env.SQUARE_ENVIRONMENT === "production" ? Environment.Production : Environment.Sandbox,
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { sourceId, orderId, amount, customerInfo } = await request.json()
|
||||
|
||||
const paymentsApi = client.paymentsApi
|
||||
|
||||
const paymentRequest = {
|
||||
sourceId,
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
amountMoney: {
|
||||
amount: BigInt(amount * 100),
|
||||
currency: "USD",
|
||||
},
|
||||
orderId,
|
||||
buyerEmailAddress: customerInfo.email,
|
||||
note: `Payment for Aunty Sparkles order - ${customerInfo.name}`,
|
||||
autocomplete: true,
|
||||
}
|
||||
|
||||
const response = await paymentsApi.createPayment(paymentRequest)
|
||||
|
||||
if (response.result.payment) {
|
||||
return Response.json({
|
||||
success: true,
|
||||
payment: response.result.payment,
|
||||
})
|
||||
} else {
|
||||
throw new Error("Payment failed")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Square payment error:", error)
|
||||
return Response.json({ success: false, error: "Payment failed" }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export async function GET() {
|
||||
// This will help us see what's actually happening with your environment variables
|
||||
return Response.json({
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
hasSquareToken: !!process.env.SQUARE_ACCESS_TOKEN,
|
||||
tokenLength: process.env.SQUARE_ACCESS_TOKEN?.length || 0,
|
||||
tokenStart: process.env.SQUARE_ACCESS_TOKEN?.substring(0, 10) || "none",
|
||||
hasLocationId: !!process.env.SQUARE_LOCATION_ID,
|
||||
locationLength: process.env.SQUARE_LOCATION_ID?.length || 0,
|
||||
locationStart: process.env.SQUARE_LOCATION_ID?.substring(0, 5) || "none",
|
||||
environment: process.env.SQUARE_ENVIRONMENT || "not set",
|
||||
allEnvKeys: Object.keys(process.env).filter((key) => key.includes("SQUARE")),
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
export async function GET() {
|
||||
// This endpoint will help you check your environment variables without exposing them
|
||||
const envCheck = {
|
||||
SQUARE_ACCESS_TOKEN: {
|
||||
exists: !!process.env.SQUARE_ACCESS_TOKEN,
|
||||
length: process.env.SQUARE_ACCESS_TOKEN?.length || 0,
|
||||
startsWithCorrectPrefix: process.env.SQUARE_ACCESS_TOKEN?.startsWith("EAAA") || false,
|
||||
isPlaceholder: process.env.SQUARE_ACCESS_TOKEN === "your_square_access_token_here",
|
||||
},
|
||||
SQUARE_LOCATION_ID: {
|
||||
exists: !!process.env.SQUARE_LOCATION_ID,
|
||||
length: process.env.SQUARE_LOCATION_ID?.length || 0,
|
||||
startsWithL: process.env.SQUARE_LOCATION_ID?.startsWith("L") || false,
|
||||
isPlaceholder: process.env.SQUARE_LOCATION_ID === "your_square_location_id_here",
|
||||
},
|
||||
SQUARE_ENVIRONMENT: {
|
||||
exists: !!process.env.SQUARE_ENVIRONMENT,
|
||||
value: process.env.SQUARE_ENVIRONMENT || "not set",
|
||||
isValid: ["sandbox", "production"].includes(process.env.SQUARE_ENVIRONMENT || ""),
|
||||
},
|
||||
NEXT_PUBLIC_SQUARE_APPLICATION_ID: {
|
||||
exists: !!process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID,
|
||||
length: process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID?.length || 0,
|
||||
isPlaceholder: process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID === "your_square_application_id_here",
|
||||
},
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
message: "Environment variables check (values hidden for security)",
|
||||
variables: envCheck,
|
||||
recommendations: {
|
||||
accessToken: !envCheck.SQUARE_ACCESS_TOKEN.exists
|
||||
? "Missing SQUARE_ACCESS_TOKEN"
|
||||
: envCheck.SQUARE_ACCESS_TOKEN.isPlaceholder
|
||||
? "Still using placeholder value"
|
||||
: !envCheck.SQUARE_ACCESS_TOKEN.startsWithCorrectPrefix
|
||||
? "Token should start with 'EAAA'"
|
||||
: "✅ Looks good",
|
||||
locationId: !envCheck.SQUARE_LOCATION_ID.exists
|
||||
? "Missing SQUARE_LOCATION_ID"
|
||||
: envCheck.SQUARE_LOCATION_ID.isPlaceholder
|
||||
? "Still using placeholder value"
|
||||
: !envCheck.SQUARE_LOCATION_ID.startsWithL
|
||||
? "Location ID should start with 'L'"
|
||||
: "✅ Looks good",
|
||||
environment: !envCheck.SQUARE_ENVIRONMENT.isValid ? "Should be 'sandbox' or 'production'" : "✅ Looks good",
|
||||
applicationId: !envCheck.NEXT_PUBLIC_SQUARE_APPLICATION_ID.exists
|
||||
? "Missing NEXT_PUBLIC_SQUARE_APPLICATION_ID"
|
||||
: envCheck.NEXT_PUBLIC_SQUARE_APPLICATION_ID.isPlaceholder
|
||||
? "Still using placeholder value"
|
||||
: "✅ Looks good",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { checkSquareConnection, getSquareLocationId, resetSquareClient } from "@/lib/square-client"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
console.log("=== Square Health Check ===")
|
||||
|
||||
// Reset client to force fresh initialization
|
||||
const { searchParams } = new URL(request.url)
|
||||
if (searchParams.get('reset') === 'true') {
|
||||
resetSquareClient()
|
||||
console.log("Square client reset")
|
||||
}
|
||||
|
||||
// Check environment variables
|
||||
const envCheck = {
|
||||
SQUARE_ACCESS_TOKEN: {
|
||||
exists: !!process.env.SQUARE_ACCESS_TOKEN,
|
||||
length: process.env.SQUARE_ACCESS_TOKEN?.length || 0,
|
||||
startsWithEAAA: process.env.SQUARE_ACCESS_TOKEN?.startsWith("EAAA") || false,
|
||||
},
|
||||
SQUARE_LOCATION_ID: {
|
||||
exists: !!process.env.SQUARE_LOCATION_ID,
|
||||
length: process.env.SQUARE_LOCATION_ID?.length || 0,
|
||||
startsWithL: process.env.SQUARE_LOCATION_ID?.startsWith("L") || false,
|
||||
},
|
||||
SQUARE_ENVIRONMENT: {
|
||||
exists: !!process.env.SQUARE_ENVIRONMENT,
|
||||
value: process.env.SQUARE_ENVIRONMENT || "not set",
|
||||
isValid: ["sandbox", "production"].includes(process.env.SQUARE_ENVIRONMENT || ""),
|
||||
},
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
}
|
||||
|
||||
console.log("Environment check:", envCheck)
|
||||
|
||||
// Test Square connection
|
||||
const connectionResult = await checkSquareConnection()
|
||||
console.log("Connection result:", connectionResult)
|
||||
|
||||
return Response.json({
|
||||
success: connectionResult.success,
|
||||
environment: envCheck,
|
||||
connection: connectionResult,
|
||||
timestamp: new Date().toISOString(),
|
||||
recommendations: {
|
||||
accessToken: !envCheck.SQUARE_ACCESS_TOKEN.exists
|
||||
? "❌ Missing SQUARE_ACCESS_TOKEN"
|
||||
: !envCheck.SQUARE_ACCESS_TOKEN.startsWithEAAA
|
||||
? "⚠️ Token should start with 'EAAA'"
|
||||
: "✅ Access token looks good",
|
||||
locationId: !envCheck.SQUARE_LOCATION_ID.exists
|
||||
? "❌ Missing SQUARE_LOCATION_ID"
|
||||
: !envCheck.SQUARE_LOCATION_ID.startsWithL
|
||||
? "⚠️ Location ID should start with 'L'"
|
||||
: "✅ Location ID looks good",
|
||||
environment: !envCheck.SQUARE_ENVIRONMENT.isValid
|
||||
? "⚠️ Should be 'sandbox' or 'production'"
|
||||
: "✅ Environment is valid",
|
||||
sdkImport: connectionResult.success
|
||||
? "✅ Square SDK imported successfully"
|
||||
: "❌ Square SDK import failed - check if 'squareup' package is installed"
|
||||
}
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error("Health check failed:", error)
|
||||
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Health check failed",
|
||||
timestamp: new Date().toISOString(),
|
||||
details: {
|
||||
type: error?.constructor?.name || "Unknown",
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
}
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { getSquareClient, getSquareLocationId } from "@/lib/square-client"
|
||||
|
||||
export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const catalogObjectId = searchParams.get("catalogObjectId")
|
||||
|
||||
if (!catalogObjectId) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: "catalogObjectId parameter is required"
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const client = await getSquareClient()
|
||||
const locationId = getSquareLocationId()
|
||||
const inventoryApi = client.inventoryApi
|
||||
|
||||
const response = await inventoryApi.batchRetrieveInventoryCounts({
|
||||
catalogObjectIds: [catalogObjectId],
|
||||
locationIds: [locationId],
|
||||
})
|
||||
|
||||
const inventory = response.result.counts?.[0]
|
||||
const quantity = inventory ? parseInt(inventory.quantity || "0") : 0
|
||||
const state = inventory?.state || "NONE"
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
quantity,
|
||||
state,
|
||||
isAvailable: quantity > 0 && state === "IN_STOCK"
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error("Square inventory error:", error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Inventory fetch failed",
|
||||
quantity: 0,
|
||||
isAvailable: false
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
import { getSquareClient, getSquareLocationId } from "@/lib/square-client"
|
||||
import type { CustomerInfo, CartItem } from "@/lib/square-types"
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const { items, customerInfo }: { items: CartItem[], customerInfo: CustomerInfo } = await request.json()
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: "No items provided"
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
if (!customerInfo.name || !customerInfo.email) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: "Customer name and email are required"
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const client = await getSquareClient()
|
||||
const locationId = getSquareLocationId()
|
||||
const ordersApi = client.ordersApi
|
||||
|
||||
// Calculate total amount
|
||||
const totalAmount = items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
|
||||
|
||||
// Create order request
|
||||
const orderRequest = {
|
||||
order: {
|
||||
locationId: locationId,
|
||||
lineItems: items.map(item => ({
|
||||
catalogObjectId: item.variationId,
|
||||
quantity: item.quantity.toString(),
|
||||
name: item.name,
|
||||
note: item.description || "",
|
||||
basePriceMoney: {
|
||||
amount: BigInt(Math.round(item.price * 100)), // Convert to cents
|
||||
currency: "USD",
|
||||
},
|
||||
})),
|
||||
metadata: {
|
||||
customerName: customerInfo.name,
|
||||
customerEmail: customerInfo.email,
|
||||
customerPhone: customerInfo.phone || "",
|
||||
...(customerInfo.address && {
|
||||
shippingAddress: JSON.stringify(customerInfo.address)
|
||||
})
|
||||
},
|
||||
state: "OPEN",
|
||||
totalMoney: {
|
||||
amount: BigInt(Math.round(totalAmount * 100)),
|
||||
currency: "USD"
|
||||
}
|
||||
},
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
}
|
||||
|
||||
console.log("Creating Square order:", {
|
||||
itemCount: items.length,
|
||||
totalAmount,
|
||||
customerEmail: customerInfo.email
|
||||
})
|
||||
|
||||
const response = await ordersApi.createOrder(orderRequest)
|
||||
|
||||
if (response.result.order) {
|
||||
console.log("Order created successfully:", response.result.order.id)
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
order: {
|
||||
id: response.result.order.id,
|
||||
state: response.result.order.state,
|
||||
totalAmount: totalAmount,
|
||||
lineItems: response.result.order.lineItems?.length || 0
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error("Order creation failed - no order returned")
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Square order creation error:", error)
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Order creation failed"
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { getSquareClient, getSquareLocationId } from "@/lib/square-client"
|
||||
import type { PaymentRequest } from "@/lib/square-types"
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const paymentData: PaymentRequest = await request.json()
|
||||
|
||||
if (!paymentData.sourceId) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: "Payment source ID is required"
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
if (!paymentData.amount || paymentData.amount <= 0) {
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: "Valid payment amount is required"
|
||||
}, { status: 400 })
|
||||
}
|
||||
|
||||
const client = await getSquareClient()
|
||||
const locationId = getSquareLocationId()
|
||||
const paymentsApi = client.paymentsApi
|
||||
|
||||
// Create payment request
|
||||
const paymentRequest = {
|
||||
sourceId: paymentData.sourceId,
|
||||
idempotencyKey: crypto.randomUUID(),
|
||||
amountMoney: {
|
||||
amount: BigInt(Math.round(paymentData.amount * 100)), // Convert to cents
|
||||
currency: paymentData.currency || "USD",
|
||||
},
|
||||
locationId: locationId,
|
||||
...(paymentData.orderId && { orderId: paymentData.orderId }),
|
||||
buyerEmailAddress: paymentData.customerInfo.email,
|
||||
note: `Aunty Sparkles order - ${paymentData.customerInfo.name}`,
|
||||
autocomplete: true,
|
||||
acceptPartialAuthorization: false,
|
||||
}
|
||||
|
||||
console.log("Processing Square payment:", {
|
||||
amount: paymentData.amount,
|
||||
currency: paymentData.currency,
|
||||
customerEmail: paymentData.customerInfo.email,
|
||||
orderId: paymentData.orderId
|
||||
})
|
||||
|
||||
const response = await paymentsApi.createPayment(paymentRequest)
|
||||
|
||||
if (response.result.payment) {
|
||||
const payment = response.result.payment
|
||||
|
||||
console.log("Payment processed successfully:", {
|
||||
paymentId: payment.id,
|
||||
status: payment.status,
|
||||
amount: payment.amountMoney?.amount
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
payment: {
|
||||
id: payment.id,
|
||||
status: payment.status,
|
||||
amount: paymentData.amount,
|
||||
currency: paymentData.currency,
|
||||
receiptUrl: payment.receiptUrl,
|
||||
orderId: payment.orderId
|
||||
}
|
||||
})
|
||||
} else {
|
||||
throw new Error("Payment processing failed - no payment returned")
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Square payment error:", error)
|
||||
|
||||
// Handle specific Square API errors
|
||||
let errorMessage = "Payment processing failed"
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("CARD_DECLINED")) {
|
||||
errorMessage = "Your card was declined. Please try a different payment method."
|
||||
} else if (error.message.includes("INSUFFICIENT_FUNDS")) {
|
||||
errorMessage = "Insufficient funds. Please try a different payment method."
|
||||
} else if (error.message.includes("INVALID_CARD")) {
|
||||
errorMessage = "Invalid card information. Please check your card details."
|
||||
} else {
|
||||
errorMessage = error.message
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({
|
||||
success: false,
|
||||
error: errorMessage
|
||||
}, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
export async function GET() {
|
||||
// Simple test to see if environment variables are working at all
|
||||
return Response.json({
|
||||
message: "Environment variable test",
|
||||
nodeEnv: process.env.NODE_ENV,
|
||||
hasSquareToken: !!process.env.SQUARE_ACCESS_TOKEN,
|
||||
tokenPreview: process.env.SQUARE_ACCESS_TOKEN
|
||||
? process.env.SQUARE_ACCESS_TOKEN.substring(0, 10) + "..."
|
||||
: "not found",
|
||||
hasLocationId: !!process.env.SQUARE_LOCATION_ID,
|
||||
locationPreview: process.env.SQUARE_LOCATION_ID
|
||||
? process.env.SQUARE_LOCATION_ID.substring(0, 5) + "..."
|
||||
: "not found",
|
||||
environment: process.env.SQUARE_ENVIRONMENT || "not set",
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import type React from "react"
|
||||
import Image from "next/image"
|
||||
import SiteFooter from "@/components/site-footer"
|
||||
|
||||
export default function Contact() {
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
message: "",
|
||||
})
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
// Handle form submission
|
||||
console.log("Form submitted:", formData)
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Header */}
|
||||
<header className="bg-[#e8e4d3] py-4">
|
||||
<div className="container mx-auto px-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<a href="/" className="flex items-center">
|
||||
<Image
|
||||
src="/images/logo-black-white.png"
|
||||
alt="Aunty Sparkles Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className="mr-4"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-8">
|
||||
<nav>
|
||||
<ul className="flex space-x-8 text-xl font-bold">
|
||||
<li>
|
||||
<a href="/about" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/gallery" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" className="text-blue-600 hover:text-blue-800 transition-colors">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<a
|
||||
href="/shop"
|
||||
className="bg-pink-500 text-white px-6 py-2 font-semibold hover:bg-pink-400 transition-colors rounded-full shadow-lg"
|
||||
>
|
||||
SHOP NOW
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="hero-bg py-20">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h1 className="text-5xl font-bold text-white mb-4">Contact</h1>
|
||||
<div className="w-16 h-1 bg-yellow-400 mb-8 mx-auto"></div>
|
||||
<p className="text-lg text-gray-300 max-w-2xl mx-auto">
|
||||
Got questions, custom requests, or just want to say hi? I'd love to hear from you!
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Cards */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex justify-center gap-8 mb-20">
|
||||
<div className="bg-white rounded-lg p-8 text-center max-w-sm">
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-4">INSTAGRAM</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Follow me on Instagram for behind-the-scenes peeks, new creations, market updates, and a little dose of
|
||||
upcycled magic.
|
||||
</p>
|
||||
<p className="font-semibold text-gray-800">@Aunty.Sparkles</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-8 text-center max-w-sm">
|
||||
<h3 className="text-2xl font-bold text-gray-800 mb-4">EMAIL</h3>
|
||||
<p className="text-gray-600 mb-6">
|
||||
I'd love to hear from you! Whether it's about a custom piece, a market inquiry, or just to say hi, you
|
||||
can email me anytime and I'll get back to you as soon as I can.
|
||||
</p>
|
||||
<p className="font-semibold text-gray-800">Aunty.Sparkles@gmail.com</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Contact Form */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-2xl mx-auto text-center mb-12">
|
||||
<h2 className="text-4xl font-bold text-white mb-4">Let's Talk</h2>
|
||||
<div className="w-16 h-1 bg-yellow-400 mb-8 mx-auto"></div>
|
||||
<p className="text-gray-300">
|
||||
Have a question, a custom request, or just want to say hi? Fill out the form below with your details and
|
||||
message, and I'll get back to you as soon as I can!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="max-w-2xl mx-auto">
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-transparent border border-gray-600 rounded px-4 py-3 text-white placeholder-gray-400 focus:border-yellow-400 focus:outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
placeholder="Email Address"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full bg-transparent border border-gray-600 rounded px-4 py-3 text-white placeholder-gray-400 focus:border-yellow-400 focus:outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder="Message"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
rows={6}
|
||||
className="w-full bg-transparent border border-gray-600 rounded px-4 py-3 text-white placeholder-gray-400 focus:border-yellow-400 focus:outline-none mb-8"
|
||||
required
|
||||
></textarea>
|
||||
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-yellow-400 text-black px-8 py-3 font-semibold hover:bg-yellow-300 transition-colors"
|
||||
>
|
||||
SEND MESSAGE
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<SiteFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import Image from "next/image"
|
||||
import SiteFooter from "@/components/site-footer"
|
||||
import ImageLightbox from "@/components/image-lightbox"
|
||||
|
||||
const galleryImages = [
|
||||
{
|
||||
src: "/images/gallery-1.webp",
|
||||
alt: "Upcycled denim jacket with purple velvet lace detailing and fringe trim",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-2.webp",
|
||||
alt: "Vintage-style denim jacket with aged patches and distressed details",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-3.webp",
|
||||
alt: "Denim jacket with bright pink fabric panels and colorful trim",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-4.webp",
|
||||
alt: "Denim jacket featuring delicate floral embroidery and decorative trim",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-5.webp",
|
||||
alt: "Denim jacket with colorful tassel fringe and heart-shaped beaded patches",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-6.webp",
|
||||
alt: "Cropped denim jacket with sequined red lips patch and geometric mesh detailing",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-7.webp",
|
||||
alt: "Denim jacket with burgundy floral embroidery and decorative trim work",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-8.webp",
|
||||
alt: "Patchwork denim jacket with golden fabric panels and embroidered cuff details",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-9.webp",
|
||||
alt: "Denim jacket with vibrant botanical patchwork panel featuring colorful folk art illustrations",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-10.webp",
|
||||
alt: "Close-up detail of denim jacket with purple velvet patches on collar and cuff",
|
||||
},
|
||||
{
|
||||
src: "/images/gallery-11.webp",
|
||||
alt: "Classic upcycled denim jacket styled for everyday wear in outdoor setting",
|
||||
},
|
||||
]
|
||||
|
||||
export default function Gallery() {
|
||||
const [lightboxImage, setLightboxImage] = useState<{ src: string; alt: string } | null>(null)
|
||||
|
||||
const openLightbox = (src: string, alt: string) => {
|
||||
setLightboxImage({ src, alt })
|
||||
}
|
||||
|
||||
const closeLightbox = () => {
|
||||
setLightboxImage(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Header */}
|
||||
<header className="bg-[#e8e4d3] py-4">
|
||||
<div className="container mx-auto px-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<a href="/" className="flex items-center">
|
||||
<Image
|
||||
src="/images/logo-black-white.png"
|
||||
alt="Aunty Sparkles Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className="mr-4"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-8">
|
||||
<nav>
|
||||
<ul className="flex space-x-8 text-xl font-bold">
|
||||
<li>
|
||||
<a href="/about" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/gallery" className="text-blue-600 hover:text-blue-800 transition-colors">
|
||||
Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<a
|
||||
href="/shop"
|
||||
className="bg-pink-500 text-white px-6 py-2 font-semibold hover:bg-pink-400 transition-colors rounded-full shadow-lg"
|
||||
>
|
||||
SHOP NOW
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="hero-bg py-20">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h1 className="text-5xl font-bold text-white mb-4">Gallery</h1>
|
||||
<div className="w-16 h-1 bg-yellow-400 mb-8 mx-auto"></div>
|
||||
<p className="text-lg text-gray-300 max-w-2xl mx-auto">
|
||||
Each jacket here started as something forgotten and unloved, thrifted or rescued before heading to the
|
||||
landfill. I reimagine every piece with unique details, playful textures, and a whole lot of love. No two are
|
||||
ever the same—just like the people who wear them. ✨
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Gallery Grid */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 max-w-6xl mx-auto">
|
||||
{galleryImages.map((image, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="aspect-square cursor-pointer group overflow-hidden rounded-lg"
|
||||
onClick={() => openLightbox(image.src, image.alt)}
|
||||
>
|
||||
<Image
|
||||
src={image.src || "/placeholder.svg"}
|
||||
alt={image.alt}
|
||||
width={400}
|
||||
height={400}
|
||||
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Lightbox */}
|
||||
<ImageLightbox
|
||||
src={lightboxImage?.src || ""}
|
||||
alt={lightboxImage?.alt || ""}
|
||||
isOpen={!!lightboxImage}
|
||||
onClose={closeLightbox}
|
||||
/>
|
||||
|
||||
{/* Footer */}
|
||||
<SiteFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: Georgia, "Times New Roman", Times, serif;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.hero-bg {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
}
|
||||
|
||||
.section-dark {
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
|
||||
.text-shadow {
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.text-shadow-lg {
|
||||
text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import type React from "react"
|
||||
import { ThemeProvider } from "@/components/theme-provider"
|
||||
import type { Metadata } from "next"
|
||||
import { cn } from "@/lib/utils"
|
||||
import "./globals.css"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Aunty Sparkles",
|
||||
description: "Whimsical upcycled treasures and handmade creations",
|
||||
icons: {
|
||||
icon: "/images/logo-black-white.png",
|
||||
shortcut: "/images/logo-black-white.png",
|
||||
apple: "/images/logo-black-white.png",
|
||||
},
|
||||
generator: 'v0.app'
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={cn("min-h-screen antialiased")}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
import Image from "next/image"
|
||||
import SiteFooter from "@/components/site-footer"
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Header */}
|
||||
<header className="bg-[#e8e4d3] py-4">
|
||||
<div className="container mx-auto px-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<a href="/" className="flex items-center">
|
||||
<Image
|
||||
src="/images/logo-black-white.png"
|
||||
alt="Aunty Sparkles Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className="mr-4"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-8">
|
||||
<nav>
|
||||
<ul className="flex space-x-8 text-xl font-bold">
|
||||
<li>
|
||||
<a href="/about" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/gallery" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<a
|
||||
href="/shop"
|
||||
className="bg-pink-500 text-white px-6 py-2 font-semibold hover:bg-pink-400 transition-colors rounded-full shadow-lg"
|
||||
>
|
||||
SHOP NOW
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section
|
||||
className="relative py-20 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('/images/denim-hero-background.png')`,
|
||||
}}
|
||||
>
|
||||
{/* Overlay for better text readability */}
|
||||
<div className="absolute inset-0 bg-black bg-opacity-40"></div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="w-1/2">
|
||||
<div className="w-96 h-96 mx-auto">
|
||||
<Image
|
||||
src="/images/embroidered-patch.webp"
|
||||
alt="Aunty Sparkles Logo"
|
||||
width={384}
|
||||
height={384}
|
||||
className="rounded-full shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/2 text-white">
|
||||
<h1 className="text-5xl font-bold mb-4 text-shadow-lg">Aunty Sparkles</h1>
|
||||
<div className="w-16 h-1 bg-yellow-400 mb-6"></div>
|
||||
<p className="text-lg mb-8 leading-relaxed text-shadow">
|
||||
Welcome to Aunty Sparkles – a whimsical little world of upcycled treasures and handmade creations that
|
||||
will make you sparkle just like me!
|
||||
</p>
|
||||
<a
|
||||
href="/shop"
|
||||
className="bg-yellow-400 text-black px-8 py-3 font-semibold hover:bg-yellow-300 transition-colors mx-auto block shadow-lg text-center"
|
||||
>
|
||||
SHOP NOW
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* About Section - Photo LEFT, Text RIGHT */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center">
|
||||
<div className="w-1/2 pr-12 flex items-start">
|
||||
<Image
|
||||
src="/images/about-photo.jpeg"
|
||||
alt="Person sitting with decorative mushrooms in forest"
|
||||
width={500}
|
||||
height={600}
|
||||
className="rounded-lg w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<h2 className="text-4xl font-bold text-white mb-8">About Aunty Sparkles</h2>
|
||||
<p className="text-gray-300 leading-relaxed mb-6">
|
||||
Aunty Sparkles is a celebration of creativity, sustainability, and a love for the little details that
|
||||
make life feel special. Rooted in upcycling and inspired by nature, every piece represents a commitment
|
||||
to giving new life to forgotten items while creating something truly unique.
|
||||
</p>
|
||||
<p className="text-gray-300 leading-relaxed">
|
||||
To be unique. Nothing here is mass-produced; each creation carries its own story, designed to bring joy
|
||||
and individuality to your life. When you shop at Aunty Sparkles, you'll find treasures that are playful,
|
||||
thoughtful, and connected by one thread: the belief that everyone deserves to sparkle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Collections Section */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="font-bold text-white text-center mb-12 text-6xl">Collections</h2>
|
||||
<div className="w-16 h-1 bg-yellow-400 mb-16"></div>
|
||||
|
||||
{/* Upcycled Jackets */}
|
||||
<div className="flex items-center mb-20">
|
||||
<div className="w-1/2 pr-12">
|
||||
<Image
|
||||
src="/images/upcycled-jackets.webp"
|
||||
alt="Upcycled Jackets and Art Collection"
|
||||
width={500}
|
||||
height={500}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<h3 className="text-3xl font-bold text-white mb-6">Upcycled Jackets</h3>
|
||||
<p className="text-gray-300 leading-relaxed mb-8">
|
||||
Every Aunty Sparkles jacket is a one-of-a-kind creation, handcrafted from secondhand denim and
|
||||
transformed into wearable art. Every piece is upcycled with care, featuring hand-painted designs and
|
||||
whimsical patches that tell a story. From bold, colorful graphics to delicate, nature-inspired details,
|
||||
each jacket lets you wear your story while keeping sustainability at heart.
|
||||
</p>
|
||||
<button className="bg-pink-500 text-white px-8 py-3 font-semibold hover:bg-pink-400 transition-colors">
|
||||
SHOP FOR JACKETS
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Art and Jewelry */}
|
||||
<div className="flex items-center mb-20">
|
||||
<div className="w-1/2 pr-12">
|
||||
<h3 className="text-3xl font-bold text-white mb-6">Art and Jewelry</h3>
|
||||
<p className="text-gray-300 leading-relaxed mb-8">
|
||||
Aunty Sparkles brings a touch of whimsy to everyday life. Handmade mushroom art captures the magic of
|
||||
nature in soft, playful tones, while unique stickers add a little sparkle to any surface. Each piece is
|
||||
designed to bring joy and wonder to your world, with hand-painted details and wearable pieces of
|
||||
woodland wonder. Each creation is made with love, inspired by the beauty and curiosity of the natural
|
||||
world.
|
||||
</p>
|
||||
<button className="bg-pink-500 text-white px-8 py-3 font-semibold hover:bg-pink-400 transition-colors">
|
||||
SHOP ART AND JEWELRY
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<Image
|
||||
src="/images/product-collage.webp"
|
||||
alt="Aunty Sparkles product collage showing art and jewelry pieces"
|
||||
width={400}
|
||||
height={400}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Block Prints */}
|
||||
<div className="flex items-center">
|
||||
<div className="w-1/2 pr-12">
|
||||
<Image
|
||||
src="/images/embroidered-patch.webp"
|
||||
alt="Embroidered patch design"
|
||||
width={400}
|
||||
height={400}
|
||||
className="rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/2">
|
||||
<h3 className="text-3xl font-bold text-white mb-6">Block Prints - COMING SOON</h3>
|
||||
<p className="text-gray-300 leading-relaxed mb-8">
|
||||
Get ready for Aunty Sparkles' newest adventure! Block Prints will be the place to discover hand-carved
|
||||
stamps and printing tools for your own creativity. Stay tuned for our hand-carved stamps and printing
|
||||
tools that will help you create your own beautiful designs. You'll discover easy-to-use block tools.
|
||||
Carve that look amazing on everything you make and create your own sparkle.
|
||||
</p>
|
||||
<button className="bg-yellow-400 text-black px-8 py-3 font-semibold hover:bg-yellow-300 transition-colors">
|
||||
SHOP PRINTED CLOTHES
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<SiteFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,483 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Image from "next/image"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import SquarePaymentForm from "@/components/square-payment-form"
|
||||
import SiteFooter from "@/components/site-footer"
|
||||
|
||||
interface Product {
|
||||
id: string
|
||||
variationId: string
|
||||
name: string
|
||||
description: string
|
||||
price: number
|
||||
currency: string
|
||||
imageUrl: string
|
||||
inventory: number
|
||||
category: string
|
||||
}
|
||||
|
||||
interface ApiResponse {
|
||||
success: boolean
|
||||
products?: Product[]
|
||||
error?: string
|
||||
message?: string
|
||||
details?: any
|
||||
}
|
||||
|
||||
export default function Shop() {
|
||||
const [products, setProducts] = useState<Product[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string>("")
|
||||
const [cart, setCart] = useState<
|
||||
Array<{
|
||||
id: string
|
||||
variationId: string
|
||||
name: string
|
||||
price: number
|
||||
quantity: number
|
||||
description?: string
|
||||
}>
|
||||
>([])
|
||||
const [showCheckout, setShowCheckout] = useState(false)
|
||||
const [orderComplete, setOrderComplete] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts()
|
||||
}, [])
|
||||
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError("")
|
||||
|
||||
console.log("Fetching products from /api/square/catalog...")
|
||||
|
||||
// First check Square health
|
||||
const healthResponse = await fetch("/api/square/health")
|
||||
const healthData = await healthResponse.json()
|
||||
|
||||
if (!healthData.success) {
|
||||
console.error("Square health check failed:", healthData)
|
||||
setError(`Square API configuration issue: ${healthData.error || 'Connection failed'}`)
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Square health check passed, fetching catalog...")
|
||||
|
||||
const response = await fetch("/api/square/catalog?includeInventory=true")
|
||||
|
||||
console.log("Response status:", response.status)
|
||||
console.log("Response headers:", Object.fromEntries(response.headers.entries()))
|
||||
|
||||
// Check if response is ok
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error("HTTP error:", response.status, errorText.substring(0, 500))
|
||||
setError(`Server error (${response.status}): ${response.statusText}`)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if response is JSON
|
||||
const contentType = response.headers.get("content-type")
|
||||
if (!contentType || !contentType.includes("application/json")) {
|
||||
const responseText = await response.text()
|
||||
console.error("Non-JSON response:", responseText.substring(0, 500))
|
||||
setError("Server returned invalid response format. Check server logs for details.")
|
||||
return
|
||||
}
|
||||
|
||||
const data: ApiResponse = await response.json()
|
||||
console.log("API Response:", data)
|
||||
|
||||
if (data.success && data.products) {
|
||||
setProducts(data.products)
|
||||
console.log("Products loaded:", data.products.length)
|
||||
} else {
|
||||
const errorMsg = data.error || data.message || "Failed to load products"
|
||||
console.error("API returned error:", errorMsg)
|
||||
setError(errorMsg)
|
||||
|
||||
// Show additional details if available
|
||||
if (data.details) {
|
||||
console.error("Error details:", data.details)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching products:", error)
|
||||
|
||||
// More specific error handling
|
||||
if (error instanceof TypeError && error.message.includes("json")) {
|
||||
setError("Server returned invalid data format. This usually means Square SDK failed to load.")
|
||||
} else if (error instanceof TypeError && error.message.includes("fetch")) {
|
||||
setError("Network connection failed. Please check your internet connection.")
|
||||
} else {
|
||||
setError(`Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`)
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const addToCart = (product: Product) => {
|
||||
if (product.inventory <= 0) {
|
||||
alert("Sorry, this item is out of stock!")
|
||||
return
|
||||
}
|
||||
|
||||
setCart((prev) => {
|
||||
const existing = prev.find((item) => item.id === product.id)
|
||||
if (existing) {
|
||||
const newQuantity = existing.quantity + 1
|
||||
if (newQuantity > product.inventory) {
|
||||
alert(`Sorry, only ${product.inventory} items available in stock!`)
|
||||
return prev
|
||||
}
|
||||
return prev.map((item) => (item.id === product.id ? { ...item, quantity: newQuantity } : item))
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
id: product.id,
|
||||
variationId: product.variationId,
|
||||
name: product.name,
|
||||
price: product.price,
|
||||
quantity: 1,
|
||||
description: product.description,
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const removeFromCart = (productId: string) => {
|
||||
setCart((prev) => prev.filter((item) => item.id !== productId))
|
||||
}
|
||||
|
||||
const updateQuantity = (productId: string, quantity: number) => {
|
||||
if (quantity === 0) {
|
||||
removeFromCart(productId)
|
||||
return
|
||||
}
|
||||
|
||||
const product = products.find((p) => p.id === productId)
|
||||
if (product && quantity > product.inventory) {
|
||||
alert(`Sorry, only ${product.inventory} items available in stock!`)
|
||||
return
|
||||
}
|
||||
|
||||
setCart((prev) => prev.map((item) => (item.id === productId ? { ...item, quantity } : item)))
|
||||
}
|
||||
|
||||
const handlePaymentSuccess = (paymentResult: any) => {
|
||||
console.log("Payment successful:", paymentResult)
|
||||
setOrderComplete(true)
|
||||
setCart([])
|
||||
setShowCheckout(false)
|
||||
fetchProducts()
|
||||
}
|
||||
|
||||
const handlePaymentError = (error: string) => {
|
||||
console.error("Payment error:", error)
|
||||
alert(`Payment failed: ${error}`)
|
||||
}
|
||||
|
||||
if (orderComplete) {
|
||||
return (
|
||||
<div className="min-h-screen bg-black flex items-center justify-center">
|
||||
<Card className="max-w-md mx-auto">
|
||||
<CardContent className="text-center p-8">
|
||||
<div className="text-6xl mb-4">✨</div>
|
||||
<h2 className="text-2xl font-bold mb-4">Order Complete!</h2>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Thank you for your purchase! You'll receive a confirmation email shortly.
|
||||
</p>
|
||||
<Button onClick={() => setOrderComplete(false)}>Continue Shopping</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (showCheckout && cart.length > 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
<header className="bg-[#e8e4d3] py-4">
|
||||
<div className="container mx-auto px-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<a href="/" className="flex items-center">
|
||||
<Image
|
||||
src="/images/logo-black-white.png"
|
||||
alt="Aunty Sparkles Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className="mr-4"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-8">
|
||||
<nav>
|
||||
<ul className="flex space-x-8 text-xl font-bold">
|
||||
<li>
|
||||
<a href="/about" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/gallery" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="w-8"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-4xl font-bold text-white">Checkout</h1>
|
||||
<Button
|
||||
onClick={() => setShowCheckout(false)}
|
||||
variant="outline"
|
||||
className="text-white border-white hover:bg-white hover:text-black"
|
||||
>
|
||||
Back to Shop
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<SquarePaymentForm
|
||||
items={cart}
|
||||
onPaymentSuccess={handlePaymentSuccess}
|
||||
onPaymentError={handlePaymentError}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<SiteFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
{/* Header */}
|
||||
<header className="bg-[#e8e4d3] py-4">
|
||||
<div className="container mx-auto px-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<a href="/" className="flex items-center">
|
||||
<Image
|
||||
src="/images/logo-black-white.png"
|
||||
alt="Aunty Sparkles Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className="mr-4"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center space-x-8">
|
||||
<nav>
|
||||
<ul className="flex space-x-8 text-xl font-bold">
|
||||
<li>
|
||||
<a href="/about" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/gallery" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact" className="text-gray-700 hover:text-gray-900 transition-colors">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="w-8">
|
||||
{cart.length > 0 && (
|
||||
<div className="relative">
|
||||
<Button
|
||||
onClick={() => setShowCheckout(true)}
|
||||
className="bg-yellow-400 text-black hover:bg-yellow-300 text-xs px-2 py-1"
|
||||
>
|
||||
Cart ({cart.reduce((sum, item) => sum + item.quantity, 0)})
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="hero-bg py-20">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h1 className="text-5xl font-bold text-white mb-4">Shop</h1>
|
||||
<div className="w-16 h-1 bg-yellow-400 mb-8 mx-auto"></div>
|
||||
<p className="text-lg text-gray-300 max-w-2xl mx-auto">
|
||||
Discover unique, handcrafted pieces that tell a story. Each item is lovingly upcycled and designed to help
|
||||
you sparkle! ✨
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Debug Info */}
|
||||
{error && (
|
||||
<section className="bg-red-900 py-4">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p className="text-white font-semibold">Debug Info: {error}</p>
|
||||
<p className="text-red-200 text-sm mt-2">
|
||||
Check browser console for more details. This usually means Square API credentials need to be configured.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Products Grid */}
|
||||
<section className="section-dark py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
{loading ? (
|
||||
<div className="text-center text-white">
|
||||
<div className="text-2xl mb-4">Loading products...</div>
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-yellow-400 mx-auto"></div>
|
||||
<p className="text-gray-300 mt-4">Connecting to Square catalog...</p>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center text-white">
|
||||
<div className="text-2xl mb-4">Unable to load products</div>
|
||||
<p className="text-gray-300 mb-4">{error}</p>
|
||||
<div className="space-y-2 mb-4">
|
||||
<p className="text-sm text-gray-400">Common solutions:</p>
|
||||
<ul className="text-sm text-gray-400 list-disc list-inside">
|
||||
<li>Check that Square API credentials are configured in environment variables</li>
|
||||
<li>Verify that products exist in your Square catalog</li>
|
||||
<li>Ensure Square API access token has catalog permissions</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchProducts}
|
||||
className="bg-yellow-400 text-black px-6 py-2 rounded hover:bg-yellow-300 transition-colors"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
</div>
|
||||
) : products.length === 0 ? (
|
||||
<div className="text-center text-white">
|
||||
<div className="text-2xl mb-4">No products available</div>
|
||||
<p className="text-gray-300">Add products to your Square catalog to see them here.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{products.map((product) => (
|
||||
<Card key={product.id} className="bg-gray-800 border-gray-700">
|
||||
<CardHeader className="p-0 relative">
|
||||
<Image
|
||||
src={product.imageUrl || "/placeholder.svg"}
|
||||
alt={product.name}
|
||||
width={400}
|
||||
height={400}
|
||||
className="w-full h-64 object-cover rounded-t-lg"
|
||||
/>
|
||||
{product.inventory <= 0 && (
|
||||
<Badge className="absolute top-2 right-2 bg-red-500">Out of Stock</Badge>
|
||||
)}
|
||||
{product.inventory > 0 && product.inventory <= 5 && (
|
||||
<Badge className="absolute top-2 right-2 bg-orange-500">Only {product.inventory} left</Badge>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="p-6">
|
||||
<CardTitle className="text-white mb-2">{product.name}</CardTitle>
|
||||
<p className="text-gray-300 text-sm mb-4 line-clamp-3">{product.description}</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xl font-bold text-yellow-400">${product.price.toFixed(2)}</span>
|
||||
<Button
|
||||
onClick={() => addToCart(product)}
|
||||
disabled={product.inventory <= 0}
|
||||
className="bg-pink-500 text-white hover:bg-pink-400 disabled:bg-gray-500 disabled:cursor-not-allowed"
|
||||
>
|
||||
{product.inventory <= 0 ? "Out of Stock" : "Add to Cart"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-400">
|
||||
{product.inventory > 5 ? "In Stock" : `${product.inventory} in stock`}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Cart Summary */}
|
||||
{cart.length > 0 && (
|
||||
<section className="section-dark py-10 border-t border-gray-700">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-md mx-auto">
|
||||
<h3 className="text-xl font-bold text-white mb-4">Shopping Cart</h3>
|
||||
<div className="space-y-2 mb-4">
|
||||
{cart.map((item) => (
|
||||
<div key={item.id} className="flex items-center justify-between text-white">
|
||||
<span className="text-sm">
|
||||
{item.name} x{item.quantity}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateQuantity(item.id, item.quantity - 1)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
-
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => updateQuantity(item.id, item.quantity + 1)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => removeFromCart(item.id)}
|
||||
className="h-6 w-6 p-0"
|
||||
>
|
||||
×
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<span className="text-white font-bold">
|
||||
Total: ${cart.reduce((sum, item) => sum + item.price * item.quantity, 0).toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setShowCheckout(true)}
|
||||
className="w-full bg-yellow-400 text-black hover:bg-yellow-300"
|
||||
>
|
||||
Proceed to Checkout
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<SiteFooter />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import Image from "next/image"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
interface ImageLightboxProps {
|
||||
src: string
|
||||
alt: string
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export default function ImageLightbox({ src, alt, isOpen, onClose }: ImageLightboxProps) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = "hidden"
|
||||
} else {
|
||||
document.body.style.overflow = "unset"
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.overflow = "unset"
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener("keydown", handleEscape)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleEscape)
|
||||
}
|
||||
}, [isOpen, onClose])
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-90 flex items-center justify-center p-4" onClick={onClose}>
|
||||
<div className="relative max-w-7xl max-h-full">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 z-10 bg-white bg-opacity-20 hover:bg-opacity-30 rounded-full p-2 transition-colors"
|
||||
aria-label="Close image"
|
||||
>
|
||||
<X className="w-6 h-6 text-white" />
|
||||
</button>
|
||||
|
||||
<div className="relative" onClick={(e) => e.stopPropagation()}>
|
||||
<Image
|
||||
src={src || "/placeholder.svg"}
|
||||
alt={alt}
|
||||
width={1200}
|
||||
height={1200}
|
||||
className="max-w-full max-h-[90vh] object-contain rounded-lg"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
export default function SiteFooter() {
|
||||
const [email, setEmail] = useState("")
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!email.trim()) {
|
||||
alert('Please enter a valid email address')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/mailchimp/subscribe', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
alert('Thank you for subscribing! Check your email for confirmation.')
|
||||
setEmail('')
|
||||
} else {
|
||||
alert(result.error || 'Subscription failed. Please try again.')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Subscription error:', error)
|
||||
alert('Something went wrong. Please try again later.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<footer
|
||||
className="relative py-16 px-4"
|
||||
style={{
|
||||
backgroundImage: `url('/images/denim-footer-background.png')`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundColor: "#a8b5c4",
|
||||
}}
|
||||
>
|
||||
<div className="container mx-auto relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-12 items-start">
|
||||
{/* Stay in Touch - Left Column */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-4xl font-bold text-black font-serif">Get in Touch </h2>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-4 py-3 rounded bg-white text-gray-800 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-yellow-400"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-yellow-400 text-black py-3 px-6 font-semibold hover:bg-yellow-300 transition-colors"
|
||||
>
|
||||
SUBMIT
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Location & Contact - Center Column */}
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white mb-2 tracking-wider">Our Location</h3>
|
||||
<p className="text-gray-700 font-medium">Revelstoke, BC</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white mb-2 tracking-wider">Contact </h3>
|
||||
<p className="text-gray-700 font-medium">Aunty.Sparkles@gmail.com</p>
|
||||
</div>
|
||||
|
||||
{/* Instagram Link */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white mb-2 tracking-wider">Follow the Latest Designs </h3>
|
||||
<a
|
||||
href="https://www.instagram.com/aunty.sparkles/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center space-x-2 text-gray-700 hover:text-black transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.073-1.689-.073-4.948 0-3.204.013-3.583.072-4.948.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.40z" />
|
||||
</svg>
|
||||
<span className="font-medium">@aunty.sparkles</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hours - Right Column */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white mb-4 tracking-wider">Hours</h3>
|
||||
<div className="space-y-2 text-gray-700 font-medium">
|
||||
<div className="flex gap-4">
|
||||
<span>M - F:</span>
|
||||
<span>8am-5pm</span>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<span>Sat:</span>
|
||||
<span>11am-6pm</span>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<span>Sun:</span>
|
||||
<span>Closed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom section */}
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState, useRef } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import type { CartItem, CustomerInfo, OrderResult } from "@/lib/square-types"
|
||||
|
||||
interface PaymentFormProps {
|
||||
items: CartItem[]
|
||||
onPaymentSuccess: (result: OrderResult) => void
|
||||
onPaymentError: (error: string) => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Square: any
|
||||
}
|
||||
}
|
||||
|
||||
export default function SquarePaymentForm({ items, onPaymentSuccess, onPaymentError }: PaymentFormProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [squareLoaded, setSquareLoaded] = useState(false)
|
||||
const [loadingError, setLoadingError] = useState<string>("")
|
||||
const [customerInfo, setCustomerInfo] = useState<CustomerInfo>({
|
||||
name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
address: {
|
||||
street: "",
|
||||
city: "",
|
||||
state: "",
|
||||
zipCode: "",
|
||||
country: "US"
|
||||
},
|
||||
})
|
||||
|
||||
const paymentsRef = useRef<any>(null)
|
||||
const cardRef = useRef<any>(null)
|
||||
|
||||
const totalAmount = items.reduce((sum, item) => sum + item.price * item.quantity, 0)
|
||||
|
||||
useEffect(() => {
|
||||
loadSquareSDK()
|
||||
return () => {
|
||||
// Cleanup on unmount
|
||||
if (cardRef.current) {
|
||||
try {
|
||||
cardRef.current.destroy()
|
||||
} catch (e) {
|
||||
console.warn("Error destroying card:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const loadSquareSDK = async () => {
|
||||
try {
|
||||
setLoadingError("")
|
||||
|
||||
// Check if Square is already loaded
|
||||
if (window.Square) {
|
||||
await initializeSquare()
|
||||
return
|
||||
}
|
||||
|
||||
// Validate required environment variables
|
||||
if (!process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID) {
|
||||
throw new Error("Square Application ID not configured")
|
||||
}
|
||||
|
||||
if (!process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID) {
|
||||
throw new Error("Square Location ID not configured")
|
||||
}
|
||||
|
||||
// Load Square Web Payments SDK
|
||||
const script = document.createElement("script")
|
||||
script.src = process.env.SQUARE_ENVIRONMENT === "production"
|
||||
? "https://web.squarecdn.com/v1/square.js"
|
||||
: "https://sandbox.web.squarecdn.com/v1/square.js"
|
||||
script.async = true
|
||||
|
||||
script.onload = async () => {
|
||||
try {
|
||||
await initializeSquare()
|
||||
} catch (error) {
|
||||
setLoadingError(`Square initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
script.onerror = () => {
|
||||
setLoadingError("Failed to load Square payment system")
|
||||
}
|
||||
|
||||
document.head.appendChild(script)
|
||||
|
||||
} catch (error) {
|
||||
setLoadingError(error instanceof Error ? error.message : "Failed to load payment system")
|
||||
}
|
||||
}
|
||||
|
||||
const initializeSquare = async () => {
|
||||
try {
|
||||
console.log("Initializing Square Web Payments SDK...")
|
||||
|
||||
// Initialize Square Payments
|
||||
paymentsRef.current = window.Square.payments(
|
||||
process.env.NEXT_PUBLIC_SQUARE_APPLICATION_ID,
|
||||
process.env.NEXT_PUBLIC_SQUARE_LOCATION_ID
|
||||
)
|
||||
|
||||
// Create and attach card payment method
|
||||
cardRef.current = await paymentsRef.current.card({
|
||||
style: {
|
||||
input: {
|
||||
fontSize: '16px',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
color: '#373F4A',
|
||||
backgroundColor: '#FFFFFF',
|
||||
borderRadius: '6px',
|
||||
borderColor: '#E0E2E5',
|
||||
borderWidth: '1px',
|
||||
padding: '12px'
|
||||
},
|
||||
'.input-container': {
|
||||
borderRadius: '6px',
|
||||
borderColor: '#E0E2E5',
|
||||
borderWidth: '1px'
|
||||
},
|
||||
'.input-container.is-focus': {
|
||||
borderColor: '#4A90E2',
|
||||
boxShadow: '0 0 0 1px #4A90E2'
|
||||
},
|
||||
'.input-container.is-error': {
|
||||
borderColor: '#E02F2F'
|
||||
},
|
||||
'.message-text': {
|
||||
color: '#E02F2F',
|
||||
fontSize: '14px'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await cardRef.current.attach('#card-container')
|
||||
setSquareLoaded(true)
|
||||
console.log("Square Web Payments SDK initialized successfully")
|
||||
|
||||
} catch (error) {
|
||||
console.error("Square initialization error:", error)
|
||||
setLoadingError(`Payment system initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCustomerInfoChange = (field: string, value: string) => {
|
||||
if (field.includes(".")) {
|
||||
const [parent, child] = field.split(".")
|
||||
setCustomerInfo((prev) => ({
|
||||
...prev,
|
||||
[parent]: {
|
||||
...prev[parent as keyof CustomerInfo],
|
||||
[child]: value,
|
||||
},
|
||||
}))
|
||||
} else {
|
||||
setCustomerInfo((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
const validateForm = (): string | null => {
|
||||
if (!customerInfo.name.trim()) return "Name is required"
|
||||
if (!customerInfo.email.trim()) return "Email is required"
|
||||
if (!/\S+@\S+\.\S+/.test(customerInfo.email)) return "Valid email is required"
|
||||
return null
|
||||
}
|
||||
|
||||
const handlePayment = async () => {
|
||||
if (!squareLoaded || !cardRef.current) {
|
||||
onPaymentError("Payment system not ready")
|
||||
return
|
||||
}
|
||||
|
||||
const validationError = validateForm()
|
||||
if (validationError) {
|
||||
onPaymentError(validationError)
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
console.log("Starting payment process...")
|
||||
|
||||
// Step 1: Create order
|
||||
console.log("Creating order...")
|
||||
const orderResponse = await fetch("/api/square/orders/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ items, customerInfo }),
|
||||
})
|
||||
|
||||
if (!orderResponse.ok) {
|
||||
const errorText = await orderResponse.text()
|
||||
throw new Error(`Order creation failed: ${errorText}`)
|
||||
}
|
||||
|
||||
const orderResult = await orderResponse.json()
|
||||
if (!orderResult.success) {
|
||||
throw new Error(orderResult.error || "Failed to create order")
|
||||
}
|
||||
|
||||
console.log("Order created:", orderResult.order.id)
|
||||
|
||||
// Step 2: Tokenize the card
|
||||
console.log("Tokenizing card...")
|
||||
const tokenResult = await cardRef.current.tokenize()
|
||||
|
||||
if (tokenResult.status !== "OK") {
|
||||
const errorMessage = tokenResult.errors?.[0]?.detail || "Card tokenization failed"
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
console.log("Card tokenized successfully")
|
||||
|
||||
// Step 3: Process payment
|
||||
console.log("Processing payment...")
|
||||
const paymentResponse = await fetch("/api/square/payments/create", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
sourceId: tokenResult.token,
|
||||
orderId: orderResult.order.id,
|
||||
amount: totalAmount,
|
||||
currency: "USD",
|
||||
customerInfo,
|
||||
items
|
||||
}),
|
||||
})
|
||||
|
||||
if (!paymentResponse.ok) {
|
||||
const errorText = await paymentResponse.text()
|
||||
throw new Error(`Payment processing failed: ${errorText}`)
|
||||
}
|
||||
|
||||
const paymentResult = await paymentResponse.json()
|
||||
if (!paymentResult.success) {
|
||||
throw new Error(paymentResult.error || "Payment failed")
|
||||
}
|
||||
|
||||
console.log("Payment processed successfully:", paymentResult.payment.id)
|
||||
|
||||
// Success!
|
||||
onPaymentSuccess({
|
||||
orderId: orderResult.order.id,
|
||||
paymentId: paymentResult.payment.id,
|
||||
status: paymentResult.payment.status,
|
||||
totalAmount: totalAmount,
|
||||
receiptUrl: paymentResult.payment.receiptUrl
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
console.error("Payment error:", error)
|
||||
onPaymentError(error instanceof Error ? error.message : "Payment failed")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
{/* Loading Error */}
|
||||
{loadingError && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{loadingError}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Order Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Order Summary</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{items.map((item) => (
|
||||
<div key={`${item.id}-${item.variationId}`} className="flex justify-between items-center">
|
||||
<div>
|
||||
<h4 className="font-medium">{item.name}</h4>
|
||||
<p className="text-sm text-gray-600">Quantity: {item.quantity}</p>
|
||||
{item.description && <p className="text-sm text-gray-500">{item.description}</p>}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">${(item.price * item.quantity).toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex justify-between items-center font-bold text-lg">
|
||||
<span>Total:</span>
|
||||
<span>${totalAmount.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Customer Information */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Customer Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input
|
||||
placeholder="Full Name *"
|
||||
value={customerInfo.name}
|
||||
onChange={(e) => handleCustomerInfoChange("name", e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="Email Address *"
|
||||
value={customerInfo.email}
|
||||
onChange={(e) => handleCustomerInfoChange("email", e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="tel"
|
||||
placeholder="Phone Number"
|
||||
value={customerInfo.phone}
|
||||
onChange={(e) => handleCustomerInfoChange("phone", e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium">Shipping Address</h4>
|
||||
<Input
|
||||
placeholder="Street Address"
|
||||
value={customerInfo.address?.street || ""}
|
||||
onChange={(e) => handleCustomerInfoChange("address.street", e.target.value)}
|
||||
/>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<Input
|
||||
placeholder="City"
|
||||
value={customerInfo.address?.city || ""}
|
||||
onChange={(e) => handleCustomerInfoChange("address.city", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="State"
|
||||
value={customerInfo.address?.state || ""}
|
||||
onChange={(e) => handleCustomerInfoChange("address.state", e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="ZIP Code"
|
||||
value={customerInfo.address?.zipCode || ""}
|
||||
onChange={(e) => handleCustomerInfoChange("address.zipCode", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Payment Form */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Payment Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!squareLoaded && !loadingError && (
|
||||
<div className="text-center py-4">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-400 mx-auto mb-2"></div>
|
||||
<p className="text-sm text-gray-600">Loading secure payment form...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
id="card-container"
|
||||
className={`mb-4 border rounded-lg min-h-[120px] ${!squareLoaded ? 'opacity-50' : ''}`}
|
||||
style={{ minHeight: '120px' }}
|
||||
>
|
||||
{/* Square card form will be inserted here */}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handlePayment}
|
||||
disabled={isLoading || !squareLoaded || !!loadingError}
|
||||
className="w-full bg-yellow-400 text-black hover:bg-yellow-300 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-black mr-2"></div>
|
||||
Processing Payment...
|
||||
</div>
|
||||
) : (
|
||||
`Pay $${totalAmount.toFixed(2)}`
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<p className="text-xs text-gray-500 mt-2 text-center">
|
||||
Secure payment powered by Square • PCI DSS Compliant
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ThemeProvider as NextThemesProvider,
|
||||
type ThemeProviderProps,
|
||||
} from 'next-themes'
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = 'Alert'
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = 'AlertTitle'
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = 'AlertDescription'
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from 'react'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
secondary:
|
||||
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
destructive:
|
||||
'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
|
||||
outline: 'text-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-10 px-4 py-2',
|
||||
sm: 'h-9 rounded-md px-3',
|
||||
lg: 'h-11 rounded-md px-8',
|
||||
icon: 'h-10 w-10',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = 'Card'
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-col space-y-1.5 p-6', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = 'CardHeader'
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-2xl font-semibold leading-none tracking-tight',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = 'CardTitle'
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = 'CardDescription'
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = 'CardContent'
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex items-center p-6 pt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = 'CardFooter'
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
let squareClient: any = null
|
||||
let clientError: string | null = null
|
||||
let isInitializing = false
|
||||
|
||||
// Dynamic import function for Square SDK
|
||||
const importSquareSDK = async () => {
|
||||
try {
|
||||
// Use dynamic import instead of require
|
||||
const squareModule = await import("squareup")
|
||||
return squareModule
|
||||
} catch (error) {
|
||||
console.error("Failed to import Square SDK:", error)
|
||||
throw new Error(`Square SDK import failed: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Square client with proper error handling
|
||||
export const getSquareClient = async () => {
|
||||
if (clientError) {
|
||||
throw new Error(clientError)
|
||||
}
|
||||
|
||||
if (squareClient) {
|
||||
return squareClient
|
||||
}
|
||||
|
||||
if (isInitializing) {
|
||||
// Wait for initialization to complete
|
||||
while (isInitializing) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
if (squareClient) return squareClient
|
||||
if (clientError) throw new Error(clientError)
|
||||
}
|
||||
|
||||
isInitializing = true
|
||||
|
||||
try {
|
||||
// Validate environment variables first
|
||||
if (!process.env.SQUARE_ACCESS_TOKEN) {
|
||||
throw new Error("SQUARE_ACCESS_TOKEN environment variable is required")
|
||||
}
|
||||
|
||||
if (!process.env.SQUARE_LOCATION_ID) {
|
||||
throw new Error("SQUARE_LOCATION_ID environment variable is required")
|
||||
}
|
||||
|
||||
console.log("Importing Square SDK...")
|
||||
|
||||
// Dynamic import of Square SDK
|
||||
const { Client, Environment } = await importSquareSDK()
|
||||
|
||||
console.log("Creating Square client...")
|
||||
|
||||
squareClient = new Client({
|
||||
accessToken: process.env.SQUARE_ACCESS_TOKEN,
|
||||
environment: process.env.SQUARE_ENVIRONMENT === "production"
|
||||
? Environment.Production
|
||||
: Environment.Sandbox,
|
||||
})
|
||||
|
||||
console.log("Square client created successfully")
|
||||
return squareClient
|
||||
|
||||
} catch (error) {
|
||||
clientError = error instanceof Error ? error.message : "Failed to initialize Square client"
|
||||
console.error("Square client initialization failed:", clientError)
|
||||
throw new Error(clientError)
|
||||
} finally {
|
||||
isInitializing = false
|
||||
}
|
||||
}
|
||||
|
||||
// Export location ID with validation
|
||||
export const getSquareLocationId = () => {
|
||||
if (!process.env.SQUARE_LOCATION_ID) {
|
||||
throw new Error("SQUARE_LOCATION_ID environment variable is required")
|
||||
}
|
||||
return process.env.SQUARE_LOCATION_ID
|
||||
}
|
||||
|
||||
// Health check function
|
||||
export const checkSquareConnection = async () => {
|
||||
try {
|
||||
console.log("Starting Square connection check...")
|
||||
|
||||
const client = await getSquareClient()
|
||||
const locationId = getSquareLocationId()
|
||||
|
||||
console.log("Testing Square API connection...")
|
||||
|
||||
// Try a simple API call to verify connection
|
||||
const locationsApi = client.locationsApi
|
||||
const response = await locationsApi.retrieveLocation(locationId)
|
||||
|
||||
console.log("Square API connection successful")
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Square connection verified",
|
||||
locationName: response.result.location?.name || "Unknown"
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Square connection check failed:", error)
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Connection failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset client (useful for testing)
|
||||
export const resetSquareClient = () => {
|
||||
squareClient = null
|
||||
clientError = null
|
||||
isInitializing = false
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// Square catalog types
|
||||
export interface SquareProduct {
|
||||
id: string
|
||||
variationId: string
|
||||
name: string
|
||||
description: string
|
||||
price: number
|
||||
currency: string
|
||||
imageUrl: string
|
||||
inventory: number
|
||||
category: string
|
||||
categoryName?: string
|
||||
isAvailable: boolean
|
||||
variations: SquareVariation[]
|
||||
}
|
||||
|
||||
export interface SquareVariation {
|
||||
id: string
|
||||
name: string
|
||||
price: number
|
||||
currency: string
|
||||
inventory: number
|
||||
sku?: string
|
||||
}
|
||||
|
||||
export interface SquareCategory {
|
||||
id: string
|
||||
name: string
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
export interface SquareInventory {
|
||||
catalogObjectId: string
|
||||
quantity: number
|
||||
locationId: string
|
||||
state: 'IN_STOCK' | 'SOLD' | 'WASTE' | 'NONE'
|
||||
}
|
||||
|
||||
// Payment types
|
||||
export interface PaymentRequest {
|
||||
sourceId: string
|
||||
orderId?: string
|
||||
amount: number
|
||||
currency: string
|
||||
customerInfo: CustomerInfo
|
||||
items: CartItem[]
|
||||
}
|
||||
|
||||
export interface CustomerInfo {
|
||||
name: string
|
||||
email: string
|
||||
phone?: string
|
||||
address?: {
|
||||
street: string
|
||||
city: string
|
||||
state: string
|
||||
zipCode: string
|
||||
country?: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
id: string
|
||||
variationId: string
|
||||
name: string
|
||||
price: number
|
||||
quantity: number
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface OrderResult {
|
||||
orderId: string
|
||||
paymentId: string
|
||||
status: string
|
||||
totalAmount: number
|
||||
receiptUrl?: string
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"name": "my-v0-project",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"dev": "next dev",
|
||||
"lint": "next lint",
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@radix-ui/react-accordion": "1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-context-menu": "2.2.4",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-hover-card": "1.1.4",
|
||||
"@radix-ui/react-label": "2.1.1",
|
||||
"@radix-ui/react-menubar": "1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "1.2.3",
|
||||
"@radix-ui/react-popover": "1.1.4",
|
||||
"@radix-ui/react-progress": "1.1.1",
|
||||
"@radix-ui/react-radio-group": "1.2.2",
|
||||
"@radix-ui/react-scroll-area": "1.2.2",
|
||||
"@radix-ui/react-select": "2.1.4",
|
||||
"@radix-ui/react-separator": "1.1.1",
|
||||
"@radix-ui/react-slider": "1.2.2",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-switch": "1.1.2",
|
||||
"@radix-ui/react-tabs": "1.1.2",
|
||||
"@radix-ui/react-toast": "1.2.4",
|
||||
"@radix-ui/react-toggle": "1.1.1",
|
||||
"@radix-ui/react-toggle-group": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.6",
|
||||
"@vercel/analytics": "1.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"geist": "^1.3.1",
|
||||
"input-otp": "1.4.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"next": "14.2.16",
|
||||
"next-themes": "^0.4.4",
|
||||
"react": "^18",
|
||||
"react-day-picker": "9.8.0",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.54.1",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "2.15.0",
|
||||
"sonner": "^1.7.1",
|
||||
"squareup": "latest",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^0.9.6",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 952 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 613 KiB |
|
After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 922 KiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 3.0 MiB |
|
After Width: | Height: | Size: 572 KiB |
|
After Width: | Height: | Size: 654 KiB |
|
After Width: | Height: | Size: 351 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 3.2 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 716 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 568 B |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1,92 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
|
||||
// all in fixtures is set to tailwind v3 as interims solutions
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))',
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0',
|
||||
},
|
||||
to: {
|
||||
height: 'var(--radix-accordion-content-height)',
|
||||
},
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--radix-accordion-content-height)',
|
||||
},
|
||||
to: {
|
||||
height: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
}
|
||||
export default config
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"target": "ES6",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||