diff --git a/src/app/api/admin/delivery-rates/route.ts b/src/app/api/admin/delivery-rates/route.ts
new file mode 100644
index 0000000..4998c7a
--- /dev/null
+++ b/src/app/api/admin/delivery-rates/route.ts
@@ -0,0 +1,27 @@
+import { NextResponse } from 'next/server'
+import { readDeliveryRates, writeDeliveryRates } from '@/lib/delivery-rates'
+import type { DeliveryRatesConfig } from '@/lib/delivery'
+
+export const dynamic = 'force-dynamic'
+
+export async function GET() {
+ return NextResponse.json(readDeliveryRates())
+}
+
+export async function PUT(req: Request) {
+ try {
+ const body = (await req.json()) as DeliveryRatesConfig
+ const tiers = ['dropoff', 'classic', 'organic'] as const
+ for (const tier of tiers) {
+ const t = body[tier]
+ if (!t || typeof t.base !== 'number' || typeof t.perMile !== 'number' || typeof t.label !== 'string') {
+ return NextResponse.json({ error: `Invalid config for tier: ${tier}` }, { status: 400 })
+ }
+ }
+ writeDeliveryRates(body)
+ return NextResponse.json({ ok: true })
+ } catch (err) {
+ console.error('[admin/delivery-rates] error:', err)
+ return NextResponse.json({ error: 'Failed to save delivery rates' }, { status: 500 })
+ }
+}
diff --git a/src/app/api/admin/items/route.ts b/src/app/api/admin/items/route.ts
index efc7a71..3a90bce 100644
--- a/src/app/api/admin/items/route.ts
+++ b/src/app/api/admin/items/route.ts
@@ -11,18 +11,47 @@ export async function GET() {
const withOverrides = items.map((item) => {
const ov = overrides[item.id] ?? {}
+ const toSlug = (n: string) => n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
+
+ // Resolve categories (same logic as catalog route)
+ const resolvedCats = ov.categoriesOverride?.length
+ ? (() => {
+ const cats = ov.categoriesOverride!.map(toSlug)
+ return {
+ categories: cats,
+ categoryLabels: ov.categoriesOverride!,
+ category: cats[0],
+ categoryLabel: ov.categoriesOverride![0],
+ }
+ })()
+ : {
+ category: ov.categoryOverride ?? item.category,
+ categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
+ categories: ov.categoryOverride
+ ? [ov.categoryOverride, ...(item.categories ?? [item.category]).slice(1)]
+ : (item.categories ?? [item.category]),
+ categoryLabels: ov.categoryLabelOverride
+ ? [ov.categoryLabelOverride, ...(item.categoryLabels ?? [item.categoryLabel]).slice(1)]
+ : (item.categoryLabels ?? [item.categoryLabel]),
+ }
+
return {
...item,
// Resolved values (what the customer sees)
hidden: ov.hidden ?? false,
- category: ov.categoryOverride ?? item.category,
- categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
+ featured: ov.featured ?? item.featured ?? false,
+ ...resolvedCats,
sortOrder: ov.sortOrder ?? 0,
showColors: ov.showColors != null ? ov.showColors : item.showColors,
colorMin: ov.colorMin ?? item.colorMin,
colorMax: ov.colorMax !== undefined ? ov.colorMax : item.colorMax,
chromeSurchargePerColor: ov.chromeSurchargePerColor ?? item.chromeSurchargePerColor,
+ disabledColors: ov.disabledColors?.length ? ov.disabledColors : item.disabledColors,
+ requiresDelivery: ov.requiresDelivery != null ? ov.requiresDelivery : item.requiresDelivery,
+ deliveryBaseOverride: ov.deliveryBaseOverride !== undefined ? ov.deliveryBaseOverride : item.deliveryBaseOverride,
+ deliveryPerMileOverride: ov.deliveryPerMileOverride !== undefined ? ov.deliveryPerMileOverride : item.deliveryPerMileOverride,
description: ov.descriptionOverride ?? item.description,
+ variations: item.variations.filter((v) => !(ov.hiddenVariationIds ?? []).includes(v.id)),
modifiers: item.modifiers
.filter((m) => !(ov.hiddenModifierIds ?? []).includes(m.id))
.map((m) => {
@@ -33,6 +62,7 @@ export async function GET() {
_rawCategory: item.category,
_rawCategoryLabel: item.categoryLabel,
_rawShowColors: item.showColors,
+ _rawVariations: item.variations,
_rawModifiers: item.modifiers,
_rawDescription: item.description,
_override: ov,
diff --git a/src/app/api/catalog/route.ts b/src/app/api/catalog/route.ts
index 62afba7..73bb345 100644
--- a/src/app/api/catalog/route.ts
+++ b/src/app/api/catalog/route.ts
@@ -3,6 +3,8 @@ import { getCatalog } from '@/lib/catalog-cache'
import { readOverrides } from '@/lib/overrides'
import type { CatalogItem } from '@/data/mock-catalog'
+export const dynamic = 'force-dynamic'
+
function applyOverrides(items: CatalogItem[]): CatalogItem[] {
const overrides = readOverrides()
@@ -12,16 +14,44 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
if (!ov) return item
return {
...item,
- category: ov.categoryOverride ?? item.category,
- categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
+ featured: ov.featured ?? item.featured,
+ // categoriesOverride (array of names) takes precedence over the old single-field overrides
+ ...(ov.categoriesOverride?.length
+ ? (() => {
+ const toSlug = (n: string) => n.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
+ const cats = ov.categoriesOverride!.map(toSlug)
+ return {
+ categories: cats,
+ categoryLabels: ov.categoriesOverride!,
+ category: cats[0],
+ categoryLabel: ov.categoriesOverride![0],
+ }
+ })()
+ : {
+ category: ov.categoryOverride ?? item.category,
+ categoryLabel: ov.categoryLabelOverride ?? item.categoryLabel,
+ categories: ov.categoryOverride
+ ? [ov.categoryOverride, ...(item.categories ?? [item.category]).slice(1)]
+ : (item.categories ?? [item.category]),
+ categoryLabels: ov.categoryLabelOverride
+ ? [ov.categoryLabelOverride, ...(item.categoryLabels ?? [item.categoryLabel]).slice(1)]
+ : (item.categoryLabels ?? [item.categoryLabel]),
+ }
+ ),
showColors: ov.showColors != null ? ov.showColors : item.showColors,
colorMin: ov.colorMin ?? item.colorMin,
colorMax: ov.colorMax !== undefined ? ov.colorMax : item.colorMax,
chromeSurchargePerColor: ov.chromeSurchargePerColor ?? item.chromeSurchargePerColor,
- quantityUnit: ov.quantityUnit ?? item.quantityUnit,
+ disabledColors: ov.disabledColors?.length ? ov.disabledColors : item.disabledColors,
+ quantityUnit: ov.quantityUnit ?? item.quantityUnit,
+ requiresDelivery: ov.requiresDelivery != null ? ov.requiresDelivery : item.requiresDelivery,
+ deliveryBaseOverride: ov.deliveryBaseOverride !== undefined ? ov.deliveryBaseOverride : item.deliveryBaseOverride,
+ deliveryPerMileOverride: ov.deliveryPerMileOverride !== undefined ? ov.deliveryPerMileOverride : item.deliveryPerMileOverride,
vinylEnabled: ov.vinylEnabled ?? item.vinylEnabled,
vinylPromo: ov.vinylPromo ?? item.vinylPromo,
description: ov.descriptionOverride ?? item.description,
+ variations: item.variations
+ .filter((v) => !(ov.hiddenVariationIds ?? []).includes(v.id)),
modifiers: item.modifiers
.filter((m) => !(ov.hiddenModifierIds ?? []).includes(m.id))
.map((m) => {
@@ -32,6 +62,8 @@ function applyOverrides(items: CatalogItem[]): CatalogItem[] {
})
.filter((item) => !(overrides[item.id]?.hidden))
.sort((a, b) => {
+ const featDiff = (b.featured ? 1 : 0) - (a.featured ? 1 : 0)
+ if (featDiff !== 0) return featDiff
const aOrder = overrides[a.id]?.sortOrder ?? 0
const bOrder = overrides[b.id]?.sortOrder ?? 0
return aOrder - bOrder
diff --git a/src/app/api/delivery-quote/route.ts b/src/app/api/delivery-quote/route.ts
index b3a64f7..abf64e0 100644
--- a/src/app/api/delivery-quote/route.ts
+++ b/src/app/api/delivery-quote/route.ts
@@ -1,10 +1,12 @@
import { NextResponse } from 'next/server'
import { geocode, calcDelivery, inferTier } from '@/lib/delivery'
+import { readDeliveryRates } from '@/lib/delivery-rates'
export async function POST(request: Request) {
- const { address, itemNames } = await request.json() as {
- address: string
- itemNames: string[]
+ const { address, itemNames, rateOverride } = await request.json() as {
+ address: string
+ itemNames: string[]
+ rateOverride?: { base: number; perMile: number }
}
if (!address?.trim()) {
@@ -17,7 +19,18 @@ export async function POST(request: Request) {
}
const tier = inferTier(itemNames ?? [])
- const quote = await calcDelivery(coords.lat, coords.lng, tier)
+ const rates = readDeliveryRates()
+
+ // Apply per-item rate override if provided (overrides just base and perMile for the inferred tier)
+ if (rateOverride) {
+ rates[tier] = {
+ ...rates[tier],
+ base: rateOverride.base,
+ perMile: rateOverride.perMile,
+ }
+ }
+
+ const quote = await calcDelivery(coords.lat, coords.lng, tier, rates)
if (quote.miles > 40) {
return NextResponse.json(
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 032c4e2..40387ce 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -4,6 +4,7 @@ import Navbar from '@/components/Navbar'
import Footer from '@/components/Footer'
import CartDrawer from '@/components/CartDrawer'
import CartFab from '@/components/CartFab'
+import ScrollToTop from '@/components/ScrollToTop'
import { CartProvider } from '@/context/CartContext'
export const metadata: Metadata = {
@@ -49,6 +50,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
{children}
+