@c-rex/templates 0.3.0-build.85 → 0.3.0-build.91
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/package.json +3 -3
- package/src/layout.tsx +147 -162
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c-rex/templates",
|
|
3
|
-
"version": "0.3.0-build.
|
|
3
|
+
"version": "0.3.0-build.91",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@types/react": "18.3.31",
|
|
37
37
|
"@types/react-dom": "18.3.7",
|
|
38
38
|
"eslint": "^9.23.0",
|
|
39
|
-
"typescript": "
|
|
39
|
+
"typescript": "5.8.3"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@c-rex/components": "*",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"@c-rex/types": "*",
|
|
49
49
|
"@c-rex/ui": "*",
|
|
50
50
|
"@c-rex/utils": "*",
|
|
51
|
+
"lucide-react": "^0.511.0",
|
|
51
52
|
"cheerio": "^1.1.0",
|
|
52
53
|
"i18next": "^25.1.3",
|
|
53
54
|
"next": "^15",
|
|
54
|
-
"next-auth": "^4.24.11",
|
|
55
55
|
"next-intl": "^4.1.0",
|
|
56
56
|
"nuqs": "^2.4.3",
|
|
57
57
|
"react": "^18.3.1",
|
package/src/layout.tsx
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { NextIntlClientProvider } from "next-intl";
|
|
3
3
|
import { getLocale } from "next-intl/server";
|
|
4
|
-
import { getServerSession, type Session } from "next-auth";
|
|
5
4
|
import { Toaster } from "@c-rex/ui/sonner";
|
|
6
5
|
import { AppConfigProvider } from "@c-rex/contexts/config-provider";
|
|
7
6
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
|
8
7
|
import { cookies } from "next/headers";
|
|
9
8
|
import { CrexSDK } from "@c-rex/core/sdk";
|
|
10
|
-
import {
|
|
9
|
+
import { getSession } from "@c-rex/core/auth/server";
|
|
11
10
|
import { resolveUILanguage, resolveContentLanguage } from "./utils";
|
|
12
11
|
import { informationUnitsLanguagesServer } from "@c-rex/services/server-requests";
|
|
13
12
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
UI_LANG_KEY,
|
|
14
|
+
SEARCH_SETTINGS_COOKIE_NAME,
|
|
15
|
+
UI_PREFERENCES_COOKIE_NAME,
|
|
16
|
+
CONTENT_LANGUAGES_COOKIE_NAME,
|
|
17
|
+
PERSONALIZATION_MODES,
|
|
19
18
|
} from "@c-rex/constants";
|
|
20
19
|
import { transformLanguageData } from "@c-rex/utils";
|
|
21
20
|
import { getSearchSettingsFromCookie } from "@c-rex/components/search-settings-store";
|
|
@@ -28,180 +27,166 @@ import { PulseThemeProvider } from "@c-rex/components/pulse/theme-context";
|
|
|
28
27
|
import { PulseSubscriptionTargetProvider } from "@c-rex/components/pulse/subscription-target-context";
|
|
29
28
|
import { getPersonalizationMode } from "@c-rex/core/personalization-mode";
|
|
30
29
|
import { CrexLogger } from "@c-rex/core/logger";
|
|
31
|
-
import type {} from "@c-rex/core/personalization-session";
|
|
32
30
|
|
|
33
31
|
interface DefaultRootLayoutProps {
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
personalizationAppContext?: string;
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
const getPersonalizationModeSafe = async (): Promise<
|
|
39
|
-
|
|
37
|
+
(typeof PERSONALIZATION_MODES)[keyof typeof PERSONALIZATION_MODES]
|
|
40
38
|
> => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
try {
|
|
40
|
+
return getPersonalizationMode();
|
|
41
|
+
} catch (error) {
|
|
42
|
+
// A misconfigured PULSE_PERSONALIZATION_MODE must not take down the
|
|
43
|
+
// whole app boot — degrade to the feature being off instead of a
|
|
44
|
+
// 500 on every request.
|
|
45
|
+
await new CrexLogger().log({
|
|
46
|
+
level: "error",
|
|
47
|
+
message: `DefaultRootLayout.getPersonalizationModeSafe: invalid PULSE_PERSONALIZATION_MODE, disabling personalization: ${error}`,
|
|
48
|
+
});
|
|
49
|
+
return PERSONALIZATION_MODES.DISABLED;
|
|
50
|
+
}
|
|
53
51
|
};
|
|
54
52
|
|
|
55
53
|
const getAvailableLanguagesSafe = async (): Promise<
|
|
56
|
-
|
|
54
|
+
{ value: string; score: number }[]
|
|
57
55
|
> => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
return languages;
|
|
71
|
-
} catch (error) {
|
|
72
|
-
// Some installations expose the Languages endpoint only intermittently
|
|
73
|
-
// or behind infrastructure that can transiently return 503. The app
|
|
74
|
-
// should still boot with the configured default language instead of
|
|
75
|
-
// failing the entire root layout.
|
|
76
|
-
await new CrexLogger().log({
|
|
77
|
-
level: "warning",
|
|
78
|
-
message: `DefaultRootLayout.getAvailableLanguagesSafe: failed to load available content languages, falling back to defaults: ${error}`,
|
|
79
|
-
});
|
|
80
|
-
return [];
|
|
56
|
+
try {
|
|
57
|
+
const data = await informationUnitsLanguagesServer();
|
|
58
|
+
const languages = (data as { value: string; score: number }[]) || [];
|
|
59
|
+
if (languages.length === 0) {
|
|
60
|
+
// A 200 with an empty body is not a transport failure: the API
|
|
61
|
+
// filters the language list by what the current token may read.
|
|
62
|
+
await new CrexLogger().log({
|
|
63
|
+
level: "warning",
|
|
64
|
+
message:
|
|
65
|
+
"DefaultRootLayout.getAvailableLanguagesSafe: Languages endpoint returned an empty list. Falling back to the persisted languages, if any.",
|
|
66
|
+
});
|
|
81
67
|
}
|
|
68
|
+
return languages;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// Some installations expose the Languages endpoint only intermittently
|
|
71
|
+
// or behind infrastructure that can transiently return 503. The app
|
|
72
|
+
// should still boot with the configured default language instead of
|
|
73
|
+
// failing the entire root layout.
|
|
74
|
+
await new CrexLogger().log({
|
|
75
|
+
level: "warning",
|
|
76
|
+
message: `DefaultRootLayout.getAvailableLanguagesSafe: failed to load available content languages, falling back to defaults: ${error}`,
|
|
77
|
+
});
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
82
80
|
};
|
|
83
81
|
|
|
84
82
|
export const DefaultRootLayout = async ({
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
children,
|
|
84
|
+
personalizationAppContext,
|
|
87
85
|
}: DefaultRootLayoutProps) => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
]);
|
|
86
|
+
const [locale, languageData] = await Promise.all([
|
|
87
|
+
getLocale(),
|
|
88
|
+
getAvailableLanguagesSafe(),
|
|
89
|
+
]);
|
|
93
90
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
);
|
|
91
|
+
const sdk = new CrexSDK();
|
|
92
|
+
const cookieStore = await cookies();
|
|
93
|
+
const uiLangCookie = cookieStore.get(UI_LANG_KEY)?.value ?? null;
|
|
94
|
+
const clientConfigs = await sdk.getClientConfig();
|
|
95
|
+
const personalizationMode = personalizationAppContext
|
|
96
|
+
? await getPersonalizationModeSafe()
|
|
97
|
+
: PERSONALIZATION_MODES.DISABLED;
|
|
98
|
+
const searchSettings = getSearchSettingsFromCookie(
|
|
99
|
+
cookieStore.get(SEARCH_SETTINGS_COOKIE_NAME)?.value,
|
|
100
|
+
);
|
|
101
|
+
const uiPreferences = getUiPreferencesFromCookie(
|
|
102
|
+
cookieStore.get(UI_PREFERENCES_COOKIE_NAME)?.value,
|
|
103
|
+
);
|
|
108
104
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
105
|
+
// The session projection carries the personalization key; it is only
|
|
106
|
+
// needed when Pulse personalizes per user.
|
|
107
|
+
const session =
|
|
108
|
+
personalizationMode === PERSONALIZATION_MODES.PULSE_USER
|
|
109
|
+
? await getSession()
|
|
110
|
+
: null;
|
|
111
|
+
const sessionKey = session?.personalizationSessionKey;
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
113
|
+
// The list is persisted client-side in a cookie by the language store.
|
|
114
|
+
// Reuse it whenever this render could not produce one, so a transient
|
|
115
|
+
// failure or a permission-filtered empty response does not blank out the
|
|
116
|
+
// language switcher for the rest of the session.
|
|
117
|
+
const fetchedLanguages = transformLanguageData(languageData);
|
|
118
|
+
const availableLanguages =
|
|
119
|
+
fetchedLanguages.length > 0
|
|
120
|
+
? fetchedLanguages
|
|
121
|
+
: getAvailableLanguagesFromCookie(
|
|
122
|
+
cookieStore.get(CONTENT_LANGUAGES_COOKIE_NAME)?.value,
|
|
123
|
+
);
|
|
124
|
+
const initialTheme =
|
|
125
|
+
personalizationMode === PERSONALIZATION_MODES.PULSE_USER
|
|
126
|
+
? "system"
|
|
127
|
+
: uiPreferences.theme;
|
|
126
128
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const fetchedLanguages = transformLanguageData(languageData);
|
|
132
|
-
const availableLanguages =
|
|
133
|
-
fetchedLanguages.length > 0
|
|
134
|
-
? fetchedLanguages
|
|
135
|
-
: getAvailableLanguagesFromCookie(
|
|
136
|
-
cookieStore.get(CONTENT_LANGUAGES_COOKIE_NAME)?.value,
|
|
137
|
-
);
|
|
138
|
-
const initialTheme =
|
|
139
|
-
personalizationMode === PERSONALIZATION_MODES.PULSE_USER
|
|
140
|
-
? "system"
|
|
141
|
-
: uiPreferences.theme;
|
|
129
|
+
const uiLang = await resolveUILanguage({
|
|
130
|
+
uiLangCookie,
|
|
131
|
+
defaultLang: clientConfigs.languageSwitcher.default,
|
|
132
|
+
});
|
|
142
133
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
availableLanguages,
|
|
151
|
-
defaultLang:
|
|
152
|
-
clientConfigs.defaultContentLanguage ??
|
|
153
|
-
clientConfigs.languageSwitcher.default,
|
|
154
|
-
});
|
|
134
|
+
const contentLang = await resolveContentLanguage({
|
|
135
|
+
contentLangCookie: searchSettings.language,
|
|
136
|
+
availableLanguages,
|
|
137
|
+
defaultLang:
|
|
138
|
+
clientConfigs.defaultContentLanguage ??
|
|
139
|
+
clientConfigs.languageSwitcher.default,
|
|
140
|
+
});
|
|
155
141
|
|
|
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
|
-
);
|
|
142
|
+
return (
|
|
143
|
+
<html
|
|
144
|
+
lang={locale}
|
|
145
|
+
className={initialTheme === "dark" ? "dark" : undefined}
|
|
146
|
+
data-theme={initialTheme}
|
|
147
|
+
data-font-scale={uiPreferences.fontScale}
|
|
148
|
+
data-readonly-mode={clientConfigs.readOnlyMode ? "true" : "false"}
|
|
149
|
+
style={{
|
|
150
|
+
fontSize:
|
|
151
|
+
uiPreferences.fontScale === "small"
|
|
152
|
+
? "93.75%"
|
|
153
|
+
: uiPreferences.fontScale === "large"
|
|
154
|
+
? "112.5%"
|
|
155
|
+
: "100%",
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
<head>
|
|
159
|
+
<link rel="icon" type="image/x-icon" href="/img/favicon.png" />
|
|
160
|
+
</head>
|
|
161
|
+
<body className="min-h-screen bg-background font-sans antialiased">
|
|
162
|
+
<NextIntlClientProvider>
|
|
163
|
+
<AppConfigProvider
|
|
164
|
+
uiLang={uiLang}
|
|
165
|
+
contentLang={contentLang}
|
|
166
|
+
availableLanguages={availableLanguages}
|
|
167
|
+
initialConfig={clientConfigs}
|
|
168
|
+
>
|
|
169
|
+
<PersonalizationProvider
|
|
170
|
+
mode={personalizationMode}
|
|
171
|
+
sessionKey={sessionKey}
|
|
172
|
+
appContext={personalizationAppContext}
|
|
173
|
+
>
|
|
174
|
+
<PulseSubscriptionTargetProvider>
|
|
175
|
+
<PulseThemeProvider>
|
|
176
|
+
<FavoritesProvider>
|
|
177
|
+
<UiPreferencesRuntime
|
|
178
|
+
initialTheme={initialTheme}
|
|
179
|
+
initialFontScale={uiPreferences.fontScale}
|
|
180
|
+
/>
|
|
181
|
+
<Toaster />
|
|
182
|
+
<NuqsAdapter>{children}</NuqsAdapter>
|
|
183
|
+
</FavoritesProvider>
|
|
184
|
+
</PulseThemeProvider>
|
|
185
|
+
</PulseSubscriptionTargetProvider>
|
|
186
|
+
</PersonalizationProvider>
|
|
187
|
+
</AppConfigProvider>
|
|
188
|
+
</NextIntlClientProvider>
|
|
189
|
+
</body>
|
|
190
|
+
</html>
|
|
191
|
+
);
|
|
207
192
|
};
|