create-brainerce-store 1.52.4 → 1.53.0
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/LICENSE +21 -0
- package/dist/index.js +4 -2
- package/messages/en.json +2 -0
- package/messages/he.json +2 -0
- package/package.json +1 -1
- package/templates/nextjs/base/next.config.ts +86 -76
- package/templates/nextjs/base/src/app/layout.tsx.ejs +227 -223
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +118 -4
- package/templates/nextjs/base/src/core/providers/store-provider.tsx.ejs +10 -1
- package/templates/nextjs/designs/atelier/app-overlay/layout.tsx.ejs +255 -251
- package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -269
|
@@ -1,251 +1,255 @@
|
|
|
1
|
-
<% if (i18nEnabled) { %>
|
|
2
|
-
import type { Metadata } from 'next';
|
|
3
|
-
import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
|
|
4
|
-
import { StoreProvider } from '@/core/providers/store-provider';
|
|
5
|
-
import { AnnouncementBar } from '@/ui/layout/announcement-bar';
|
|
6
|
-
import { SiteHeader } from '@/ui/layout/site-header';
|
|
7
|
-
import { SiteFooter } from '@/ui/layout/site-footer';
|
|
8
|
-
import { CartDrawer } from '@/ui/cart/cart-drawer';
|
|
9
|
-
import { BrainerceBotWidget } from '@/components/brainerce-bot';
|
|
10
|
-
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
|
|
11
|
-
import { getDirection, supportedLocales } from '@/i18n';
|
|
12
|
-
import { getNonce } from '@/core/lib/nonce';
|
|
13
|
-
import '../globals.css';
|
|
14
|
-
|
|
15
|
-
// Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
|
|
16
|
-
// (Hebrew serif — the gallery voice) for display. Exposed as CSS variables
|
|
17
|
-
// consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
|
|
18
|
-
const sans = Assistant({
|
|
19
|
-
subsets: ['hebrew', 'latin'],
|
|
20
|
-
weight: ['400', '500', '600', '700'],
|
|
21
|
-
variable: '--font-sans',
|
|
22
|
-
});
|
|
23
|
-
const serif = Frank_Ruhl_Libre({
|
|
24
|
-
subsets: ['hebrew', 'latin'],
|
|
25
|
-
weight: ['400', '500', '700'],
|
|
26
|
-
variable: '--font-serif',
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
30
|
-
|
|
31
|
-
export const metadata: Metadata = {
|
|
32
|
-
metadataBase: new URL(baseUrl),
|
|
33
|
-
title: {
|
|
34
|
-
default: <%- storeNameJs %>,
|
|
35
|
-
template: <%- titleTemplateJs %>,
|
|
36
|
-
},
|
|
37
|
-
description: <%- storeNameJs %>,
|
|
38
|
-
alternates: {
|
|
39
|
-
canonical: '/',
|
|
40
|
-
},
|
|
41
|
-
openGraph: {
|
|
42
|
-
siteName: <%- storeNameJs %>,
|
|
43
|
-
type: 'website',
|
|
44
|
-
},
|
|
45
|
-
robots: {
|
|
46
|
-
index: true,
|
|
47
|
-
follow: true,
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const organizationJsonLd = {
|
|
52
|
-
'@context': 'https://schema.org',
|
|
53
|
-
'@type': 'Organization',
|
|
54
|
-
name: <%- storeNameJs %>,
|
|
55
|
-
url: baseUrl,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
export function generateStaticParams() {
|
|
59
|
-
return supportedLocales.map((locale) => ({ locale }));
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export default async function RootLayout({
|
|
63
|
-
children,
|
|
64
|
-
params,
|
|
65
|
-
}: {
|
|
66
|
-
children: React.ReactNode;
|
|
67
|
-
params: Promise<{ locale: string }>;
|
|
68
|
-
}) {
|
|
69
|
-
const { locale } = await params;
|
|
70
|
-
const dir = getDirection(locale);
|
|
71
|
-
const nonce = await getNonce();
|
|
72
|
-
|
|
73
|
-
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
74
|
-
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
75
|
-
// seeded a particular content type yet. New stores ship with default rows
|
|
76
|
-
// seeded by the backend (StoresService.seedDefaultContent).
|
|
77
|
-
const client = getServerClient(locale);
|
|
78
|
-
const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
|
|
79
|
-
client.content.announcement.list(locale).catch(() => []),
|
|
80
|
-
client.content.header.get('main', locale).catch(() => null),
|
|
81
|
-
client.content.footer.get('main', locale).catch(() => null),
|
|
82
|
-
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
83
|
-
// render with the real currency, feature flags, and i18n config at frame 0
|
|
84
|
-
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
85
|
-
fetchStoreInfo(locale),
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
import {
|
|
136
|
-
import {
|
|
137
|
-
import {
|
|
138
|
-
import {
|
|
139
|
-
import {
|
|
140
|
-
import {
|
|
141
|
-
import '
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
1
|
+
<% if (i18nEnabled) { %>
|
|
2
|
+
import type { Metadata } from 'next';
|
|
3
|
+
import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
|
|
4
|
+
import { StoreProvider } from '@/core/providers/store-provider';
|
|
5
|
+
import { AnnouncementBar } from '@/ui/layout/announcement-bar';
|
|
6
|
+
import { SiteHeader } from '@/ui/layout/site-header';
|
|
7
|
+
import { SiteFooter } from '@/ui/layout/site-footer';
|
|
8
|
+
import { CartDrawer } from '@/ui/cart/cart-drawer';
|
|
9
|
+
import { BrainerceBotWidget } from '@/components/brainerce-bot';
|
|
10
|
+
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
|
|
11
|
+
import { getDirection, getMessages, supportedLocales } from '@/i18n';
|
|
12
|
+
import { getNonce } from '@/core/lib/nonce';
|
|
13
|
+
import '../globals.css';
|
|
14
|
+
|
|
15
|
+
// Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
|
|
16
|
+
// (Hebrew serif — the gallery voice) for display. Exposed as CSS variables
|
|
17
|
+
// consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
|
|
18
|
+
const sans = Assistant({
|
|
19
|
+
subsets: ['hebrew', 'latin'],
|
|
20
|
+
weight: ['400', '500', '600', '700'],
|
|
21
|
+
variable: '--font-sans',
|
|
22
|
+
});
|
|
23
|
+
const serif = Frank_Ruhl_Libre({
|
|
24
|
+
subsets: ['hebrew', 'latin'],
|
|
25
|
+
weight: ['400', '500', '700'],
|
|
26
|
+
variable: '--font-serif',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
30
|
+
|
|
31
|
+
export const metadata: Metadata = {
|
|
32
|
+
metadataBase: new URL(baseUrl),
|
|
33
|
+
title: {
|
|
34
|
+
default: <%- storeNameJs %>,
|
|
35
|
+
template: <%- titleTemplateJs %>,
|
|
36
|
+
},
|
|
37
|
+
description: <%- storeNameJs %>,
|
|
38
|
+
alternates: {
|
|
39
|
+
canonical: '/',
|
|
40
|
+
},
|
|
41
|
+
openGraph: {
|
|
42
|
+
siteName: <%- storeNameJs %>,
|
|
43
|
+
type: 'website',
|
|
44
|
+
},
|
|
45
|
+
robots: {
|
|
46
|
+
index: true,
|
|
47
|
+
follow: true,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const organizationJsonLd = {
|
|
52
|
+
'@context': 'https://schema.org',
|
|
53
|
+
'@type': 'Organization',
|
|
54
|
+
name: <%- storeNameJs %>,
|
|
55
|
+
url: baseUrl,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function generateStaticParams() {
|
|
59
|
+
return supportedLocales.map((locale) => ({ locale }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default async function RootLayout({
|
|
63
|
+
children,
|
|
64
|
+
params,
|
|
65
|
+
}: {
|
|
66
|
+
children: React.ReactNode;
|
|
67
|
+
params: Promise<{ locale: string }>;
|
|
68
|
+
}) {
|
|
69
|
+
const { locale } = await params;
|
|
70
|
+
const dir = getDirection(locale);
|
|
71
|
+
const nonce = await getNonce();
|
|
72
|
+
|
|
73
|
+
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
74
|
+
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
75
|
+
// seeded a particular content type yet. New stores ship with default rows
|
|
76
|
+
// seeded by the backend (StoresService.seedDefaultContent).
|
|
77
|
+
const client = getServerClient(locale);
|
|
78
|
+
const [announcements, siteHeader, siteFooter, storeInfo, messages] = await Promise.all([
|
|
79
|
+
client.content.announcement.list(locale).catch(() => []),
|
|
80
|
+
client.content.header.get('main', locale).catch(() => null),
|
|
81
|
+
client.content.footer.get('main', locale).catch(() => null),
|
|
82
|
+
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
83
|
+
// render with the real currency, feature flags, and i18n config at frame 0
|
|
84
|
+
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
85
|
+
fetchStoreInfo(locale),
|
|
86
|
+
// SSR-fetch UI message strings so translated text renders at frame 0 —
|
|
87
|
+
// without this, every page (and every locale switch) shows raw
|
|
88
|
+
// "namespace.key" strings until the client-only fetch resolves.
|
|
89
|
+
getMessages(locale),
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<html lang={locale} dir={dir}>
|
|
94
|
+
<head>
|
|
95
|
+
<script
|
|
96
|
+
type="application/ld+json"
|
|
97
|
+
nonce={nonce}
|
|
98
|
+
suppressHydrationWarning
|
|
99
|
+
dangerouslySetInnerHTML={{
|
|
100
|
+
__html: JSON.stringify(organizationJsonLd)
|
|
101
|
+
.replace(/</g, '\\u003c')
|
|
102
|
+
.replace(/>/g, '\\u003e')
|
|
103
|
+
.replace(/&/g, '\\u0026'),
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
106
|
+
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
107
|
+
SPA route changes. Cookieless (no consent banner needed); the
|
|
108
|
+
merchant can toggle it per sales channel in the dashboard. */}
|
|
109
|
+
<script
|
|
110
|
+
defer
|
|
111
|
+
src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
|
|
112
|
+
data-channel={
|
|
113
|
+
process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
|
|
114
|
+
}
|
|
115
|
+
nonce={nonce}
|
|
116
|
+
suppressHydrationWarning
|
|
117
|
+
/>
|
|
118
|
+
</head>
|
|
119
|
+
<body className={`${sans.variable} ${serif.variable} font-sans`}>
|
|
120
|
+
<StoreProvider locale={locale} initialStoreInfo={storeInfo} initialMessages={messages}>
|
|
121
|
+
<div className="min-h-screen flex flex-col">
|
|
122
|
+
<AnnouncementBar announcements={announcements} />
|
|
123
|
+
<SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
|
|
124
|
+
<main className="flex-1">{children}</main>
|
|
125
|
+
<SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} locale={locale} />
|
|
126
|
+
</div>
|
|
127
|
+
<CartDrawer />
|
|
128
|
+
<BrainerceBotWidget />
|
|
129
|
+
</StoreProvider>
|
|
130
|
+
</body>
|
|
131
|
+
</html>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
<% } else { %>
|
|
135
|
+
import type { Metadata } from 'next';
|
|
136
|
+
import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
|
|
137
|
+
import { StoreProvider } from '@/core/providers/store-provider';
|
|
138
|
+
import { AnnouncementBar } from '@/ui/layout/announcement-bar';
|
|
139
|
+
import { SiteHeader } from '@/ui/layout/site-header';
|
|
140
|
+
import { SiteFooter } from '@/ui/layout/site-footer';
|
|
141
|
+
import { CartDrawer } from '@/ui/cart/cart-drawer';
|
|
142
|
+
import { BrainerceBotWidget } from '@/components/brainerce-bot';
|
|
143
|
+
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
|
|
144
|
+
import { getNonce } from '@/core/lib/nonce';
|
|
145
|
+
import './globals.css';
|
|
146
|
+
|
|
147
|
+
// Atelier: Assistant (clean Hebrew grotesque) for body, Frank Ruhl Libre
|
|
148
|
+
// (Hebrew serif — the gallery voice) for display. Exposed as CSS variables
|
|
149
|
+
// consumed by tailwind's font-sans / font-serif and the h1-h3 base styles.
|
|
150
|
+
const sans = Assistant({
|
|
151
|
+
subsets: ['hebrew', 'latin'],
|
|
152
|
+
weight: ['400', '500', '600', '700'],
|
|
153
|
+
variable: '--font-sans',
|
|
154
|
+
});
|
|
155
|
+
const serif = Frank_Ruhl_Libre({
|
|
156
|
+
subsets: ['hebrew', 'latin'],
|
|
157
|
+
weight: ['400', '500', '700'],
|
|
158
|
+
variable: '--font-serif',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
162
|
+
|
|
163
|
+
export const metadata: Metadata = {
|
|
164
|
+
metadataBase: new URL(baseUrl),
|
|
165
|
+
title: {
|
|
166
|
+
default: <%- storeNameJs %>,
|
|
167
|
+
template: <%- titleTemplateJs %>,
|
|
168
|
+
},
|
|
169
|
+
description: <%- storeNameJs %>,
|
|
170
|
+
alternates: {
|
|
171
|
+
canonical: '/',
|
|
172
|
+
},
|
|
173
|
+
openGraph: {
|
|
174
|
+
siteName: <%- storeNameJs %>,
|
|
175
|
+
locale: '<%= ogLocale %>',
|
|
176
|
+
type: 'website',
|
|
177
|
+
},
|
|
178
|
+
robots: {
|
|
179
|
+
index: true,
|
|
180
|
+
follow: true,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const organizationJsonLd = {
|
|
185
|
+
'@context': 'https://schema.org',
|
|
186
|
+
'@type': 'Organization',
|
|
187
|
+
name: <%- storeNameJs %>,
|
|
188
|
+
url: baseUrl,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export default async function RootLayout({
|
|
192
|
+
children,
|
|
193
|
+
}: {
|
|
194
|
+
children: React.ReactNode;
|
|
195
|
+
}) {
|
|
196
|
+
const nonce = await getNonce();
|
|
197
|
+
|
|
198
|
+
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
199
|
+
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
200
|
+
// seeded a particular content type yet. New stores ship with default rows
|
|
201
|
+
// seeded by the backend (StoresService.seedDefaultContent).
|
|
202
|
+
const client = getServerClient();
|
|
203
|
+
const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
|
|
204
|
+
client.content.announcement.list().catch(() => []),
|
|
205
|
+
client.content.header.get('main').catch(() => null),
|
|
206
|
+
client.content.footer.get('main').catch(() => null),
|
|
207
|
+
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
208
|
+
// render with the real currency, feature flags, and i18n config at frame 0
|
|
209
|
+
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
210
|
+
fetchStoreInfo(),
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<html lang="<%= language %>" dir="<%= direction %>">
|
|
215
|
+
<head>
|
|
216
|
+
<script
|
|
217
|
+
type="application/ld+json"
|
|
218
|
+
nonce={nonce}
|
|
219
|
+
suppressHydrationWarning
|
|
220
|
+
dangerouslySetInnerHTML={{
|
|
221
|
+
__html: JSON.stringify(organizationJsonLd)
|
|
222
|
+
.replace(/</g, '\\u003c')
|
|
223
|
+
.replace(/>/g, '\\u003e')
|
|
224
|
+
.replace(/&/g, '\\u0026'),
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
228
|
+
SPA route changes. Cookieless (no consent banner needed); the
|
|
229
|
+
merchant can toggle it per sales channel in the dashboard. */}
|
|
230
|
+
<script
|
|
231
|
+
defer
|
|
232
|
+
src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
|
|
233
|
+
data-channel={
|
|
234
|
+
process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
|
|
235
|
+
}
|
|
236
|
+
nonce={nonce}
|
|
237
|
+
suppressHydrationWarning
|
|
238
|
+
/>
|
|
239
|
+
</head>
|
|
240
|
+
<body className={`${sans.variable} ${serif.variable} font-sans`}>
|
|
241
|
+
<StoreProvider initialStoreInfo={storeInfo}>
|
|
242
|
+
<div className="min-h-screen flex flex-col">
|
|
243
|
+
<AnnouncementBar announcements={announcements} />
|
|
244
|
+
<SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
|
|
245
|
+
<main className="flex-1">{children}</main>
|
|
246
|
+
<SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
|
|
247
|
+
</div>
|
|
248
|
+
<CartDrawer />
|
|
249
|
+
<BrainerceBotWidget />
|
|
250
|
+
</StoreProvider>
|
|
251
|
+
</body>
|
|
252
|
+
</html>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
<% } %>
|