Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions src/app/store/[slug]/actions/subscribe/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Newsletter Subscription API Route
*
* Handles newsletter subscription requests
* Future integration point for email service (Resend)
*/

import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import prisma from "@/lib/prisma";

/**
* POST /store/[slug]/actions/subscribe
* Subscribe to newsletter
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ slug: string }> }
) {
try {
const { slug } = await params;
const body = await request.json();
const { email } = body;

// Validate email
if (!email || typeof email !== 'string') {
return NextResponse.json(
{ error: 'Email is required' },
{ status: 400 }
);
}

// Simple email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: 'Invalid email address' },
{ status: 400 }
);
}

// Get store from headers or slug
const headersList = await headers();
const storeId = headersList.get("x-store-id");

const store = await prisma.store.findFirst({
where: storeId
? { id: storeId, deletedAt: null }
: { slug, deletedAt: null },
select: { id: true, name: true },
});

if (!store) {
return NextResponse.json(
{ error: 'Store not found' },
{ status: 404 }
);
}

// TODO: Future implementation
// 1. Check if email already subscribed
// 2. Store subscription in database (create NewsletterSubscription model)
// 3. Send confirmation email via Resend
// 4. Handle double opt-in confirmation

// For now, just log and return success
console.log(`Newsletter subscription for ${store.name}: ${email}`);

// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 500));

return NextResponse.json(
{
success: true,
message: 'Successfully subscribed to newsletter'
},
{ status: 200 }
);
} catch (error) {
console.error('Newsletter subscription error:', error);
return NextResponse.json(
{ error: 'Failed to process subscription' },
{ status: 500 }
);
}
}
105 changes: 105 additions & 0 deletions src/app/store/[slug]/components/homepage/categories-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Link from "next/link";
import Image from "next/image";
import { Button } from "@/components/ui/button";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import type { CategoriesConfig } from "@/types/storefront-config";
import type { Category, StoreInfo } from "./types";

interface CategoriesSectionProps {
store: StoreInfo;
categories: Category[];
config: CategoriesConfig;
}

/**
* Categories Grid Section
* Displays product categories with configurable columns and product counts
* Supports 2, 3, or 4 column layouts
*/
export function CategoriesSection({
store,
categories,
config
}: CategoriesSectionProps) {
if (!config.enabled || categories.length === 0) {
return null;
}

// Limit categories based on config
const displayCategories = categories.slice(0, config.maxCount);

// Grid column classes based on config
const gridClasses = cn(
"grid gap-4 sm:gap-6",
{
"grid-cols-2": config.columns === 2,
"grid-cols-2 sm:grid-cols-3": config.columns === 3,
"grid-cols-2 sm:grid-cols-3 md:grid-cols-4": config.columns === 4,
}
);

return (
<section className="container mx-auto px-4 py-16">
{/* Section Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h2 className="text-3xl font-bold mb-2">Shop by Category</h2>
<p className="text-muted-foreground">
Explore our curated collections
</p>
</div>
<Button asChild variant="outline">
<Link href={`/store/${store.slug}/categories`}>
View All
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>

{/* Categories Grid */}
<div className={gridClasses}>
{displayCategories.map((category) => (
<Link
key={category.id}
href={`/store/${store.slug}/categories/${category.slug}`}
className="group relative rounded-xl overflow-hidden border-2 border-transparent hover:border-primary transition-all duration-300 hover:shadow-xl"
>
{/* Category Image */}
{category.image ? (
<div className="aspect-square bg-muted relative">
<Image
src={category.image}
alt={category.name}
fill
className="object-cover group-hover:scale-110 transition-transform duration-500"
unoptimized
sizes="(max-width: 640px) 50vw, (max-width: 768px) 33vw, 25vw"
/>
</div>
) : (
<div className="aspect-square bg-linear-to-br from-muted to-muted/50 flex items-center justify-center">
<span className="text-5xl opacity-30">📦</span>
</div>
)}

{/* Gradient Overlay */}
<div className="absolute inset-0 bg-linear-to-t from-black/70 via-black/20 to-transparent" />

{/* Category Info */}
<div className="absolute bottom-0 left-0 right-0 p-4">
<h3 className="text-white font-bold text-lg mb-1">
{category.name}
</h3>
{config.showProductCount && (
<p className="text-white/80 text-sm">
{category._count.products} {category._count.products === 1 ? 'product' : 'products'}
</p>
)}
</div>
</Link>
))}
</div>
</section>
);
}
74 changes: 74 additions & 0 deletions src/app/store/[slug]/components/homepage/featured-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { ArrowRight } from "lucide-react";
import { ProductGrid } from "../product-grid";
import type { FeaturedProductsConfig } from "@/types/storefront-config";
import type { Product, StoreInfo } from "./types";

interface FeaturedSectionProps {
store: StoreInfo;
products: Product[];
config: FeaturedProductsConfig;
}

/**
* Featured Products Section
* Displays featured products with configurable count and headings
* Includes empty state when no products are available
*/
export function FeaturedSection({
store,
products,
config
}: FeaturedSectionProps) {
if (!config.enabled) {
return null;
}

// Limit products based on config
const displayProducts = products.slice(0, config.count);

return (
<section className="container mx-auto px-4 py-16 bg-muted/30">
{/* Section Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h2 className="text-3xl font-bold mb-2">{config.heading}</h2>
{config.subheading && (
<p className="text-muted-foreground">
{config.subheading}
</p>
)}
</div>
<Button asChild variant="outline">
<Link href={`/store/${store.slug}/products`}>
View All
<ArrowRight className="ml-2 h-4 w-4" />
</Link>
</Button>
</div>

{/* Products or Empty State */}
{displayProducts.length === 0 ? (
<div className="text-center py-16 bg-card rounded-lg border-2 border-dashed">
<div className="text-6xl mb-4">🛍️</div>
<h3 className="text-xl font-semibold mb-2">No Featured Products Yet</h3>
<p className="text-muted-foreground mb-6">
Check back soon for amazing deals!
</p>
<Button asChild>
<Link href={`/store/${store.slug}/products`}>
Browse All Products
</Link>
</Button>
</div>
) : (
<ProductGrid
products={displayProducts}
storeSlug={store.slug}
columns={{ mobile: 1, tablet: 2, desktop: 4 }}
/>
)}
</section>
);
}
126 changes: 126 additions & 0 deletions src/app/store/[slug]/components/homepage/hero-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ShoppingBag, ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import type { HeroConfig } from "@/types/storefront-config";
import type { StoreInfo } from "./types";

interface HeroSectionProps {
store: StoreInfo;
config: HeroConfig;
}

/**
* Hero Section Component
* Supports multiple styles: gradient, image, minimal
* Configurable headline, subheadline, and CTA buttons
*/
export function HeroSection({ store, config }: HeroSectionProps) {
const {
style = 'gradient',
headline,
subheadline,
primaryCta,
secondaryCta,
backgroundImage,
} = config;

// Base styles
const baseClasses = "relative overflow-hidden";

// Style-specific classes
const styleClasses = {
gradient: "bg-linear-to-br from-blue-50 via-indigo-50 to-purple-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900",
image: "bg-cover bg-center bg-no-repeat",
minimal: "bg-background",
};

return (
<section
className={cn(baseClasses, styleClasses[style])}
style={style === 'image' && backgroundImage ? { backgroundImage: `url(${backgroundImage})` } : undefined}
>
{/* Grid pattern overlay for gradient style */}
{style === 'gradient' && (
<div className="absolute inset-0 bg-grid-slate-900/[0.04] bg-size-[40px_40px]" />
)}

{/* Dark overlay for image style */}
{style === 'image' && (
<div className="absolute inset-0 bg-black/40" />
)}

<div className="relative container mx-auto px-4 py-20 sm:py-28 lg:py-32">
<div className="max-w-3xl mx-auto text-center space-y-8">
{/* Store Badge */}
<Badge
variant="secondary"
className={cn(
"text-sm",
style === 'image' && "bg-white/90 text-black hover:bg-white"
)}
>
Welcome to {store.name}
</Badge>

{/* Headline */}
<h1
className={cn(
"text-4xl sm:text-5xl lg:text-6xl font-bold tracking-tight",
style === 'image' && "text-white"
)}
>
{headline || (
<>
Discover Amazing
<span className="bg-linear-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
{" "}Products{" "}
</span>
Today
</>
)}
</h1>

{/* Subheadline */}
{subheadline && (
<p
className={cn(
"text-lg sm:text-xl max-w-2xl mx-auto",
style === 'image' ? "text-white/90" : "text-muted-foreground"
)}
>
{subheadline}
</p>
)}

{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center">
{primaryCta && (
<Button asChild size="lg" className="text-base">
<Link href={primaryCta.href}>
<ShoppingBag className="mr-2 h-5 w-5" />
{primaryCta.text}
</Link>
</Button>
)}

{secondaryCta && (
<Button
asChild
size="lg"
variant={style === 'image' ? 'secondary' : 'outline'}
className="text-base"
>
<Link href={secondaryCta.href}>
{secondaryCta.text}
<ArrowRight className="ml-2 h-5 w-5" />
</Link>
</Button>
)}
</div>
</div>
</div>
</section>
);
}
Loading