Skip to content
Merged
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
4 changes: 4 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const nextConfig: NextConfig = {
hostname: "**.trycloudflare.com",
pathname: "/rails/active_storage/**",
},
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
},
};
Expand Down
6 changes: 2 additions & 4 deletions src/app/[country]/[locale]/(storefront)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Footer } from "@/components/layout/Footer";
import { Header } from "@/components/layout/Header";
import { getCategories } from "@/lib/data/categories";
import { getTenantConfigByHost } from "@/lib/tenant";
import { getRequestHost } from "@/lib/tenant/request";

interface StorefrontLayoutProps {
children: React.ReactNode;
Expand Down Expand Up @@ -41,10 +42,7 @@ export default async function StorefrontLayout({
const { country, locale } = await params;
const basePath = `/${country}/${locale}`;
const requestHeaders = await headers();
const tenantHost =
requestHeaders.get("x-forwarded-host") ??
requestHeaders.get("host") ??
"localhost";
const tenantHost = getRequestHost(requestHeaders) ?? "localhost";
const tenantConfig = await getTenantConfigByHost(tenantHost);

const rootCategories = await getCategories({
Expand Down
9 changes: 7 additions & 2 deletions src/app/[country]/[locale]/(storefront)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import type { ReactElement } from "react";
import { FeaturedProductsSection } from "@/components/home/FeaturedProductsSection";
import { FeaturesSection } from "@/components/home/FeaturesSection";
import { HeroSection } from "@/components/home/HeroSection";
Expand All @@ -25,7 +26,9 @@ interface HomePageProps {
* always include the store's configured default country/locale as a
* fallback even if the markets fetch fails.
*/
export async function generateStaticParams() {
export async function generateStaticParams(): Promise<
Array<{ country: string; locale: string }>
> {
const fallback = {
country: getDefaultCountry(),
locale: getDefaultLocale(),
Expand Down Expand Up @@ -72,7 +75,9 @@ export async function generateMetadata({
return generateHomeMetadata({ country, locale });
}

export default async function HomePage({ params }: HomePageProps) {
export default async function HomePage({
params,
}: HomePageProps): Promise<ReactElement> {
const { country, locale } = await params;
const basePath = `/${country}/${locale}`;
const currency = await resolveCurrency(country);
Expand Down
16 changes: 9 additions & 7 deletions src/app/[country]/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ import { buildOrganizationJsonLd } from "@/lib/seo";
import { getDefaultCountry, getDefaultLocale } from "@/lib/store";
import { getTenantConfigByHost } from "@/lib/tenant";
import { buildCssVars } from "@/lib/tenant/css-vars";
import { getTenantConfigFromRequest } from "@/lib/tenant/request";
import { toPublicTenantConfig } from "@/lib/tenant/normalize";
import {
getRequestHost,
getTenantConfigFromRequest,
} from "@/lib/tenant/request";
import deMessages from "../../../../messages/de.json";
import enMessages from "../../../../messages/en.json";
import esMessages from "../../../../messages/es.json";
import frMessages from "../../../../messages/fr.json";
import plMessages from "../../../../messages/pl.json";
import LocaleLayoutLoading from "./loading";

const messagesMap: Record<string, IntlMessages> = {
en: enMessages,
Expand Down Expand Up @@ -73,7 +78,7 @@ export default async function CountryLocaleLayout({
params,
}: CountryLocaleLayoutProps) {
return (
<Suspense fallback={null}>
<Suspense fallback={<LocaleLayoutLoading />}>
<CountryLocaleLayoutInner params={params}>
{children}
</CountryLocaleLayoutInner>
Expand All @@ -87,10 +92,7 @@ async function CountryLocaleLayoutInner({
}: CountryLocaleLayoutProps) {
const { country, locale } = await params;
const requestHeaders = await headers();
const host =
requestHeaders.get("x-forwarded-host") ??
requestHeaders.get("host") ??
"localhost";
const host = getRequestHost(requestHeaders) ?? "localhost";
const tenantConfig = await getTenantConfigByHost(host);
if (!tenantConfig) {
return <OlittFallbackPage host={host} />;
Expand Down Expand Up @@ -135,7 +137,7 @@ async function CountryLocaleLayoutInner({
data-tenant-host={tenantConfig.host}
data-tenant-id={tenantConfig.tenantId}
>
<TenantConfigProvider config={tenantConfig}>
<TenantConfigProvider config={toPublicTenantConfig(tenantConfig)}>
<NextIntlClientProvider
messages={messages}
locale={locale as "en" | "de" | "pl"}
Expand Down
22 changes: 22 additions & 0 deletions src/app/[country]/[locale]/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { headers } from "next/headers";
import { getTenantConfigByHost } from "@/lib/tenant/olitt";
import { getRequestHost } from "@/lib/tenant/request";
import { getTenantBrandName } from "@/lib/tenant/surface";

export default async function Loading() {
const requestHeaders = await headers();
const host = getRequestHost(requestHeaders) ?? "";
const tenantConfig = host ? await getTenantConfigByHost(host) : null;
const storeName = getTenantBrandName(tenantConfig) ?? "Store";

return (
<div className="min-h-screen flex items-center justify-center bg-background px-6">
<div className="flex flex-col items-center gap-4 text-center">
<div className="h-10 w-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
<p className="text-xl font-medium tracking-tight text-foreground">
Loading {storeName}
</p>
</div>
</div>
);
}
1 change: 0 additions & 1 deletion src/components/home/FeaturesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ function getFeatureIcon(iconName?: string) {
return <ShieldCheck className="w-6 h-6" />;
case "shopping-bag":
return <ShoppingBag className="w-6 h-6" />;
case "sparkles":
default:
return <Sparkles className="w-6 h-6" />;
}
Expand Down
8 changes: 6 additions & 2 deletions src/components/home/HeroSection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
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";
Expand Down Expand Up @@ -178,13 +179,16 @@ export async function HeroSection({ basePath, section }: HeroSectionProps) {
style={{ backgroundColor: cardBackground }}
>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent pointer-events-none" />
<img
<Image
src={
section.media?.imageUrl ??
"https://images.unsplash.com/photo-1523381210434-271e8be1f52b?auto=format&fit=crop&q=80&w=1200"
}
alt={section.media?.alt ?? section.title}
className="w-full h-full object-cover grayscale-[0.2] hover:grayscale-0 transition-all duration-700 scale-105 hover:scale-100"
fill
priority
sizes="(min-width: 1024px) 50vw, 100vw"
className="object-cover grayscale-[0.2] hover:grayscale-0 transition-all duration-700 scale-105 hover:scale-100"
/>
</div>

Expand Down
8 changes: 5 additions & 3 deletions src/components/page-builder/ImageBannerSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ArrowRight } from "lucide-react";
import Link from "next/link";
import type { CSSProperties } from "react";
import type { CSSProperties, ReactElement } from "react";
import { Button } from "@/components/ui/button";
import type { DynamicPageImageBannerSectionConfig } from "@/lib/page-builder";

Expand All @@ -16,7 +16,9 @@ function resolveHref(basePath: string, href: string): string {
return href.startsWith("/") ? `${basePath}${href}` : `${basePath}/${href}`;
}

function getHeightClass(height: DynamicPageImageBannerSectionConfig["height"]) {
function getHeightClass(
height: DynamicPageImageBannerSectionConfig["height"],
): string {
switch (height) {
case "sm":
return "min-h-[320px]";
Expand All @@ -30,7 +32,7 @@ function getHeightClass(height: DynamicPageImageBannerSectionConfig["height"]) {
export function ImageBannerSection({
basePath,
section,
}: ImageBannerSectionProps) {
}: ImageBannerSectionProps): ReactElement {
const theme = section.theme ?? {};
const foreground = theme.foreground ?? "#ffffff";
const mutedTextColor = theme.mutedForeground ?? "#e5e7eb";
Expand Down
26 changes: 16 additions & 10 deletions src/contexts/TenantContext.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
"use client";

import { createContext, type ReactNode, useContext, useMemo } from "react";
import type { TenantConfig } from "@/lib/tenant";
import {
createContext,
type ReactElement,
type ReactNode,
useContext,
useMemo,
} from "react";
import type { PublicTenantConfig } from "@/lib/tenant";

const TenantContext = createContext<TenantConfig | null>(null);
const TenantContext = createContext<PublicTenantConfig | null>(null);

export function TenantConfigProvider({
Comment thread
kipsang01 marked this conversation as resolved.
children,
config,
}: {
children: ReactNode;
config: TenantConfig;
}) {
config: PublicTenantConfig;
}): ReactElement {
const value = useMemo(() => config, [config]);

return (
<TenantContext.Provider value={value}>{children}</TenantContext.Provider>
);
}

export function useTenantConfig(): TenantConfig {
export function useTenantConfig(): PublicTenantConfig {
const context = useContext(TenantContext);
if (!context) {
throw new Error(
Expand All @@ -29,18 +35,18 @@ export function useTenantConfig(): TenantConfig {
return context;
}

export function useTenantTheme() {
export function useTenantTheme(): PublicTenantConfig["theme"] {
return useTenantConfig().theme;
}

export function useTenantNavigation() {
export function useTenantNavigation(): PublicTenantConfig["navigation"] {
return useTenantConfig().navigation;
}

export function useTenantPayments() {
export function useTenantPayments(): PublicTenantConfig["paymentKeys"] {
return useTenantConfig().paymentKeys;
}

export function useTenantSpree() {
export function useTenantSpree(): PublicTenantConfig["spree"] {
return useTenantConfig().spree;
}
9 changes: 3 additions & 6 deletions src/contexts/__tests__/TenantContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ const tenantConfig = {
apiUrl: "https://spree.example.com",
publishableKey: "pub-key",
},
paymentKeys: {
stripePublishableKey: "pk_test_123",
},
theme: {
colors: {
primary: "#111111",
Expand All @@ -25,9 +22,9 @@ const tenantConfig = {
navigation: {
links: [{ label: "Products", href: "/products" }],
},
raw: {},
source: "olitt",
fetchedAt: new Date().toISOString(),
paymentKeys: {
stripePublishableKey: "pk_test_123",
},
} as never;

function wrapper({ children }: { children: React.ReactNode }) {
Expand Down
16 changes: 10 additions & 6 deletions src/lib/data/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export async function cachedListProducts(
baseUrl?: string,
publishableKey?: string,
_spreeScope?: string,
) {
): Promise<PaginatedResponse<Product>> {
"use cache: remote";
cacheLife("tenMinutes");
cacheTag("products");
Expand All @@ -85,7 +85,9 @@ export async function cachedListProducts(
}).products.list(params, options);
}

export async function getProducts(params?: ProductListParams) {
export async function getProducts(
params?: ProductListParams,
): Promise<PaginatedResponse<Product>> {
const options = await getLocaleOptions();
const userToken = await getAccessToken();
const spreeConfig = await resolveSpreeConfig();
Expand Down Expand Up @@ -116,7 +118,7 @@ export async function cachedGetProduct(
baseUrl?: string,
publishableKey?: string,
_spreeScope?: string,
) {
): Promise<Product> {
"use cache: remote";
cacheLife("tenMinutes");
cacheTag("products", `product:${slugOrId}`);
Expand All @@ -133,7 +135,7 @@ export async function cachedGetProduct(
export async function getProduct(
slugOrId: string,
params?: { expand?: string[] },
) {
): Promise<Product> {
const options = await getLocaleOptions();
const userToken = await getAccessToken();
const spreeConfig = await resolveSpreeConfig();
Expand All @@ -155,7 +157,7 @@ async function cachedGetProductFilters(
baseUrl?: string,
publishableKey?: string,
_spreeScope?: string,
) {
): Promise<ProductFiltersResponse> {
"use cache: remote";
cacheLife("tenMinutes");
cacheTag("product-filters");
Expand All @@ -169,7 +171,9 @@ async function cachedGetProductFilters(
}).products.filters(params, options);
}

export async function getProductFilters(params?: Record<string, unknown>) {
export async function getProductFilters(
params?: Record<string, unknown>,
): Promise<ProductFiltersResponse> {
const options = await getLocaleOptions();
const userToken = await getAccessToken();
const spreeConfig = await resolveSpreeConfig();
Expand Down
12 changes: 5 additions & 7 deletions src/lib/tenant/css-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,13 @@ export function resolveTenantThemeConfig(

export function buildCssVars(config: TenantConfig): Record<string, string> {
const theme = resolveTenantThemeConfig(config);
const branding = getNestedRecord(theme["branding"]);
const branding = getNestedRecord(theme.branding);
const colors =
getNestedRecord(theme["colors"]) ??
getNestedRecord(branding?.["colors"]) ??
{};
const fonts = getNestedRecord(theme["fonts"]) ?? {};
getNestedRecord(theme.colors) ?? getNestedRecord(branding?.colors) ?? {};
const fonts = getNestedRecord(theme.fonts) ?? {};

const spacing = getString(theme["spacing"]);
const radius = getString(theme["borderRadius"]);
const spacing = getString(theme.spacing);
const radius = getString(theme.borderRadius);
const spacingPreset =
SPACING_MAP[(spacing as keyof typeof SPACING_MAP) ?? "comfortable"] ??
SPACING_MAP.comfortable;
Expand Down
27 changes: 26 additions & 1 deletion src/lib/tenant/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { TenantConfig } from "./types";
import type {
PublicTenantConfig,
PublicTenantPaymentKeys,
TenantConfig,
} from "./types";

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
Expand Down Expand Up @@ -150,6 +154,17 @@ function collectPaymentKeys(
return result;
}

function collectPublicPaymentKeys(
config: TenantConfig,
): PublicTenantPaymentKeys {
const stripePublishableKey =
config.paymentKeys.stripePublishableKey?.trim() || undefined;

return {
...(stripePublishableKey ? { stripePublishableKey } : {}),
};
}

function getSpreeConfig(record: Record<string, unknown>): {
apiUrl: string;
publishableKey: string;
Expand Down Expand Up @@ -199,3 +214,13 @@ export function buildTenantConfigFromRecord(
fetchedAt: new Date().toISOString(),
};
}

export function toPublicTenantConfig(config: TenantConfig): PublicTenantConfig {
return {
storeName: config.storeName,
spree: config.spree,
paymentKeys: collectPublicPaymentKeys(config),
theme: config.theme,
navigation: config.navigation,
};
}
Loading