Initialized repository for project Auntysparkles.com clone

Co-authored-by: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com>
This commit is contained in:
v0 2025-09-14 09:40:55 +00:00
commit 5f0a40b555
68 changed files with 7314 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@ -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

30
README.md Normal file
View File

@ -0,0 +1,30 @@
# Auntysparkles.com clone
*Automatically synced with your [v0.app](https://v0.app) deployments*
[![Deployed on Vercel](https://img.shields.io/badge/Deployed%20on-Vercel-black?style=for-the-badge&logo=vercel)](https://vercel.com/jeff-emmetts-projects/v0-auntysparkles-com-clone)
[![Built with v0](https://img.shields.io/badge/Built%20with-v0.app-black?style=for-the-badge)](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

212
app/about/page.tsx Normal file
View File

@ -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 fashionthe 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 everywhereespecially 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 decadesor even centuriesto 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
meaningfulproving 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
canvasthey'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>
)
}

View File

@ -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 })
}
}

View File

@ -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)
}
}

View File

@ -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 })
}
}

View File

@ -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 })
}
}

View File

@ -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 })
}
}

View File

@ -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(),
})
}

View File

@ -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",
},
})
}

View File

@ -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 })
}
}

View File

@ -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 })
}
}

View File

@ -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 })
}
}

View File

@ -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 })
}
}

17
app/api/test-env/route.ts Normal file
View File

@ -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(),
})
}

174
app/contact/page.tsx Normal file
View File

@ -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>
)
}

160
app/gallery/page.tsx Normal file
View File

@ -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 samejust 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>
)
}

41
app/globals.css Normal file
View File

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

32
app/layout.tsx Normal file
View File

@ -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>
)
}

210
app/page.tsx Normal file
View File

@ -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>
)
}

483
app/shop/page.tsx Normal file
View File

@ -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>
)
}

21
components.json Normal file
View File

@ -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"
}

View File

@ -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>
)
}

127
components/site-footer.tsx Normal file
View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
}

59
components/ui/alert.tsx Normal file
View File

@ -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 }

36
components/ui/badge.tsx Normal file
View File

@ -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 }

56
components/ui/button.tsx Normal file
View File

@ -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 }

79
components/ui/card.tsx Normal file
View File

@ -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 }

22
components/ui/input.tsx Normal file
View File

@ -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 }

117
lib/square-client.ts Normal file
View File

@ -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
}

77
lib/square-types.ts Normal file
View File

@ -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
}

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

14
next.config.mjs Normal file
View File

@ -0,0 +1,14 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
images: {
unoptimized: true,
},
}
export default nextConfig

73
package.json Normal file
View File

@ -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"
}
}

3752
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
}
export default config

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 716 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

BIN
public/placeholder-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

View File

@ -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

BIN
public/placeholder-user.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/placeholder.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/placeholder.svg Normal file
View File

@ -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

92
styles/globals.css Normal file
View File

@ -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;
}
}

98
tailwind.config.ts Normal file
View File

@ -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

27
tsconfig.json Normal file
View File

@ -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"]
}