@c-rex/templates 0.3.0-build.70 → 0.3.0-build.72

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