diff --git a/src/app/[country]/[locale]/(checkout)/__tests__/layout.test.tsx b/src/app/[country]/[locale]/(checkout)/__tests__/layout.test.tsx new file mode 100644 index 00000000..6a13027d --- /dev/null +++ b/src/app/[country]/[locale]/(checkout)/__tests__/layout.test.tsx @@ -0,0 +1,35 @@ +import { render, screen } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("next-intl", () => ({ + useTranslations: () => (key: string) => key, +})); + +vi.mock("@/contexts/CheckoutContext", () => ({ + CheckoutProvider: ({ children }: { children: ReactNode }) => children, + CheckoutSummary: () => null, +})); + +vi.mock("@/contexts/TenantContext", () => ({ + useTenantConfig: () => ({ storeName: "Test Store" }), +})); + +import CheckoutLayout from "../layout"; + +describe("CheckoutLayout branding", () => { + it("renders the OLITT logo and never the Spree logo", () => { + render( + +
checkout body
+
, + ); + + const logos = screen.getAllByRole("img", { name: "Test Store" }); + expect(logos.length).toBeGreaterThan(0); + for (const logo of logos) { + expect(logo).toHaveAttribute("src", "/olitt-logo.svg"); + expect(logo.getAttribute("src")).not.toContain("spree"); + } + }); +}); diff --git a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx index a7243d97..61ff1413 100644 --- a/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx +++ b/src/app/[country]/[locale]/(checkout)/checkout/[id]/page.tsx @@ -86,8 +86,6 @@ async function CheckoutDataLoader({ params }: CheckoutPageProps) { @@ -101,8 +99,6 @@ async function CheckoutDataLoader({ params }: CheckoutPageProps) { diff --git a/src/app/[country]/[locale]/(checkout)/layout.tsx b/src/app/[country]/[locale]/(checkout)/layout.tsx index e727b428..2396a818 100644 --- a/src/app/[country]/[locale]/(checkout)/layout.tsx +++ b/src/app/[country]/[locale]/(checkout)/layout.tsx @@ -23,7 +23,7 @@ function CheckoutHeader() { const tenantConfig = useTenantConfig(); const branding = resolveTenantBranding(tenantConfig, { name: tenantConfig.storeName ?? "Store", - logoUrl: "/spree.png", + logoUrl: "/olitt-logo.svg", }); const navigation = resolveTenantNavigation(tenantConfig); @@ -31,7 +31,7 @@ function CheckoutHeader() {
{branding.name +
{/* Mobile header */} -
+
@@ -175,13 +166,7 @@ function CheckoutLayoutContent({ children }: CheckoutLayoutProps) {
{/* Desktop summary sidebar — Shopify: light gray bg with left border */} -
+
diff --git a/src/app/[country]/[locale]/(storefront)/page.tsx b/src/app/[country]/[locale]/(storefront)/page.tsx index cd434d1b..02052253 100644 --- a/src/app/[country]/[locale]/(storefront)/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/page.tsx @@ -105,9 +105,7 @@ export default async function HomePage({ ); diff --git a/src/app/[country]/[locale]/(storefront)/pages/[...slug]/page.tsx b/src/app/[country]/[locale]/(storefront)/pages/[...slug]/page.tsx index 50722937..19e8f662 100644 --- a/src/app/[country]/[locale]/(storefront)/pages/[...slug]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/pages/[...slug]/page.tsx @@ -58,12 +58,6 @@ export default async function DynamicStorePage({ } return ( - + ); } diff --git a/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx b/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx index 7224c88f..1d5d717a 100644 --- a/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx +++ b/src/app/[country]/[locale]/(storefront)/products/[slug]/page.tsx @@ -107,8 +107,6 @@ export default async function ProductPage({ @@ -118,8 +116,6 @@ export default async function ProductPage({ diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index 718d6fea..00000000 Binary files a/src/app/favicon.ico and /dev/null differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 69277513..68c1d825 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -35,6 +35,7 @@ export const metadata: Metadata = { default: rootStoreName, }, description: getStoreDescription(), + icons: { icon: "/olitt-logo.svg" }, }; export default async function RootLayout({ diff --git a/src/components/home/FeaturedProductsSection.tsx b/src/components/home/FeaturedProductsSection.tsx index 95408d71..ba52e97f 100644 --- a/src/components/home/FeaturedProductsSection.tsx +++ b/src/components/home/FeaturedProductsSection.tsx @@ -1,10 +1,10 @@ import Link from "next/link"; -import type { CSSProperties } from "react"; import { Suspense } from "react"; import { FeaturedProducts } from "@/components/products/FeaturedProducts"; import { ProductCardSkeleton } from "@/components/products/ProductCardSkeleton"; import { Button } from "@/components/ui/button"; import type { HomepageFeaturedProductsSectionConfig } from "@/lib/homepage"; +import { buildSectionThemeVars } from "@/lib/homepage/section-theme"; function CarouselSkeleton() { return ( @@ -18,24 +18,17 @@ function CarouselSkeleton() { interface FeaturedProductsSectionProps { basePath: string; - locale: string; - country: string; currency?: string; section: HomepageFeaturedProductsSectionConfig; } export async function FeaturedProductsSection({ basePath, - locale, - country, currency, section, }: FeaturedProductsSectionProps) { const theme = section.theme ?? {}; - const sectionStyle: CSSProperties = { - backgroundColor: theme.background, - color: theme.foreground, - }; + const sectionStyle = buildSectionThemeVars(theme); const mutedTextColor = theme.mutedForeground ?? "#64748b"; const borderColor = theme.borderColor ?? "#cbd5e1"; @@ -79,12 +72,7 @@ export async function FeaturedProductsSection({
}> - +
diff --git a/src/components/home/FeaturesSection.tsx b/src/components/home/FeaturesSection.tsx index 08b51efb..2ccbfeae 100644 --- a/src/components/home/FeaturesSection.tsx +++ b/src/components/home/FeaturesSection.tsx @@ -1,6 +1,6 @@ import { ShieldCheck, ShoppingBag, Sparkles, Truck } from "lucide-react"; -import type { CSSProperties } from "react"; import type { HomepageFeaturesSectionConfig } from "@/lib/homepage"; +import { buildSectionThemeVars } from "@/lib/homepage/section-theme"; interface FeaturesSectionProps { section: HomepageFeaturesSectionConfig; @@ -21,10 +21,7 @@ function getFeatureIcon(iconName?: string) { export async function FeaturesSection({ section }: FeaturesSectionProps) { const theme = section.theme ?? {}; - const sectionStyle: CSSProperties = { - backgroundColor: theme.background, - color: theme.foreground, - }; + const sectionStyle = buildSectionThemeVars(theme); const mutedTextColor = theme.mutedForeground ?? "#6b7280"; const cardBackground = theme.cardBackground ?? "#ffffff"; const borderColor = theme.borderColor ?? "#e5e7eb"; diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx index 1dba2f59..6e37fce2 100644 --- a/src/components/home/HeroSection.tsx +++ b/src/components/home/HeroSection.tsx @@ -1,12 +1,12 @@ import { ArrowRight, Play } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; -import type { CSSProperties } from "react"; import { Button } from "@/components/ui/button"; import type { HomepageActionIcon, HomepageHeroSectionConfig, } from "@/lib/homepage"; +import { buildSectionThemeVars } from "@/lib/homepage/section-theme"; interface HeroSectionProps { basePath: string; @@ -36,10 +36,7 @@ function getActionIcon(icon?: HomepageActionIcon) { export async function HeroSection({ basePath, section }: HeroSectionProps) { const theme = section.theme ?? {}; - const sectionStyle: CSSProperties = { - backgroundColor: theme.background, - color: theme.foreground, - }; + const sectionStyle = buildSectionThemeVars(theme); const mutedTextColor = theme.mutedForeground ?? "#6b7280"; const cardBackground = theme.cardBackground ?? "#ffffff"; const borderColor = theme.borderColor ?? "#e5e7eb"; diff --git a/src/components/page-builder/DynamicPageRenderer.tsx b/src/components/page-builder/DynamicPageRenderer.tsx index acc61120..12b26fa5 100644 --- a/src/components/page-builder/DynamicPageRenderer.tsx +++ b/src/components/page-builder/DynamicPageRenderer.tsx @@ -4,24 +4,18 @@ import type { DynamicPageConfig } from "@/lib/page-builder"; interface DynamicPageRendererProps { page: DynamicPageConfig; basePath: string; - locale: string; - country: string; currency?: string; } export function DynamicPageRenderer({ page, basePath, - locale, - country, currency, }: DynamicPageRendererProps) { return ( diff --git a/src/components/page-builder/PageSectionsRenderer.tsx b/src/components/page-builder/PageSectionsRenderer.tsx index bb316d1e..0a37a29a 100644 --- a/src/components/page-builder/PageSectionsRenderer.tsx +++ b/src/components/page-builder/PageSectionsRenderer.tsx @@ -8,8 +8,6 @@ import type { DynamicPageSectionConfig } from "@/lib/page-builder"; interface PageSectionsRendererProps { sections: DynamicPageSectionConfig[]; basePath: string; - locale: string; - country: string; currency?: string; keyPrefix?: string; } @@ -17,8 +15,6 @@ interface PageSectionsRendererProps { export function PageSectionsRenderer({ sections, basePath, - locale, - country, currency, keyPrefix = "section", }: PageSectionsRendererProps) { @@ -39,9 +35,7 @@ export function PageSectionsRenderer({ ); diff --git a/src/components/products/FeaturedProducts.tsx b/src/components/products/FeaturedProducts.tsx index ac989146..ab16c713 100644 --- a/src/components/products/FeaturedProducts.tsx +++ b/src/components/products/FeaturedProducts.tsx @@ -1,8 +1,7 @@ import dynamic from "next/dynamic"; import { ProductCardSkeleton } from "@/components/products/ProductCardSkeleton"; import { PRODUCT_CARD_FIELDS } from "@/lib/data/cached"; -import { cachedListProducts } from "@/lib/data/products"; -import { getAccessToken } from "@/lib/spree"; +import { getProducts } from "@/lib/data/products"; const LazyProductCarousel = dynamic( () => @@ -22,23 +21,17 @@ const LazyProductCarousel = dynamic( interface FeaturedProductsProps { basePath: string; - locale: string; - country: string; currency?: string; } export async function FeaturedProducts({ basePath, - locale, - country, currency, }: FeaturedProductsProps) { - const userToken = await getAccessToken(); - const productsResponse = await cachedListProducts( - { limit: 8, fields: PRODUCT_CARD_FIELDS }, - { locale, country }, - userToken, - ); + const productsResponse = await getProducts({ + limit: 10, + fields: PRODUCT_CARD_FIELDS, + }); return ( {/* Image */} -
+
-

+

{product.name}

{displayPrice && ( - + {displayPrice} )} {onSale && strikethroughPrice && ( - + {strikethroughPrice} )}
{!product.purchasable && ( - {t("outOfStock")} + + {t("outOfStock")} + )}
diff --git a/src/components/products/ProductCarousel.tsx b/src/components/products/ProductCarousel.tsx index 0b050657..d887d7e7 100644 --- a/src/components/products/ProductCarousel.tsx +++ b/src/components/products/ProductCarousel.tsx @@ -20,7 +20,7 @@ interface ProductCarouselProps { } const NAV_BUTTON_BASE = - "absolute top-1/2 -translate-y-1/2 z-10 w-10 h-10 flex items-center justify-center cursor-pointer rounded-lg bg-white border border-gray-300 text-gray-600 hover:bg-gray-100 hover:text-gray-900 transition-colors"; + "absolute top-1/2 -translate-y-1/2 z-10 w-10 h-10 flex items-center justify-center cursor-pointer rounded-lg bg-background border border-border text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"; export function ProductCarousel({ products, @@ -49,7 +49,7 @@ export function ProductCarousel({ if (products.length === 0) { return (
-

{t("noProductsFound")}

+

{t("noProductsFound")}

); } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 3464538f..8119db1e 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -11,7 +11,7 @@ const buttonVariants = cva( variant: { default: "bg-primary text-primary-foreground hover:bg-primary/80", outline: - "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + "border-border bg-background text-foreground hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", ghost: diff --git a/src/lib/homepage/section-theme.ts b/src/lib/homepage/section-theme.ts new file mode 100644 index 00000000..c0d463aa --- /dev/null +++ b/src/lib/homepage/section-theme.ts @@ -0,0 +1,44 @@ +import type { CSSProperties } from "react"; +import type { HomepageThemeConfig } from "./types"; + +type ThemeVars = CSSProperties & Record<`--${string}`, string | undefined>; + +/** + * Build the inline style for a themed homepage section. + * + * Paints the section and scopes the design-system color variables to it, so + * descendants styled with utility classes such as `bg-background` or + * `text-foreground` resolve against the section's theme instead of the page + * defaults. The variables are set on the section element, so pages and + * components outside the section are unaffected. + * + * The accent pair is applied only when the theme carries both `accent` and + * `accentForeground`: an accent without its generator-validated label color + * would leave button labels on the default ink, which can fail contrast. + */ +export function buildSectionThemeVars(theme: HomepageThemeConfig): ThemeVars { + const hasReadableAccent = Boolean(theme.accent && theme.accentForeground); + const vars: ThemeVars = { + backgroundColor: theme.background, + color: theme.foreground, + "--background": theme.background, + "--foreground": theme.foreground, + "--card": theme.cardBackground, + "--card-foreground": theme.foreground, + "--muted": theme.cardBackground, + "--muted-foreground": theme.mutedForeground, + "--border": theme.borderColor, + "--input": theme.borderColor, + "--primary": hasReadableAccent ? theme.accent : undefined, + "--primary-foreground": hasReadableAccent + ? theme.accentForeground + : undefined, + "--ring": theme.accent, + }; + + for (const key of Object.keys(vars) as (keyof ThemeVars)[]) { + if (vars[key] === undefined) delete vars[key]; + } + + return vars; +} diff --git a/src/lib/homepage/types.ts b/src/lib/homepage/types.ts index e6d2d1dc..d9dcbbd7 100644 --- a/src/lib/homepage/types.ts +++ b/src/lib/homepage/types.ts @@ -17,6 +17,7 @@ export interface HomepageThemeConfig { background?: string; foreground?: string; accent?: string; + accentForeground?: string; mutedForeground?: string; cardBackground?: string; borderColor?: string;