@c-rex/templates 0.3.0-build.71 → 0.3.0-build.74
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 +2 -1
- package/src/layout.tsx +172 -93
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.74",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src"
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"cheerio": "^1.1.0",
|
|
52
52
|
"i18next": "^25.1.3",
|
|
53
53
|
"next": "^15",
|
|
54
|
+
"next-auth": "^4.24.11",
|
|
54
55
|
"next-intl": "^4.1.0",
|
|
55
56
|
"nuqs": "^2.4.3",
|
|
56
57
|
"react": "^18.3.1",
|
package/src/layout.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
import { Toaster } from "@c-rex/ui/sonner";
|
|
5
6
|
import { AppConfigProvider } from "@c-rex/contexts/config-provider";
|
|
6
7
|
import { NuqsAdapter } from "nuqs/adapters/next/app";
|
|
@@ -10,119 +11,197 @@ import { getIssuerMetadata } from "@c-rex/core/OIDC";
|
|
|
10
11
|
import { resolveUILanguage, resolveContentLanguage } from "./utils";
|
|
11
12
|
import { informationUnitsLanguagesServer } from "@c-rex/services/server-requests";
|
|
12
13
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
UI_LANG_KEY,
|
|
15
|
+
SEARCH_SETTINGS_COOKIE_NAME,
|
|
16
|
+
UI_PREFERENCES_COOKIE_NAME,
|
|
17
|
+
CONTENT_LANGUAGES_COOKIE_NAME,
|
|
18
|
+
PERSONALIZATION_MODES,
|
|
16
19
|
} from "@c-rex/constants";
|
|
17
20
|
import { transformLanguageData } from "@c-rex/utils";
|
|
18
21
|
import { getSearchSettingsFromCookie } from "@c-rex/components/search-settings-store";
|
|
22
|
+
import { getAvailableLanguagesFromCookie } from "@c-rex/components/language-store";
|
|
19
23
|
import { FavoritesProvider } from "@c-rex/components/favorites-context";
|
|
20
24
|
import { getUiPreferencesFromCookie } from "@c-rex/components/ui-preferences-store";
|
|
21
25
|
import { UiPreferencesRuntime } from "@c-rex/components/ui-preferences-runtime";
|
|
26
|
+
import { PersonalizationProvider } from "@c-rex/components/pulse/personalization-context";
|
|
27
|
+
import { PulseThemeProvider } from "@c-rex/components/pulse/theme-context";
|
|
28
|
+
import { PulseSubscriptionTargetProvider } from "@c-rex/components/pulse/subscription-target-context";
|
|
29
|
+
import { getPersonalizationMode } from "@c-rex/core/personalization-mode";
|
|
30
|
+
import { CrexLogger } from "@c-rex/core/logger";
|
|
31
|
+
import type {} from "@c-rex/core/personalization-session";
|
|
22
32
|
|
|
23
33
|
interface DefaultRootLayoutProps {
|
|
24
|
-
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
personalizationAppContext?: string;
|
|
25
36
|
}
|
|
26
37
|
|
|
38
|
+
const getPersonalizationModeSafe = async (): Promise<
|
|
39
|
+
(typeof PERSONALIZATION_MODES)[keyof typeof PERSONALIZATION_MODES]
|
|
40
|
+
> => {
|
|
41
|
+
try {
|
|
42
|
+
return getPersonalizationMode();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// A misconfigured PULSE_PERSONALIZATION_MODE must not take down the
|
|
45
|
+
// whole app boot — degrade to the feature being off instead of a
|
|
46
|
+
// 500 on every request.
|
|
47
|
+
await new CrexLogger().log({
|
|
48
|
+
level: "error",
|
|
49
|
+
message: `DefaultRootLayout.getPersonalizationModeSafe: invalid PULSE_PERSONALIZATION_MODE, disabling personalization: ${error}`,
|
|
50
|
+
});
|
|
51
|
+
return PERSONALIZATION_MODES.DISABLED;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
27
55
|
const getAvailableLanguagesSafe = async (): Promise<
|
|
28
|
-
|
|
56
|
+
{ value: string; score: number }[]
|
|
29
57
|
> => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
58
|
+
try {
|
|
59
|
+
const data = await informationUnitsLanguagesServer();
|
|
60
|
+
const languages = (data as { value: string; score: number }[]) || [];
|
|
61
|
+
if (languages.length === 0) {
|
|
62
|
+
// A 200 with an empty body is not a transport failure: the API
|
|
63
|
+
// filters the language list by what the current token may read.
|
|
64
|
+
await new CrexLogger().log({
|
|
65
|
+
level: "warning",
|
|
66
|
+
message:
|
|
67
|
+
"DefaultRootLayout.getAvailableLanguagesSafe: Languages endpoint returned an empty list. Falling back to the persisted languages, if any.",
|
|
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 [];
|
|
81
|
+
}
|
|
44
82
|
};
|
|
45
83
|
|
|
46
84
|
export const DefaultRootLayout = async ({
|
|
47
|
-
|
|
85
|
+
children,
|
|
86
|
+
personalizationAppContext,
|
|
48
87
|
}: DefaultRootLayoutProps) => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
88
|
+
const [locale, metadata, languageData] = await Promise.all([
|
|
89
|
+
getLocale(),
|
|
90
|
+
getIssuerMetadata(),
|
|
91
|
+
getAvailableLanguagesSafe(),
|
|
92
|
+
]);
|
|
54
93
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
94
|
+
const sdk = new CrexSDK();
|
|
95
|
+
const cookieStore = await cookies();
|
|
96
|
+
const uiLangCookie = cookieStore.get(UI_LANG_KEY)?.value ?? null;
|
|
97
|
+
const clientConfigs = await sdk.getClientConfig();
|
|
98
|
+
const serverConfigs = sdk.getServerConfig();
|
|
99
|
+
const personalizationMode = personalizationAppContext
|
|
100
|
+
? await getPersonalizationModeSafe()
|
|
101
|
+
: PERSONALIZATION_MODES.DISABLED;
|
|
102
|
+
const searchSettings = getSearchSettingsFromCookie(
|
|
103
|
+
cookieStore.get(SEARCH_SETTINGS_COOKIE_NAME)?.value,
|
|
104
|
+
);
|
|
105
|
+
const uiPreferences = getUiPreferencesFromCookie(
|
|
106
|
+
cookieStore.get(UI_PREFERENCES_COOKIE_NAME)?.value,
|
|
107
|
+
);
|
|
66
108
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
sdk.updateConfigProp("OIDC", {
|
|
110
|
+
...serverConfigs.OIDC,
|
|
111
|
+
issuerMetadata: metadata,
|
|
112
|
+
});
|
|
71
113
|
|
|
72
|
-
|
|
114
|
+
// The personalization key is added by the SDK's custom session callback.
|
|
115
|
+
// Calling getServerSession without these options uses NextAuth's default
|
|
116
|
+
// callback and silently drops that key, leaving authenticated Pulse users
|
|
117
|
+
// looking unauthenticated in the browser.
|
|
118
|
+
const userAuthConfig =
|
|
119
|
+
personalizationMode === PERSONALIZATION_MODES.PULSE_USER
|
|
120
|
+
? await sdk.getUserAuthConfig()
|
|
121
|
+
: undefined;
|
|
122
|
+
const session: Session | null = userAuthConfig
|
|
123
|
+
? ((await getServerSession(userAuthConfig)) as Session | null)
|
|
124
|
+
: null;
|
|
125
|
+
const sessionKey = session?.personalizationSessionKey;
|
|
73
126
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
127
|
+
// The list is persisted client-side in a cookie by the language store.
|
|
128
|
+
// Reuse it whenever this render could not produce one, so a transient
|
|
129
|
+
// failure or a permission-filtered empty response does not blank out the
|
|
130
|
+
// language switcher for the rest of the session.
|
|
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;
|
|
78
142
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
clientConfigs.defaultContentLanguage ??
|
|
84
|
-
clientConfigs.languageSwitcher.default,
|
|
85
|
-
});
|
|
143
|
+
const uiLang = await resolveUILanguage({
|
|
144
|
+
uiLangCookie,
|
|
145
|
+
defaultLang: clientConfigs.languageSwitcher.default,
|
|
146
|
+
});
|
|
86
147
|
|
|
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
|
-
|
|
148
|
+
const contentLang = await resolveContentLanguage({
|
|
149
|
+
contentLangCookie: searchSettings.language,
|
|
150
|
+
availableLanguages,
|
|
151
|
+
defaultLang:
|
|
152
|
+
clientConfigs.defaultContentLanguage ??
|
|
153
|
+
clientConfigs.languageSwitcher.default,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<html
|
|
158
|
+
lang={locale}
|
|
159
|
+
className={initialTheme === "dark" ? "dark" : undefined}
|
|
160
|
+
data-theme={initialTheme}
|
|
161
|
+
data-font-scale={uiPreferences.fontScale}
|
|
162
|
+
data-readonly-mode={clientConfigs.readOnlyMode ? "true" : "false"}
|
|
163
|
+
style={{
|
|
164
|
+
fontSize:
|
|
165
|
+
uiPreferences.fontScale === "small"
|
|
166
|
+
? "93.75%"
|
|
167
|
+
: uiPreferences.fontScale === "large"
|
|
168
|
+
? "112.5%"
|
|
169
|
+
: "100%",
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<head>
|
|
173
|
+
<link rel="icon" type="image/x-icon" href="/img/favicon.png" />
|
|
174
|
+
</head>
|
|
175
|
+
<body className="min-h-screen bg-background font-sans antialiased">
|
|
176
|
+
<NextIntlClientProvider>
|
|
177
|
+
<AppConfigProvider
|
|
178
|
+
uiLang={uiLang}
|
|
179
|
+
contentLang={contentLang}
|
|
180
|
+
availableLanguages={availableLanguages}
|
|
181
|
+
initialConfig={clientConfigs}
|
|
182
|
+
loginEnabled={Boolean(serverConfigs.OIDC.client.enabled)}
|
|
183
|
+
>
|
|
184
|
+
<PersonalizationProvider
|
|
185
|
+
mode={personalizationMode}
|
|
186
|
+
sessionKey={sessionKey}
|
|
187
|
+
appContext={personalizationAppContext}
|
|
188
|
+
>
|
|
189
|
+
<PulseSubscriptionTargetProvider>
|
|
190
|
+
<PulseThemeProvider>
|
|
191
|
+
<FavoritesProvider>
|
|
192
|
+
<UiPreferencesRuntime
|
|
193
|
+
initialTheme={initialTheme}
|
|
194
|
+
initialFontScale={uiPreferences.fontScale}
|
|
195
|
+
/>
|
|
196
|
+
<Toaster />
|
|
197
|
+
<NuqsAdapter>{children}</NuqsAdapter>
|
|
198
|
+
</FavoritesProvider>
|
|
199
|
+
</PulseThemeProvider>
|
|
200
|
+
</PulseSubscriptionTargetProvider>
|
|
201
|
+
</PersonalizationProvider>
|
|
202
|
+
</AppConfigProvider>
|
|
203
|
+
</NextIntlClientProvider>
|
|
204
|
+
</body>
|
|
205
|
+
</html>
|
|
206
|
+
);
|
|
128
207
|
};
|