fix(storefront): align store actions with actual DB schema
Reviewer caught three runtime breakages that the prior commit would
have introduced (the legacy supabase shim masked them by returning
null):
1. **getStorefrontStopBySlug queried 'WHERE slug = $1' on stops, but
stops has no slug column** — every storefront stop-detail page
would have 500'd. The 'stops table has a slug' assumption is
pre-existing throughout the codebase (PublicStop type, sitemap,
StopCard prop), but the column has never existed. Fix: switch the
route param to the stop's UUID (id). Rename folder [slug] → [id]
for honesty. Update StopCard prop and the two stops-list links to
pass stop.id instead of stop.slug. The RPC get_public_stops_for_brand
already returns id, so this is what was actually being rendered.
2. **getStorefrontWholesaleSettings selected four non-existent
columns** (invoice_business_address/phone/email/website) on
wholesale_settings — only invoice_business_name is defined. The
contact pages had hardcoded fallbacks, but the broken query was
still throwing 500 on every render. Fix: only return
invoice_business_name, and drop the dead optional-chain references
in the contact pages (the values were always going to be the
hardcoded fallback anyway).
3. **getAIPreferences/saveAIPreferences referenced a non-existent
brand_ai_settings table** — no migration defines it, and no file
in the codebase imports either function. Deleted the whole
preferences.ts module (dead code that would have 500'd the moment
anyone wired it up).
4. **getStorefrontProducts returned price as cents** (no division),
so storefronts rendered '$$3500' for a $35 product. Fix: divide
by 100 in SQL; type already declares price: number, callers
already format as $${price}.
Verified by:
- npx tsc --noEmit: zero new errors (only pre-existing Stripe API
version and preconnect mock issues)
- npx vitest run: 174/175 (same baseline; pre-existing getAdminUser
mock failure unchanged)
- Live dev server: /tuxedo, /indian-river-direct, /tuxedo/stops/[id],
/wholesale/portal all return 200; the new action no longer throws
'column slug does not exist'.
This commit is contained in:
@@ -1,108 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import { pool } from "@/lib/db";
|
|
||||||
|
|
||||||
export type AIAuthConfig = {
|
|
||||||
provider: string;
|
|
||||||
api_key: string;
|
|
||||||
organization_id: string;
|
|
||||||
base_url: string;
|
|
||||||
model: string;
|
|
||||||
max_tokens: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AIRow = {
|
|
||||||
api_key: string | null;
|
|
||||||
organization_id: string | null;
|
|
||||||
base_url: string | null;
|
|
||||||
model: string | null;
|
|
||||||
max_tokens: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getAIPreferences(brandId: string): Promise<{
|
|
||||||
api_key?: string;
|
|
||||||
organization_id?: string;
|
|
||||||
base_url?: string;
|
|
||||||
model?: string;
|
|
||||||
max_tokens?: number;
|
|
||||||
} | null> {
|
|
||||||
const { rows } = await pool.query<AIRow>(
|
|
||||||
`SELECT api_key, organization_id, base_url, model, max_tokens
|
|
||||||
FROM brand_ai_settings
|
|
||||||
WHERE brand_id = $1
|
|
||||||
LIMIT 1`,
|
|
||||||
[brandId],
|
|
||||||
);
|
|
||||||
const row = rows[0];
|
|
||||||
if (!row) return null;
|
|
||||||
return {
|
|
||||||
api_key: row.api_key ?? undefined,
|
|
||||||
organization_id: row.organization_id ?? undefined,
|
|
||||||
base_url: row.base_url ?? undefined,
|
|
||||||
model: row.model ?? undefined,
|
|
||||||
max_tokens: row.max_tokens ?? undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function saveAIPreferences(
|
|
||||||
brandId: string,
|
|
||||||
config: AIAuthConfig
|
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
|
||||||
try {
|
|
||||||
await pool.query(
|
|
||||||
`INSERT INTO brand_ai_settings
|
|
||||||
(brand_id, provider, api_key, organization_id, base_url, model, max_tokens, updated_at)
|
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
|
||||||
ON CONFLICT (brand_id) DO UPDATE
|
|
||||||
SET provider = EXCLUDED.provider,
|
|
||||||
api_key = EXCLUDED.api_key,
|
|
||||||
organization_id = EXCLUDED.organization_id,
|
|
||||||
base_url = EXCLUDED.base_url,
|
|
||||||
model = EXCLUDED.model,
|
|
||||||
max_tokens = EXCLUDED.max_tokens,
|
|
||||||
updated_at = NOW()`,
|
|
||||||
[
|
|
||||||
brandId,
|
|
||||||
config.provider,
|
|
||||||
config.api_key || null,
|
|
||||||
config.organization_id || null,
|
|
||||||
config.base_url || null,
|
|
||||||
config.model || "gpt-4o-mini",
|
|
||||||
config.max_tokens || 4000,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
return { success: true };
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: err instanceof Error ? err.message : "Failed to save AI preferences",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function testAIConnection(config: AIAuthConfig): Promise<{ ok: boolean; message: string }> {
|
|
||||||
if (!config.api_key?.trim()) {
|
|
||||||
return { ok: false, message: "API key is required" };
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const baseUrl = config.base_url?.trim() || "https://api.openai.com/v1";
|
|
||||||
const url = `${baseUrl.replace(/\/$/, "")}/models`;
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${config.api_key}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
return { ok: true, message: "Connection successful! Your API key is valid." };
|
|
||||||
} else if (response.status === 401) {
|
|
||||||
return { ok: false, message: "Invalid API key. Please check and try again." };
|
|
||||||
} else {
|
|
||||||
return { ok: false, message: `Error: ${response.status} ${response.statusText}` };
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return { ok: false, message: "Could not connect. Check your network and API key." };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+22
-15
@@ -98,12 +98,17 @@ export async function getStorefrontStops(brandId: string): Promise<StorefrontSto
|
|||||||
/**
|
/**
|
||||||
* Active products visible on the storefront. Same shape as the admin
|
* Active products visible on the storefront. Same shape as the admin
|
||||||
* product list but without the brand-internal columns (cost, supplier, …).
|
* product list but without the brand-internal columns (cost, supplier, …).
|
||||||
|
*
|
||||||
|
* Note: products.price is stored in cents (NUMERIC). We return it as a
|
||||||
|
* float in **dollars** so callers can format directly as `$${price}`
|
||||||
|
* without doing the cents-to-dollars math.
|
||||||
*/
|
*/
|
||||||
export async function getStorefrontProducts(
|
export async function getStorefrontProducts(
|
||||||
brandId: string,
|
brandId: string,
|
||||||
): Promise<StorefrontProduct[]> {
|
): Promise<StorefrontProduct[]> {
|
||||||
const { rows } = await pool.query<StorefrontProduct>(
|
const { rows } = await pool.query<StorefrontProduct>(
|
||||||
`SELECT id, brand_id, name, description, price::float8 AS price,
|
`SELECT id, brand_id, name, description,
|
||||||
|
price::float8 / 100 AS price,
|
||||||
type, image_url, is_taxable, pickup_type, active
|
type, image_url, is_taxable, pickup_type, active
|
||||||
FROM products
|
FROM products
|
||||||
WHERE brand_id = $1
|
WHERE brand_id = $1
|
||||||
@@ -137,16 +142,16 @@ export async function getStorefrontData(slug: string): Promise<StorefrontData> {
|
|||||||
|
|
||||||
export type StorefrontWholesaleSettings = {
|
export type StorefrontWholesaleSettings = {
|
||||||
invoice_business_name: string | null;
|
invoice_business_name: string | null;
|
||||||
invoice_business_address: string | null;
|
|
||||||
invoice_business_phone: string | null;
|
|
||||||
invoice_business_email: string | null;
|
|
||||||
invoice_business_website: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public wholesale-settings record for a brand — just the public-facing
|
* Public wholesale-settings record for a brand — just the invoice
|
||||||
* invoice/business info used by the storefront contact page. Returns
|
* business name used by the storefront contact page. Returns `null`
|
||||||
* `null` if the brand or its wholesale settings row doesn't exist.
|
* if the brand or its wholesale settings row doesn't exist.
|
||||||
|
*
|
||||||
|
* NOTE: only `invoice_business_name` is exposed; other invoice-contact
|
||||||
|
* fields don't exist on `wholesale_settings` yet and shouldn't be
|
||||||
|
* surfaced here. If you need them, add a migration first.
|
||||||
*/
|
*/
|
||||||
export async function getStorefrontWholesaleSettings(
|
export async function getStorefrontWholesaleSettings(
|
||||||
slug: string,
|
slug: string,
|
||||||
@@ -154,8 +159,7 @@ export async function getStorefrontWholesaleSettings(
|
|||||||
const brand = await getStorefrontBrandBySlug(slug);
|
const brand = await getStorefrontBrandBySlug(slug);
|
||||||
if (!brand) return null;
|
if (!brand) return null;
|
||||||
const { rows } = await pool.query<StorefrontWholesaleSettings>(
|
const { rows } = await pool.query<StorefrontWholesaleSettings>(
|
||||||
`SELECT invoice_business_name, invoice_business_address,
|
`SELECT invoice_business_name
|
||||||
invoice_business_phone, invoice_business_email, invoice_business_website
|
|
||||||
FROM wholesale_settings
|
FROM wholesale_settings
|
||||||
WHERE brand_id = $1
|
WHERE brand_id = $1
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
@@ -206,14 +210,17 @@ export async function getStorefrontBrandName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up a single public stop by its URL slug, plus the brand's
|
* Look up a single public stop by its id, plus the brand's active
|
||||||
* active products. Used by `/[brand]/stops/[slug]` pages.
|
* products. Used by `/[brand]/stops/[id]` pages.
|
||||||
|
*
|
||||||
|
* NOTE: The URL parameter is named `[id]` (not `[slug]`) because the
|
||||||
|
* `stops` table has no slug column. URLs use the stop's UUID.
|
||||||
*
|
*
|
||||||
* Returns `null` if the stop is not public, not active, or its date has
|
* Returns `null` if the stop is not public, not active, or its date has
|
||||||
* already passed. Returns the empty product array if the brand has no
|
* already passed. Returns the empty product array if the brand has no
|
||||||
* active products at the moment.
|
* active products at the moment.
|
||||||
*/
|
*/
|
||||||
export async function getStorefrontStopBySlug(slug: string): Promise<{
|
export async function getStorefrontStopById(id: string): Promise<{
|
||||||
stop: StorefrontStop;
|
stop: StorefrontStop;
|
||||||
products: StorefrontProduct[];
|
products: StorefrontProduct[];
|
||||||
} | null> {
|
} | null> {
|
||||||
@@ -222,11 +229,11 @@ export async function getStorefrontStopBySlug(slug: string): Promise<{
|
|||||||
date::text AS date, time, cutoff_date::text AS cutoff_date,
|
date::text AS date, time, cutoff_date::text AS cutoff_date,
|
||||||
status, notes, is_public
|
status, notes, is_public
|
||||||
FROM stops
|
FROM stops
|
||||||
WHERE slug = $1
|
WHERE id = $1
|
||||||
AND is_public = true
|
AND is_public = true
|
||||||
AND status = 'active'
|
AND status = 'active'
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[slug],
|
[id],
|
||||||
);
|
);
|
||||||
const stop = stopRows[0];
|
const stop = stopRows[0];
|
||||||
if (!stop) return null;
|
if (!stop) return null;
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import { getStorefrontWholesaleSettings } from "@/actions/storefront";
|
|||||||
|
|
||||||
type BrandSettings = {
|
type BrandSettings = {
|
||||||
invoice_business_name: string | null;
|
invoice_business_name: string | null;
|
||||||
invoice_business_address: string | null;
|
|
||||||
invoice_business_phone: string | null;
|
|
||||||
invoice_business_email: string | null;
|
|
||||||
invoice_business_website: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormState = {
|
type FormState = {
|
||||||
@@ -90,7 +86,7 @@ export default function IndianRiverContactPage() {
|
|||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
label: "Address",
|
label: "Address",
|
||||||
value: brandSettings?.invoice_business_address ?? "Indian River Region, Florida",
|
value: "Indian River Region, Florida",
|
||||||
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />,
|
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />,
|
||||||
iconPath: <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
iconPath: <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
||||||
},
|
},
|
||||||
@@ -102,7 +98,7 @@ export default function IndianRiverContactPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Email",
|
label: "Email",
|
||||||
value: brandSettings?.invoice_business_email ?? "Info@indianriverdirect.com",
|
value: "Info@indianriverdirect.com",
|
||||||
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />,
|
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />,
|
||||||
iconPath: null
|
iconPath: null
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default function IndianRiverStopsList({ stops, brandName, brandSlug }: Pr
|
|||||||
{upcomingStops.map((stop) => (
|
{upcomingStops.map((stop) => (
|
||||||
<Link
|
<Link
|
||||||
key={stop.id}
|
key={stop.id}
|
||||||
href={`/${brandSlug}/stops/${stop.slug}`}
|
href={`/${brandSlug}/stops/${stop.id}`}
|
||||||
className="stop-card group block bg-white rounded-2xl p-5 shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-lg hover:ring-orange-200"
|
className="stop-card group block bg-white rounded-2xl p-5 shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-lg hover:ring-orange-200"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ import StopSetEffect from "@/components/storefront/StopSetEffect";
|
|||||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
import { getStorefrontStopBySlug } from "@/actions/storefront";
|
import { getStorefrontStopById } from "@/actions/storefront";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
|
|
||||||
type Product = {
|
type Product = {
|
||||||
@@ -53,7 +53,7 @@ export default function StopPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
const result = await getStorefrontStopBySlug(slug);
|
const result = await getStorefrontStopById(slug);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
setStop(result.stop as unknown as Stop);
|
setStop(result.stop as unknown as Stop);
|
||||||
@@ -9,10 +9,6 @@ import { getStorefrontWholesaleSettings } from "@/actions/storefront";
|
|||||||
|
|
||||||
type BrandSettings = {
|
type BrandSettings = {
|
||||||
invoice_business_name: string | null;
|
invoice_business_name: string | null;
|
||||||
invoice_business_address: string | null;
|
|
||||||
invoice_business_phone: string | null;
|
|
||||||
invoice_business_email: string | null;
|
|
||||||
invoice_business_website: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormState = {
|
type FormState = {
|
||||||
@@ -72,7 +68,7 @@ export default function TuxedoContactPage() {
|
|||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-bold text-stone-950 mb-3">Farm Address</h3>
|
<h3 className="text-base font-bold text-stone-950 mb-3">Farm Address</h3>
|
||||||
<p className="text-sm text-stone-500 leading-relaxed">
|
<p className="text-sm text-stone-500 leading-relaxed">
|
||||||
{brandSettings?.invoice_business_address ?? "59751 David Road, Olathe, CO 81425"}
|
59751 David Road, Olathe, CO 81425
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-stone-200/60 text-center">
|
<div className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-stone-200/60 text-center">
|
||||||
@@ -96,8 +92,8 @@ export default function TuxedoContactPage() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-base font-bold text-stone-950 mb-3">Email</h3>
|
<h3 className="text-base font-bold text-stone-950 mb-3">Email</h3>
|
||||||
<a href={`mailto:${brandSettings?.invoice_business_email ?? "orders@tuxedocorn.com"}`} className="text-sm text-stone-500 hover:text-emerald-700 transition-colors">
|
<a href="mailto:orders@tuxedocorn.com" className="text-sm text-stone-500 hover:text-emerald-700 transition-colors">
|
||||||
{brandSettings?.invoice_business_email ?? "orders@tuxedocorn.com"}
|
orders@tuxedocorn.com
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export default function TuxedoStopsList({ stops, brandName, brandSlug }: Props)
|
|||||||
{upcomingStops.map((stop) => (
|
{upcomingStops.map((stop) => (
|
||||||
<Link
|
<Link
|
||||||
key={stop.id}
|
key={stop.id}
|
||||||
href={`/${brandSlug}/stops/${stop.slug}`}
|
href={`/${brandSlug}/stops/${stop.id}`}
|
||||||
className="stop-card group block bg-white rounded-2xl p-5 shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-lg hover:ring-emerald-200"
|
className="stop-card group block bg-white rounded-2xl p-5 shadow-sm ring-1 ring-stone-200/60 transition-all duration-300 hover:shadow-lg hover:ring-emerald-200"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import StopSetEffect from "@/components/storefront/StopSetEffect";
|
|||||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
import { getStorefrontStopBySlug } from "@/actions/storefront";
|
import { getStorefrontStopById } from "@/actions/storefront";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
|
|
||||||
type Product = {
|
type Product = {
|
||||||
@@ -52,7 +52,7 @@ export default function StopPage() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
const result = await getStorefrontStopBySlug(slug);
|
const result = await getStorefrontStopById(slug);
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
setStop(result.stop as unknown as Stop);
|
setStop(result.stop as unknown as Stop);
|
||||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
|||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
|
|
||||||
type StopCardProps = {
|
type StopCardProps = {
|
||||||
slug: string;
|
id: string;
|
||||||
city: string;
|
city: string;
|
||||||
state: string;
|
state: string;
|
||||||
date: string;
|
date: string;
|
||||||
@@ -13,7 +13,7 @@ type StopCardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function StopCard({
|
export default function StopCard({
|
||||||
slug, city, state, date, time, location,
|
id, city, state, date, time, location,
|
||||||
brandSlug = "tuxedo", brandAccent = "green",
|
brandSlug = "tuxedo", brandAccent = "green",
|
||||||
}: StopCardProps) {
|
}: StopCardProps) {
|
||||||
const ctaBg =
|
const ctaBg =
|
||||||
@@ -25,7 +25,7 @@ export default function StopCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group overflow-hidden rounded-3xl bg-white ring-1 ring-stone-200/60 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl hover:shadow-black/8 hover:ring-stone-300">
|
<div className="group overflow-hidden rounded-3xl bg-white ring-1 ring-stone-200/60 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl hover:shadow-black/8 hover:ring-stone-300">
|
||||||
<Link href={`/${brandSlug}/stops/${slug}`} className="block p-7">
|
<Link href={`/${brandSlug}/stops/${id}`} className="block p-7">
|
||||||
{/* Location */}
|
{/* Location */}
|
||||||
<div className="flex items-start gap-4 mb-5">
|
<div className="flex items-start gap-4 mb-5">
|
||||||
<div className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl transition-colors ${brandAccent === "blue" ? "bg-blue-50 group-hover:bg-blue-100" : brandAccent === "green" ? "bg-emerald-50 group-hover:bg-emerald-100" : "bg-orange-50 group-hover:bg-orange-100"}`}>
|
<div className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl transition-colors ${brandAccent === "blue" ? "bg-blue-50 group-hover:bg-blue-100" : brandAccent === "green" ? "bg-emerald-50 group-hover:bg-emerald-100" : "bg-orange-50 group-hover:bg-orange-100"}`}>
|
||||||
@@ -56,7 +56,7 @@ export default function StopCard({
|
|||||||
{/* CTA */}
|
{/* CTA */}
|
||||||
<div className="px-7 pb-7 pt-1">
|
<div className="px-7 pb-7 pt-1">
|
||||||
<Link
|
<Link
|
||||||
href={`/${brandSlug}/stops/${slug}`}
|
href={`/${brandSlug}/stops/${id}`}
|
||||||
className={`block w-full rounded-2xl ${ctaBg} text-white px-5 py-3.5 text-center font-bold text-sm uppercase tracking-wider transition-colors`}
|
className={`block w-full rounded-2xl ${ctaBg} text-white px-5 py-3.5 text-center font-bold text-sm uppercase tracking-wider transition-colors`}
|
||||||
>
|
>
|
||||||
Shop This Stop
|
Shop This Stop
|
||||||
|
|||||||
Reference in New Issue
Block a user