@alphasquad/saleor-template-advance 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/next.config.ts +38 -0
- package/package.json +1 -1
- package/src/app/components/layout/footer.tsx +32 -8
- package/src/app/components/layout/header/header.tsx +41 -11
- package/src/app/components/showroom/showroomHeroCarousel.tsx +7 -1
- package/src/app/components/showroom/testimonialsGrid.tsx +6 -1
- package/src/app/page.tsx +6 -1
- package/src/lib/overrides/README.md +80 -0
- package/src/lib/overrides/defaults.ts +5 -0
- package/src/lib/overrides/resolve.ts +38 -0
- package/src/lib/overrides/tenant-overrides.d.ts +4 -0
package/next.config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { NextConfig } from "next";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import fs from "node:fs";
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
|
|
@@ -47,12 +48,49 @@ function getRemoteImageHosts(): string[] {
|
|
|
47
48
|
);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// --- Tenant Override Resolution ---
|
|
52
|
+
// When running via @alphasquad/create-saleor-storefront CLI wrapper,
|
|
53
|
+
// process.cwd() is the tenant root. The tenant can place override components
|
|
54
|
+
// in src/overrides/ which the template will use instead of its defaults.
|
|
55
|
+
const tenantRoot = process.cwd();
|
|
56
|
+
const tenantOverridesIndex = path.join(tenantRoot, "src", "overrides", "index.ts");
|
|
57
|
+
const tenantOverridesDir = path.join(tenantRoot, "src", "overrides");
|
|
58
|
+
const hasTenantOverrides =
|
|
59
|
+
fs.existsSync(tenantOverridesIndex) ||
|
|
60
|
+
fs.existsSync(tenantOverridesIndex.replace(".ts", ".tsx")) ||
|
|
61
|
+
fs.existsSync(tenantOverridesIndex.replace(".ts", ".js"));
|
|
62
|
+
|
|
50
63
|
const nextConfig: NextConfig = {
|
|
51
64
|
// Next.js skips tsconfig path resolution for files inside node_modules.
|
|
52
65
|
// When this template is consumed as a package, we need explicit aliases.
|
|
53
66
|
transpilePackages: ["@alphasquad/saleor-template-advance"],
|
|
54
67
|
webpack(config) {
|
|
55
68
|
config.resolve.alias["@"] = path.resolve(__dirname, "src");
|
|
69
|
+
|
|
70
|
+
// Map @tenant-overrides to tenant's src/overrides if it exists,
|
|
71
|
+
// otherwise use the template's empty defaults
|
|
72
|
+
config.resolve = config.resolve || {};
|
|
73
|
+
config.resolve.alias = config.resolve.alias || {};
|
|
74
|
+
|
|
75
|
+
if (hasTenantOverrides) {
|
|
76
|
+
(config.resolve.alias as Record<string, string>)["@tenant-overrides"] =
|
|
77
|
+
tenantOverridesDir;
|
|
78
|
+
} else {
|
|
79
|
+
// When no tenant overrides exist, use the template's empty defaults.
|
|
80
|
+
// In Next.js, next.config.ts is compiled and __dirname is available.
|
|
81
|
+
// However when installed as npm package, we need the template package root.
|
|
82
|
+
// The config file is always at the template root, so we use a simple heuristic:
|
|
83
|
+
// try the template root (where next.config.ts lives) first, then cwd.
|
|
84
|
+
const candidates = [
|
|
85
|
+
path.join(path.dirname(require.resolve("./package.json")), "src", "lib", "overrides", "defaults"),
|
|
86
|
+
path.join(process.cwd(), "src", "lib", "overrides", "defaults"),
|
|
87
|
+
];
|
|
88
|
+
const defaultsPath = candidates.find((p) =>
|
|
89
|
+
fs.existsSync(p + ".ts") || fs.existsSync(p + ".js")
|
|
90
|
+
) || candidates[0];
|
|
91
|
+
(config.resolve.alias as Record<string, string>)["@tenant-overrides"] = defaultsPath;
|
|
92
|
+
}
|
|
93
|
+
|
|
56
94
|
return config;
|
|
57
95
|
},
|
|
58
96
|
eslint: {
|
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { storefrontOverrides } from "@tenant-overrides";
|
|
1
2
|
import { fetchMenuBySlug } from "@/graphql/queries/getMenuBySlug";
|
|
2
3
|
import Image from "next/image";
|
|
3
4
|
import Link from "next/link";
|
|
@@ -26,6 +27,15 @@ type FooterSection = {
|
|
|
26
27
|
children: FooterChild[];
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Props passed to both the default and any tenant override footer.
|
|
32
|
+
* Server-fetched data is always provided, preserving SSR performance.
|
|
33
|
+
*/
|
|
34
|
+
export interface FooterRendererProps {
|
|
35
|
+
footerMenu: any;
|
|
36
|
+
siteInfo: any;
|
|
37
|
+
}
|
|
38
|
+
|
|
29
39
|
const SectionTitle = ({ children }: { children: React.ReactNode }) => (
|
|
30
40
|
<span className="text-base font-secondary font-bold text-white uppercase tracking-wider">
|
|
31
41
|
{children}
|
|
@@ -83,22 +93,17 @@ function normalizePhone(raw: string) {
|
|
|
83
93
|
return digits ? `tel:${digits}` : null;
|
|
84
94
|
}
|
|
85
95
|
|
|
86
|
-
|
|
87
|
-
|
|
96
|
+
const DefaultFooterRenderer = ({ footerMenu, siteInfo }: FooterRendererProps) => {
|
|
97
|
+
const currentYear = new Date().getFullYear();
|
|
88
98
|
|
|
89
99
|
const TENANT_NAME = process.env.NEXT_PUBLIC_BRAND_NAME || process.env.NEXT_PUBLIC_TENANT_NAME || "Storefront";
|
|
90
100
|
const logo =
|
|
91
101
|
process.env.NEXT_PUBLIC_LOGO_URL ||
|
|
92
102
|
"https://webshopmanager.com/files/images/logo.png";
|
|
93
103
|
|
|
94
|
-
// Fetch footer menu data from backend
|
|
95
|
-
const footerMenu = await fetchMenuBySlug("footer");
|
|
96
|
-
|
|
97
|
-
// Fetch site info for address and phone
|
|
98
|
-
const siteInfo = await fetchSiteInfo();
|
|
99
104
|
const getInfo = (k: string) =>
|
|
100
105
|
siteInfo?.metadata
|
|
101
|
-
?.find((m) => m.key.toLowerCase() === k.toLowerCase())
|
|
106
|
+
?.find((m: any) => m.key.toLowerCase() === k.toLowerCase())
|
|
102
107
|
?.value?.trim() || "";
|
|
103
108
|
|
|
104
109
|
const address = getInfo("Address");
|
|
@@ -280,4 +285,23 @@ function normalizePhone(raw: string) {
|
|
|
280
285
|
);
|
|
281
286
|
};
|
|
282
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Server component that fetches footer data and delegates rendering
|
|
290
|
+
* to either the tenant's override or the default footer.
|
|
291
|
+
*
|
|
292
|
+
* Data fetching always happens server-side regardless of which renderer is used,
|
|
293
|
+
* preserving SSR performance and SEO.
|
|
294
|
+
*/
|
|
295
|
+
async function Footer() {
|
|
296
|
+
const [footerMenu, siteInfo] = await Promise.all([
|
|
297
|
+
fetchMenuBySlug("footer"),
|
|
298
|
+
fetchSiteInfo(),
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const FooterRenderer =
|
|
302
|
+
(storefrontOverrides as any).Footer || DefaultFooterRenderer;
|
|
303
|
+
|
|
304
|
+
return <FooterRenderer footerMenu={footerMenu} siteInfo={siteInfo} />;
|
|
305
|
+
}
|
|
306
|
+
|
|
283
307
|
export default Footer;
|
|
@@ -2,17 +2,23 @@ import { Suspense } from "react";
|
|
|
2
2
|
import { NavBar } from "./navBar";
|
|
3
3
|
import TopBar from "./topBar";
|
|
4
4
|
import { fetchCategories, fetchMenuData } from "@/hooks/serverNavbarData";
|
|
5
|
+
import type { CategoryNode, MenuItem } from "@/hooks/serverNavbarData";
|
|
6
|
+
import { storefrontOverrides } from "@tenant-overrides";
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Props passed to both the default and any tenant override header.
|
|
10
|
+
* This ensures tenant headers receive server-fetched data without
|
|
11
|
+
* needing client-side fetches (preserving SSR performance).
|
|
12
|
+
*/
|
|
13
|
+
export interface HeaderRendererProps {
|
|
14
|
+
categories: CategoryNode[];
|
|
15
|
+
menuItems: MenuItem[];
|
|
16
|
+
}
|
|
15
17
|
|
|
18
|
+
const DefaultHeaderRenderer = ({
|
|
19
|
+
categories,
|
|
20
|
+
menuItems,
|
|
21
|
+
}: HeaderRendererProps) => {
|
|
16
22
|
return (
|
|
17
23
|
<header className="w-full">
|
|
18
24
|
<Suspense
|
|
@@ -23,7 +29,6 @@ export const Header = async () => {
|
|
|
23
29
|
/>
|
|
24
30
|
}
|
|
25
31
|
>
|
|
26
|
-
{/* Contact + Timings Banner */}
|
|
27
32
|
<TopBar />
|
|
28
33
|
</Suspense>
|
|
29
34
|
<Suspense
|
|
@@ -37,8 +42,33 @@ export const Header = async () => {
|
|
|
37
42
|
/>
|
|
38
43
|
}
|
|
39
44
|
>
|
|
40
|
-
<NavBar categories={
|
|
45
|
+
<NavBar categories={categories} menuItems={menuItems} />
|
|
41
46
|
</Suspense>
|
|
42
47
|
</header>
|
|
43
48
|
);
|
|
44
49
|
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Server component that fetches navigation data and delegates rendering
|
|
53
|
+
* to either the tenant's override or the default header.
|
|
54
|
+
*
|
|
55
|
+
* Data fetching always happens server-side regardless of which renderer is used,
|
|
56
|
+
* preserving SSR performance and SEO (no client-side fetch waterfalls).
|
|
57
|
+
*/
|
|
58
|
+
export const Header = async () => {
|
|
59
|
+
const [categories, menuItems] = await Promise.allSettled([
|
|
60
|
+
fetchCategories(),
|
|
61
|
+
fetchMenuData(),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const categoriesData =
|
|
65
|
+
categories.status === "fulfilled" ? categories.value : [];
|
|
66
|
+
const menuItemsData = menuItems.status === "fulfilled" ? menuItems.value : [];
|
|
67
|
+
|
|
68
|
+
const HeaderRenderer =
|
|
69
|
+
(storefrontOverrides as any).Header || DefaultHeaderRenderer;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<HeaderRenderer categories={categoriesData} menuItems={menuItemsData} />
|
|
73
|
+
);
|
|
74
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { storefrontOverrides } from "@tenant-overrides";
|
|
3
4
|
import { GET_PAGE_METADATA_BY_SLUG } from "@/graphql/queries/getHeroMetadata";
|
|
4
5
|
import { useQuery } from "@apollo/client";
|
|
5
6
|
import Image from "next/image";
|
|
@@ -62,7 +63,7 @@ const SearchFormSection = () => {
|
|
|
62
63
|
);
|
|
63
64
|
};
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
const DefaultShowroomHeroCarousel = ({}: ShowroomHeroCarouselProps) => {
|
|
66
67
|
const { data, loading } = useQuery(GET_PAGE_METADATA_BY_SLUG, {
|
|
67
68
|
variables: { slug: "hero-section" },
|
|
68
69
|
});
|
|
@@ -139,3 +140,8 @@ export const ShowroomHeroCarousel = ({}: ShowroomHeroCarouselProps) => {
|
|
|
139
140
|
</div>
|
|
140
141
|
);
|
|
141
142
|
};
|
|
143
|
+
|
|
144
|
+
// Allow tenant repos to override the hero carousel via storefrontOverrides.ShowroomHeroCarousel
|
|
145
|
+
export const ShowroomHeroCarousel =
|
|
146
|
+
(storefrontOverrides as any).ShowroomHeroCarousel ||
|
|
147
|
+
DefaultShowroomHeroCarousel;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { storefrontOverrides } from "@tenant-overrides";
|
|
1
2
|
import { GET_TESTIMONIAL_PAGE_TYPE } from "@/graphql/queries/getPageTypeId";
|
|
2
3
|
import { GET_TESTIMONIALS } from "@/graphql/queries/getTestimonials";
|
|
3
4
|
import createApolloServerClient from "@/graphql/server-client";
|
|
4
5
|
import Heading from "../reuseableUI/heading";
|
|
5
6
|
import { TestimonialCard } from "../reuseableUI/testimonialCard";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
const DefaultTestimonialsGrid = async ({ first = 6 }: { first?: number }) => {
|
|
8
9
|
let items: Array<{
|
|
9
10
|
id: string;
|
|
10
11
|
name: string;
|
|
@@ -104,3 +105,7 @@ export const TestimonialsGrid = async ({ first = 6 }: { first?: number }) => {
|
|
|
104
105
|
</section>
|
|
105
106
|
);
|
|
106
107
|
};
|
|
108
|
+
|
|
109
|
+
// Allow tenant repos to override via storefrontOverrides.TestimonialsGrid
|
|
110
|
+
export const TestimonialsGrid =
|
|
111
|
+
(storefrontOverrides as any).TestimonialsGrid || DefaultTestimonialsGrid;
|
package/src/app/page.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { getStoreName } from "./utils/branding";
|
|
|
9
9
|
import { BrandsSwiperServer } from "./components/showroom/brandsSwiperServer";
|
|
10
10
|
import NewslettersHomeModal from "./components/reuseableUI/newsletter/newslettersHomeModal";
|
|
11
11
|
import { FeaturesStrip } from "./components/showroom/featureStrip";
|
|
12
|
+
import { withOverride } from "@/lib/overrides/resolve";
|
|
12
13
|
|
|
13
14
|
export const metadata: Metadata = {
|
|
14
15
|
title: `Home - ${getStoreName()}`,
|
|
@@ -73,7 +74,7 @@ const FeaturedBrandsLoadingState = () => {
|
|
|
73
74
|
);
|
|
74
75
|
};
|
|
75
76
|
|
|
76
|
-
|
|
77
|
+
function DefaultHome() {
|
|
77
78
|
return (
|
|
78
79
|
<main className="overflow-x-hidden">
|
|
79
80
|
<Suspense fallback={<SkeletonLoader type="hero" />}>
|
|
@@ -192,3 +193,7 @@ export default function Home() {
|
|
|
192
193
|
</main>
|
|
193
194
|
);
|
|
194
195
|
}
|
|
196
|
+
|
|
197
|
+
// Allow tenant repos to override the entire homepage via storefrontOverrides.HomePage
|
|
198
|
+
const HomePage = withOverride("HomePage", DefaultHome);
|
|
199
|
+
export default HomePage;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Tenant Override System
|
|
2
|
+
|
|
3
|
+
This template supports component overrides from tenant wrapper repos created by `@alphasquad/create-saleor-storefront`.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
1. The CLI creates a tenant repo with `src/overrides/index.ts` that exports a `storefrontOverrides` registry
|
|
8
|
+
2. `next.config.ts` maps `@tenant-overrides` to the tenant's `src/overrides/` directory (or empty defaults if none exist)
|
|
9
|
+
3. Template components check the registry and use tenant overrides when available, falling back to template defaults
|
|
10
|
+
|
|
11
|
+
## SSR & Performance Guarantees
|
|
12
|
+
|
|
13
|
+
The override system is designed to preserve server-side rendering and performance:
|
|
14
|
+
|
|
15
|
+
- **Header**: The template always fetches `categories` and `menuItems` server-side. Both the default and any tenant override receive these as props (`HeaderRendererProps`). Tenant headers do NOT need to fetch navigation data client-side.
|
|
16
|
+
- **Footer**: The template always fetches `footerMenu` and `siteInfo` server-side. Both the default and any tenant override receive these as props (`FooterRendererProps`).
|
|
17
|
+
- **ShowroomHeroCarousel**: Client component. Override replaces it 1:1.
|
|
18
|
+
- **TestimonialsGrid**: Server component. Override replaces it 1:1.
|
|
19
|
+
- **HomePage**: Full page replacement. The template's metadata export still applies.
|
|
20
|
+
|
|
21
|
+
**Rule**: If the template component is a server component that fetches data, the override system passes that data as props. Tenant overrides should NOT re-fetch the same data client-side.
|
|
22
|
+
|
|
23
|
+
## Supported Override Keys
|
|
24
|
+
|
|
25
|
+
| Key | Props Received | Rendering |
|
|
26
|
+
|-----|---------------|-----------|
|
|
27
|
+
| `HomePage` | None (full page) | Server or Client |
|
|
28
|
+
| `Header` | `{ categories, menuItems }` | Server-fetched, render only |
|
|
29
|
+
| `Footer` | `{ footerMenu, siteInfo }` | Server-fetched, render only |
|
|
30
|
+
| `ShowroomHeroCarousel` | None | Client component |
|
|
31
|
+
| `TestimonialsGrid` | `{ first }` | Server component |
|
|
32
|
+
|
|
33
|
+
## Tenant Override Example
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// src/overrides/index.ts
|
|
37
|
+
import MyHomePage from "./HomePage";
|
|
38
|
+
import MyHeader from "./Header";
|
|
39
|
+
|
|
40
|
+
export const storefrontOverrides = {
|
|
41
|
+
HomePage: MyHomePage,
|
|
42
|
+
Header: MyHeader,
|
|
43
|
+
};
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// src/overrides/Header.tsx
|
|
48
|
+
// Receives server-fetched data as props - no client-side fetching needed
|
|
49
|
+
export default function MyHeader({ categories, menuItems }) {
|
|
50
|
+
return (
|
|
51
|
+
<header>
|
|
52
|
+
<nav>
|
|
53
|
+
{menuItems.map(item => (
|
|
54
|
+
<a key={item.id} href={item.url}>{item.name}</a>
|
|
55
|
+
))}
|
|
56
|
+
</nav>
|
|
57
|
+
</header>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Adding New Override Points
|
|
63
|
+
|
|
64
|
+
To make a new component overridable while preserving SSR:
|
|
65
|
+
|
|
66
|
+
1. Import: `import { storefrontOverrides } from "@tenant-overrides";`
|
|
67
|
+
2. If the component fetches data server-side:
|
|
68
|
+
- Define a `RendererProps` interface with the fetched data
|
|
69
|
+
- Split into: async data-fetcher (stays as export) + renderer (overridable)
|
|
70
|
+
- The data-fetcher passes props to whichever renderer is active
|
|
71
|
+
3. If the component is client-only:
|
|
72
|
+
- Rename: `const DefaultFoo = ...`
|
|
73
|
+
- Export with override: `export const Foo = (storefrontOverrides as any).Foo || DefaultFoo;`
|
|
74
|
+
|
|
75
|
+
## Zero Impact When Unused
|
|
76
|
+
|
|
77
|
+
When no tenant overrides exist (e.g., running the template directly):
|
|
78
|
+
- `@tenant-overrides` resolves to `src/lib/overrides/defaults.ts` (empty object)
|
|
79
|
+
- All override checks resolve to `DefaultXxx` immediately
|
|
80
|
+
- No bundle size increase, no runtime overhead
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Override Resolution System
|
|
3
|
+
*
|
|
4
|
+
* Allows tenant wrapper repos (created by @alphasquad/create-saleor-storefront)
|
|
5
|
+
* to override specific template components by placing files in src/overrides/.
|
|
6
|
+
*
|
|
7
|
+
* Supported override keys (expand as needed):
|
|
8
|
+
* - HomePage: Replaces the entire homepage content
|
|
9
|
+
* - Header: Replaces the site header
|
|
10
|
+
* - Footer: Replaces the site footer
|
|
11
|
+
* - ShowroomHeroCarousel: Replaces the hero section
|
|
12
|
+
* - FeaturesStrip: Replaces the features strip
|
|
13
|
+
* - CategoryGrid: Replaces the category grid
|
|
14
|
+
* - ProductGrid: Replaces the product grid
|
|
15
|
+
* - TestimonialsGrid: Replaces the testimonials section
|
|
16
|
+
* - AboutUs: Replaces the about us section
|
|
17
|
+
* - BrandsSwiper: Replaces the brands swiper
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export { storefrontOverrides } from "@tenant-overrides";
|
|
21
|
+
|
|
22
|
+
// Re-export a typed helper
|
|
23
|
+
import { storefrontOverrides } from "@tenant-overrides";
|
|
24
|
+
import type { ComponentType } from "react";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolves an override component by key.
|
|
28
|
+
* Returns the tenant's override if available, otherwise the provided default.
|
|
29
|
+
*/
|
|
30
|
+
export function withOverride<P extends Record<string, unknown>>(
|
|
31
|
+
key: string,
|
|
32
|
+
DefaultComponent: ComponentType<P>
|
|
33
|
+
): ComponentType<P> {
|
|
34
|
+
const Override = (storefrontOverrides as Record<string, ComponentType<any>>)[
|
|
35
|
+
key
|
|
36
|
+
];
|
|
37
|
+
return Override || DefaultComponent;
|
|
38
|
+
}
|