mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Cookie consent banner and footer on login screen (#5042)
This commit is contained in:
@@ -159,6 +159,8 @@ public class RequestUriUtils {
|
|||||||
|| trimmedUri.startsWith(
|
|| trimmedUri.startsWith(
|
||||||
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers +
|
"/api/v1/proprietary/ui-data/login") // Login page config (SSO providers +
|
||||||
// enableLogin)
|
// enableLogin)
|
||||||
|
|| trimmedUri.startsWith(
|
||||||
|
"/api/v1/ui-data/footer-info") // Public footer configuration
|
||||||
|| trimmedUri.startsWith("/v1/api-docs")
|
|| trimmedUri.startsWith("/v1/api-docs")
|
||||||
|| trimmedUri.startsWith("/api/v1/invite/validate")
|
|| trimmedUri.startsWith("/api/v1/invite/validate")
|
||||||
|| trimmedUri.startsWith("/api/v1/invite/accept")
|
|| trimmedUri.startsWith("/api/v1/invite/accept")
|
||||||
|
|||||||
@@ -58,6 +58,21 @@ public class UIDataController {
|
|||||||
this.runtimePathConfig = runtimePathConfig;
|
this.runtimePathConfig = runtimePathConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/footer-info")
|
||||||
|
@Operation(summary = "Get public footer configuration data")
|
||||||
|
public ResponseEntity<FooterData> getFooterData() {
|
||||||
|
FooterData data = new FooterData();
|
||||||
|
data.setAnalyticsEnabled(applicationProperties.getSystem().getEnableAnalytics());
|
||||||
|
data.setTermsAndConditions(applicationProperties.getLegal().getTermsAndConditions());
|
||||||
|
data.setPrivacyPolicy(applicationProperties.getLegal().getPrivacyPolicy());
|
||||||
|
data.setAccessibilityStatement(
|
||||||
|
applicationProperties.getLegal().getAccessibilityStatement());
|
||||||
|
data.setCookiePolicy(applicationProperties.getLegal().getCookiePolicy());
|
||||||
|
data.setImpressum(applicationProperties.getLegal().getImpressum());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/home")
|
@GetMapping("/home")
|
||||||
@Operation(summary = "Get home page data")
|
@Operation(summary = "Get home page data")
|
||||||
public ResponseEntity<HomeData> getHomeData() {
|
public ResponseEntity<HomeData> getHomeData() {
|
||||||
@@ -237,6 +252,16 @@ public class UIDataController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Data classes
|
// Data classes
|
||||||
|
@Data
|
||||||
|
public static class FooterData {
|
||||||
|
private Boolean analyticsEnabled;
|
||||||
|
private String termsAndConditions;
|
||||||
|
private String privacyPolicy;
|
||||||
|
private String accessibilityStatement;
|
||||||
|
private String cookiePolicy;
|
||||||
|
private String impressum;
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class HomeData {
|
public static class HomeData {
|
||||||
private boolean showSurveyFromDocker;
|
private boolean showSurveyFromDocker;
|
||||||
|
|||||||
@@ -24,8 +24,8 @@
|
|||||||
--cc-toggle-on-knob-bg: var(--cc-btn-primary-color);
|
--cc-toggle-on-knob-bg: var(--cc-btn-primary-color);
|
||||||
--cc-toggle-off-knob-bg: var(--cc-btn-primary-color);
|
--cc-toggle-off-knob-bg: var(--cc-btn-primary-color);
|
||||||
|
|
||||||
--cc-toggle-enabled-icon-color: var(--cc-btn-primary-color);
|
--cc-toggle-enabled-icon-color: var(--cc-btn-primary-color);
|
||||||
--cc-toggle-disabled-icon-color: var(--cc-btn-primary-color);
|
--cc-toggle-disabled-icon-color: var(--cc-btn-primary-color);
|
||||||
|
|
||||||
--cc-toggle-readonly-bg: var(--md-sys-color-surface);
|
--cc-toggle-readonly-bg: var(--md-sys-color-surface);
|
||||||
--cc-toggle-readonly-knob-bg: var(--md-sys-color-outline);
|
--cc-toggle-readonly-knob-bg: var(--md-sys-color-outline);
|
||||||
@@ -34,10 +34,10 @@
|
|||||||
--cc-section-category-border: var(--md-sys-color-outline);
|
--cc-section-category-border: var(--md-sys-color-outline);
|
||||||
|
|
||||||
--cc-cookie-category-block-bg: var(--cc-btn-secondary-bg);
|
--cc-cookie-category-block-bg: var(--cc-btn-secondary-bg);
|
||||||
--cc-cookie-category-block-border: var(--cc-btn-secondary-bg);
|
--cc-cookie-category-block-border: var(--cc-btn-secondary-bg);
|
||||||
--cc-cookie-category-block-hover-bg: var(--cc-btn-secondary-bg);
|
--cc-cookie-category-block-hover-bg: var(--cc-btn-secondary-bg);
|
||||||
--cc-cookie-category-block-hover-border: var(--cc-btn-secondary-bg);
|
--cc-cookie-category-block-hover-border: var(--cc-btn-secondary-bg);
|
||||||
|
|
||||||
--cc-cookie-category-expanded-block-bg: var(--cc-btn-secondary-bg);
|
--cc-cookie-category-expanded-block-bg: var(--cc-btn-secondary-bg);
|
||||||
--cc-cookie-category-expanded-block-hover-bg: var(--cc-toggle-readonly-bg);
|
--cc-cookie-category-expanded-block-hover-bg: var(--cc-toggle-readonly-bg);
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
*/
|
*/
|
||||||
--cc-footer-bg: var(--cc-bg);
|
--cc-footer-bg: var(--cc-bg);
|
||||||
--cc-footer-color: var(--cc-primary-color);
|
--cc-footer-color: var(--cc-primary-color);
|
||||||
--cc-footer-border-color: var(--cc-bg);
|
--cc-footer-border-color: var(--cc-bg);
|
||||||
}
|
}
|
||||||
.cm__body{
|
.cm__body{
|
||||||
max-width: 90% !important;
|
max-width: 90% !important;
|
||||||
@@ -81,4 +81,4 @@
|
|||||||
/* Lower z-index so cookie banner appears behind onboarding modals */
|
/* Lower z-index so cookie banner appears behind onboarding modals */
|
||||||
#cc-main {
|
#cc-main {
|
||||||
z-index: 100 !important;
|
z-index: 100 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -299,7 +299,8 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
contextPath + "/api/v1/auth/refresh",
|
contextPath + "/api/v1/auth/refresh",
|
||||||
contextPath + "/api/v1/auth/me",
|
contextPath + "/api/v1/auth/me",
|
||||||
contextPath + "/api/v1/invite/validate",
|
contextPath + "/api/v1/invite/validate",
|
||||||
contextPath + "/api/v1/invite/accept"
|
contextPath + "/api/v1/invite/accept",
|
||||||
|
contextPath + "/api/v1/ui-data/footer-info"
|
||||||
};
|
};
|
||||||
|
|
||||||
for (String pattern : publicApiPatterns) {
|
for (String pattern : publicApiPatterns) {
|
||||||
|
|||||||
@@ -54,17 +54,17 @@
|
|||||||
--cc-secondary-color: #b0b0b0;
|
--cc-secondary-color: #b0b0b0;
|
||||||
|
|
||||||
--cc-btn-primary-bg: #4dabf7;
|
--cc-btn-primary-bg: #4dabf7;
|
||||||
--cc-btn-primary-color: #2d2d2d;
|
--cc-btn-primary-color: #ffffff;
|
||||||
--cc-btn-primary-border-color: #4dabf7;
|
--cc-btn-primary-border-color: #4dabf7;
|
||||||
--cc-btn-primary-hover-bg: #3d3d3d;
|
--cc-btn-primary-hover-bg: #3d3d3d;
|
||||||
--cc-btn-primary-hover-color: #e5e5e5;
|
--cc-btn-primary-hover-color: #ffffff;
|
||||||
--cc-btn-primary-hover-border-color: #3d3d3d;
|
--cc-btn-primary-hover-border-color: #3d3d3d;
|
||||||
|
|
||||||
--cc-btn-secondary-bg: #3d3d3d;
|
--cc-btn-secondary-bg: #3d3d3d;
|
||||||
--cc-btn-secondary-color: #e5e5e5;
|
--cc-btn-secondary-color: #ffffff;
|
||||||
--cc-btn-secondary-border-color: #3d3d3d;
|
--cc-btn-secondary-border-color: #3d3d3d;
|
||||||
--cc-btn-secondary-hover-bg: #4dabf7;
|
--cc-btn-secondary-hover-bg: #4dabf7;
|
||||||
--cc-btn-secondary-hover-color: #2d2d2d;
|
--cc-btn-secondary-hover-color: #ffffff;
|
||||||
--cc-btn-secondary-hover-border-color: #4dabf7;
|
--cc-btn-secondary-hover-border-color: #4dabf7;
|
||||||
|
|
||||||
--cc-separator-border-color: #555555;
|
--cc-separator-border-color: #555555;
|
||||||
@@ -180,4 +180,27 @@
|
|||||||
/* Lower z-index so cookie banner appears behind onboarding modals */
|
/* Lower z-index so cookie banner appears behind onboarding modals */
|
||||||
#cc-main {
|
#cc-main {
|
||||||
z-index: 100 !important;
|
z-index: 100 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure consent modal text is visible in both themes */
|
||||||
|
#cc-main .cm {
|
||||||
|
background: var(--cc-bg) !important;
|
||||||
|
color: var(--cc-primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cc-main .cm__title {
|
||||||
|
color: var(--cc-primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cc-main .cm__desc {
|
||||||
|
color: var(--cc-primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cc-main .cm__footer {
|
||||||
|
color: var(--cc-primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#cc-main .cm__footer-links a,
|
||||||
|
#cc-main .cm__link {
|
||||||
|
color: var(--cc-primary-color) !important;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Flex } from '@mantine/core';
|
import { Flex } from '@mantine/core';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useCookieConsent } from '@app/hooks/useCookieConsent';
|
import { useCookieConsent } from '@app/hooks/useCookieConsent';
|
||||||
|
import { useFooterInfo } from '@app/hooks/useFooterInfo';
|
||||||
|
|
||||||
interface FooterProps {
|
interface FooterProps {
|
||||||
privacyPolicy?: string;
|
privacyPolicy?: string;
|
||||||
@@ -9,6 +10,7 @@ interface FooterProps {
|
|||||||
cookiePolicy?: string;
|
cookiePolicy?: string;
|
||||||
impressum?: string;
|
impressum?: string;
|
||||||
analyticsEnabled?: boolean;
|
analyticsEnabled?: boolean;
|
||||||
|
forceLightMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Footer({
|
export default function Footer({
|
||||||
@@ -17,10 +19,36 @@ export default function Footer({
|
|||||||
accessibilityStatement,
|
accessibilityStatement,
|
||||||
cookiePolicy,
|
cookiePolicy,
|
||||||
impressum,
|
impressum,
|
||||||
analyticsEnabled = false
|
analyticsEnabled,
|
||||||
|
forceLightMode = false
|
||||||
}: FooterProps) {
|
}: FooterProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { showCookiePreferences } = useCookieConsent({ analyticsEnabled });
|
const { footerInfo } = useFooterInfo();
|
||||||
|
|
||||||
|
console.log('[Footer] Props analyticsEnabled:', analyticsEnabled);
|
||||||
|
console.log('[Footer] Fetched footerInfo:', footerInfo);
|
||||||
|
|
||||||
|
// Use props if provided, otherwise fall back to fetched footer info
|
||||||
|
const finalAnalyticsEnabled = analyticsEnabled ?? footerInfo?.analyticsEnabled ?? false;
|
||||||
|
const finalPrivacyPolicy = privacyPolicy ?? footerInfo?.privacyPolicy;
|
||||||
|
const finalTermsAndConditions = termsAndConditions ?? footerInfo?.termsAndConditions;
|
||||||
|
const finalAccessibilityStatement = accessibilityStatement ?? footerInfo?.accessibilityStatement;
|
||||||
|
const finalCookiePolicy = cookiePolicy ?? footerInfo?.cookiePolicy;
|
||||||
|
const finalImpressum = impressum ?? footerInfo?.impressum;
|
||||||
|
|
||||||
|
console.log('[Footer] Final analyticsEnabled:', finalAnalyticsEnabled);
|
||||||
|
|
||||||
|
const { showCookiePreferences } = useCookieConsent({ analyticsEnabled: finalAnalyticsEnabled, forceLightMode });
|
||||||
|
|
||||||
|
// Default URLs
|
||||||
|
const defaultTermsUrl = "https://www.stirling.com/legal/terms-of-service";
|
||||||
|
const defaultPrivacyUrl = "https://www.stirling.com/legal/privacy-policy";
|
||||||
|
const defaultAccessibilityUrl = "https://www.stirling.com/accessibility";
|
||||||
|
|
||||||
|
// Use provided URLs or fall back to defaults
|
||||||
|
const finalTermsUrl = finalTermsAndConditions || defaultTermsUrl;
|
||||||
|
const finalPrivacyUrl = finalPrivacyPolicy || defaultPrivacyUrl;
|
||||||
|
const finalAccessibilityUrl = finalAccessibilityStatement || defaultAccessibilityUrl;
|
||||||
|
|
||||||
// Helper to check if a value is valid (not null/undefined/empty string)
|
// Helper to check if a value is valid (not null/undefined/empty string)
|
||||||
const isValidLink = (link?: string) => link && link.trim().length > 0;
|
const isValidLink = (link?: string) => link && link.trim().length > 0;
|
||||||
@@ -28,8 +56,8 @@ export default function Footer({
|
|||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
height: 'var(--footer-height)',
|
height: 'var(--footer-height)',
|
||||||
backgroundColor: 'var(--mantine-color-gray-1)',
|
backgroundColor: forceLightMode ? '#f1f3f5' : 'var(--mantine-color-gray-1)',
|
||||||
borderTop: '1px solid var(--mantine-color-gray-2)',
|
borderTop: forceLightMode ? '1px solid #e9ecef' : '1px solid var(--mantine-color-gray-2)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@@ -38,7 +66,10 @@ export default function Footer({
|
|||||||
justify="center"
|
justify="center"
|
||||||
align="center"
|
align="center"
|
||||||
direction="row"
|
direction="row"
|
||||||
style={{ fontSize: '0.75rem' }}>
|
style={{
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
color: forceLightMode ? '#495057' : undefined
|
||||||
|
}}>
|
||||||
<a
|
<a
|
||||||
className="footer-link px-3"
|
className="footer-link px-3"
|
||||||
id="survey"
|
id="survey"
|
||||||
@@ -48,57 +79,51 @@ export default function Footer({
|
|||||||
>
|
>
|
||||||
{t('survey.nav', 'Survey')}
|
{t('survey.nav', 'Survey')}
|
||||||
</a>
|
</a>
|
||||||
{isValidLink(privacyPolicy) && (
|
<a
|
||||||
|
className="footer-link px-3"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={finalPrivacyUrl}
|
||||||
|
>
|
||||||
|
{t('legal.privacy', 'Privacy Policy')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="footer-link px-3"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={finalTermsUrl}
|
||||||
|
>
|
||||||
|
{t('legal.terms', 'Terms and Conditions')}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
className="footer-link px-3"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href={finalAccessibilityUrl}
|
||||||
|
>
|
||||||
|
{t('legal.accessibility', 'Accessibility')}
|
||||||
|
</a>
|
||||||
|
{isValidLink(finalCookiePolicy) && (
|
||||||
<a
|
<a
|
||||||
className="footer-link px-3"
|
className="footer-link px-3"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href={privacyPolicy}
|
href={finalCookiePolicy}
|
||||||
>
|
|
||||||
{t('legal.privacy', 'Privacy Policy')}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{isValidLink(termsAndConditions) && (
|
|
||||||
<a
|
|
||||||
className="footer-link px-3"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href={termsAndConditions}
|
|
||||||
>
|
|
||||||
{t('legal.terms', 'Terms and Conditions')}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{isValidLink(accessibilityStatement) && (
|
|
||||||
<a
|
|
||||||
className="footer-link px-3"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href={accessibilityStatement}
|
|
||||||
>
|
|
||||||
{t('legal.accessibility', 'Accessibility')}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{isValidLink(cookiePolicy) && (
|
|
||||||
<a
|
|
||||||
className="footer-link px-3"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
href={cookiePolicy}
|
|
||||||
>
|
>
|
||||||
{t('legal.cookie', 'Cookie Policy')}
|
{t('legal.cookie', 'Cookie Policy')}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{isValidLink(impressum) && (
|
{isValidLink(finalImpressum) && (
|
||||||
<a
|
<a
|
||||||
className="footer-link px-3"
|
className="footer-link px-3"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href={impressum}
|
href={finalImpressum}
|
||||||
>
|
>
|
||||||
{t('legal.impressum', 'Impressum')}
|
{t('legal.impressum', 'Impressum')}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{analyticsEnabled && (
|
{finalAnalyticsEnabled && (
|
||||||
<button
|
<button
|
||||||
className="footer-link px-3"
|
className="footer-link px-3"
|
||||||
id="cookieBanner"
|
id="cookieBanner"
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ declare global {
|
|||||||
|
|
||||||
interface CookieConsentConfig {
|
interface CookieConsentConfig {
|
||||||
analyticsEnabled?: boolean;
|
analyticsEnabled?: boolean;
|
||||||
|
forceLightMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useCookieConsent = ({
|
export const useCookieConsent = ({
|
||||||
analyticsEnabled = false
|
analyticsEnabled = false,
|
||||||
|
forceLightMode = false
|
||||||
}: CookieConsentConfig = {}) => {
|
}: CookieConsentConfig = {}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { config } = useAppConfig();
|
const { config } = useAppConfig();
|
||||||
@@ -69,6 +71,12 @@ export const useCookieConsent = ({
|
|||||||
|
|
||||||
// Detect current theme and set appropriate mode
|
// Detect current theme and set appropriate mode
|
||||||
const detectTheme = () => {
|
const detectTheme = () => {
|
||||||
|
// If forceLightMode is enabled, always use light mode
|
||||||
|
if (forceLightMode) {
|
||||||
|
document.documentElement.classList.remove('cc--darkmode');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const mantineScheme = document.documentElement.getAttribute('data-mantine-color-scheme');
|
const mantineScheme = document.documentElement.getAttribute('data-mantine-color-scheme');
|
||||||
const hasLightClass = document.documentElement.classList.contains('light');
|
const hasLightClass = document.documentElement.classList.contains('light');
|
||||||
const hasDarkClass = document.documentElement.classList.contains('dark');
|
const hasDarkClass = document.documentElement.classList.contains('dark');
|
||||||
@@ -104,21 +112,24 @@ export const useCookieConsent = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for theme changes
|
// Listen for theme changes (but not if forceLightMode is enabled)
|
||||||
const themeObserver = new MutationObserver((mutations) => {
|
let themeObserver: MutationObserver | null = null;
|
||||||
mutations.forEach((mutation) => {
|
if (!forceLightMode) {
|
||||||
if (mutation.type === 'attributes' &&
|
themeObserver = new MutationObserver((mutations) => {
|
||||||
(mutation.attributeName === 'data-mantine-color-scheme' ||
|
mutations.forEach((mutation) => {
|
||||||
mutation.attributeName === 'class')) {
|
if (mutation.type === 'attributes' &&
|
||||||
detectTheme();
|
(mutation.attributeName === 'data-mantine-color-scheme' ||
|
||||||
}
|
mutation.attributeName === 'class')) {
|
||||||
|
detectTheme();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
themeObserver.observe(document.documentElement, {
|
themeObserver.observe(document.documentElement, {
|
||||||
attributes: true,
|
attributes: true,
|
||||||
attributeFilter: ['data-mantine-color-scheme', 'class']
|
attributeFilter: ['data-mantine-color-scheme', 'class']
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Initialize cookie consent with full configuration
|
// Initialize cookie consent with full configuration
|
||||||
@@ -245,6 +256,65 @@ export const useCookieConsent = ({
|
|||||||
};
|
};
|
||||||
}, [analyticsEnabled, config?.enablePosthog, config?.enableScarf, t]);
|
}, [analyticsEnabled, config?.enablePosthog, config?.enableScarf, t]);
|
||||||
|
|
||||||
|
// Update theme when forceLightMode changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInitialized) return;
|
||||||
|
|
||||||
|
const detectTheme = () => {
|
||||||
|
if (forceLightMode) {
|
||||||
|
document.documentElement.classList.remove('cc--darkmode');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mantineScheme = document.documentElement.getAttribute('data-mantine-color-scheme');
|
||||||
|
const hasLightClass = document.documentElement.classList.contains('light');
|
||||||
|
const hasDarkClass = document.documentElement.classList.contains('dark');
|
||||||
|
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
let isDarkMode = false;
|
||||||
|
if (mantineScheme) {
|
||||||
|
isDarkMode = mantineScheme === 'dark';
|
||||||
|
} else if (hasLightClass) {
|
||||||
|
isDarkMode = false;
|
||||||
|
} else if (hasDarkClass) {
|
||||||
|
isDarkMode = true;
|
||||||
|
} else {
|
||||||
|
isDarkMode = systemPrefersDark;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.classList.toggle('cc--darkmode', isDarkMode);
|
||||||
|
return isDarkMode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update theme immediately
|
||||||
|
detectTheme();
|
||||||
|
|
||||||
|
// Set up or remove theme observer based on forceLightMode
|
||||||
|
let themeObserver: MutationObserver | null = null;
|
||||||
|
if (!forceLightMode) {
|
||||||
|
themeObserver = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'attributes' &&
|
||||||
|
(mutation.attributeName === 'data-mantine-color-scheme' ||
|
||||||
|
mutation.attributeName === 'class')) {
|
||||||
|
detectTheme();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
themeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-mantine-color-scheme', 'class']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (themeObserver) {
|
||||||
|
themeObserver.disconnect();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [forceLightMode, isInitialized]);
|
||||||
|
|
||||||
// Hide cookie banner when tour is active
|
// Hide cookie banner when tour is active
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isInitialized || !window.CookieConsent) {
|
if (!isInitialized || !window.CookieConsent) {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import apiClient from '@app/services/apiClient';
|
||||||
|
|
||||||
|
export interface FooterInfo {
|
||||||
|
analyticsEnabled?: boolean;
|
||||||
|
termsAndConditions?: string;
|
||||||
|
privacyPolicy?: string;
|
||||||
|
accessibilityStatement?: string;
|
||||||
|
cookiePolicy?: string;
|
||||||
|
impressum?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch public footer configuration data.
|
||||||
|
* This endpoint is always accessible without authentication.
|
||||||
|
*/
|
||||||
|
export function useFooterInfo() {
|
||||||
|
const [footerInfo, setFooterInfo] = useState<FooterInfo | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchFooterInfo = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
console.log('[useFooterInfo] Fetching footer info from /api/v1/ui-data/footer-info...');
|
||||||
|
const response = await apiClient.get<FooterInfo>('/api/v1/ui-data/footer-info', {
|
||||||
|
suppressErrorToast: true,
|
||||||
|
} as any);
|
||||||
|
console.log('[useFooterInfo] Footer info received:', response.data);
|
||||||
|
setFooterInfo(response.data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[useFooterInfo] Failed to fetch footer info:', err);
|
||||||
|
setError(err as Error);
|
||||||
|
// Set defaults on error
|
||||||
|
setFooterInfo({
|
||||||
|
analyticsEnabled: false,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchFooterInfo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { footerInfo, loading, error };
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import Login from '@app/routes/Login';
|
|||||||
import { useAuth } from '@app/auth/UseSession';
|
import { useAuth } from '@app/auth/UseSession';
|
||||||
import { springAuth } from '@app/auth/springAuthClient';
|
import { springAuth } from '@app/auth/springAuthClient';
|
||||||
import { PreferencesProvider } from '@app/contexts/PreferencesContext';
|
import { PreferencesProvider } from '@app/contexts/PreferencesContext';
|
||||||
|
import { OnboardingProvider } from '@app/contexts/OnboardingContext';
|
||||||
|
|
||||||
// Mock i18n to return fallback text
|
// Mock i18n to return fallback text
|
||||||
vi.mock('react-i18next', () => ({
|
vi.mock('react-i18next', () => ({
|
||||||
@@ -65,7 +66,9 @@ vi.mock('react-router-dom', async () => {
|
|||||||
// Test wrapper with MantineProvider
|
// Test wrapper with MantineProvider
|
||||||
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
const TestWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
<PreferencesProvider>{children}</PreferencesProvider>
|
<PreferencesProvider>
|
||||||
|
<OnboardingProvider>{children}</OnboardingProvider>
|
||||||
|
</PreferencesProvider>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import LoginRightCarousel from '@app/components/shared/LoginRightCarousel';
|
|||||||
import buildLoginSlides from '@app/components/shared/loginSlides';
|
import buildLoginSlides from '@app/components/shared/loginSlides';
|
||||||
import styles from '@app/routes/authShared/AuthLayout.module.css';
|
import styles from '@app/routes/authShared/AuthLayout.module.css';
|
||||||
import { useLogoVariant } from '@app/hooks/useLogoVariant';
|
import { useLogoVariant } from '@app/hooks/useLogoVariant';
|
||||||
|
import Footer from '@app/components/shared/Footer';
|
||||||
|
|
||||||
interface AuthLayoutProps {
|
interface AuthLayoutProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@@ -67,6 +68,9 @@ export default function AuthLayout({ children }: AuthLayoutProps) {
|
|||||||
<LoginRightCarousel imageSlides={imageSlides} initialSeconds={5} slideSeconds={8} />
|
<LoginRightCarousel imageSlides={imageSlides} initialSeconds={5} slideSeconds={8} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ position: 'fixed', bottom: 0, left: 0, right: 0, width: '100%', zIndex: 10 }}>
|
||||||
|
<Footer forceLightMode={true} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user