@c-rex/templates 0.3.0-build.85 → 0.3.0-build.90

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 +3 -3
  2. 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.85",
3
+ "version": "0.3.0-build.90",
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": "~5.8.3"
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 { getIssuerMetadata } from "@c-rex/core/OIDC";
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
- UI_LANG_KEY,
15
- SEARCH_SETTINGS_COOKIE_NAME,
16
- UI_PREFERENCES_COOKIE_NAME,
17
- CONTENT_LANGUAGES_COOKIE_NAME,
18
- PERSONALIZATION_MODES,
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
- children: React.ReactNode;
35
- personalizationAppContext?: string;
32
+ children: React.ReactNode;
33
+ personalizationAppContext?: string;
36
34
  }
37
35
 
38
36
  const getPersonalizationModeSafe = async (): Promise<
39
- (typeof PERSONALIZATION_MODES)[keyof typeof PERSONALIZATION_MODES]
37
+ (typeof PERSONALIZATION_MODES)[keyof typeof PERSONALIZATION_MODES]
40
38
  > => {
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
- }
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
- { value: string; score: number }[]
54
+ { value: string; score: number }[]
57
55
  > => {
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 [];
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
- children,
86
- personalizationAppContext,
83
+ children,
84
+ personalizationAppContext,
87
85
  }: DefaultRootLayoutProps) => {
88
- const [locale, metadata, languageData] = await Promise.all([
89
- getLocale(),
90
- getIssuerMetadata(),
91
- getAvailableLanguagesSafe(),
92
- ]);
86
+ const [locale, languageData] = await Promise.all([
87
+ getLocale(),
88
+ getAvailableLanguagesSafe(),
89
+ ]);
93
90
 
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
- );
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
- sdk.updateConfigProp("OIDC", {
110
- ...serverConfigs.OIDC,
111
- issuerMetadata: metadata,
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
- // 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;
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
- // 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;
129
+ const uiLang = await resolveUILanguage({
130
+ uiLangCookie,
131
+ defaultLang: clientConfigs.languageSwitcher.default,
132
+ });
142
133
 
143
- const uiLang = await resolveUILanguage({
144
- uiLangCookie,
145
- defaultLang: clientConfigs.languageSwitcher.default,
146
- });
147
-
148
- const contentLang = await resolveContentLanguage({
149
- contentLangCookie: searchSettings.language,
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
- 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
- );
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
  };