feat: Implement organization-based SSO login with an AsyncCombobox and add logo variant branding options.

This commit is contained in:
2026-02-26 11:50:34 -03:00
parent 824da230a4
commit 947abcb0bc
24 changed files with 823 additions and 143 deletions
+36 -31
View File
@@ -2,6 +2,7 @@
import React, { createContext, useContext, useEffect, useState } from 'react';
import { lmsApi, Organization, getImageUrl } from '@/lib/api';
import { usePathname } from 'next/navigation';
interface BrandingContextType {
branding: Organization | null;
@@ -21,42 +22,14 @@ export const BrandingProvider: React.FC<{ children: React.ReactNode }> = ({ chil
const orgId = process.env.NEXT_PUBLIC_ORG_ID || '00000000-0000-0000-0000-000000000001';
const pathname = usePathname();
useEffect(() => {
const loadBranding = async () => {
try {
const data = await lmsApi.getBranding(orgId);
setBranding(data);
// Apply CSS variables
if (data.primary_color) {
document.documentElement.style.setProperty('--primary-color', data.primary_color);
}
if (data.secondary_color) {
document.documentElement.style.setProperty('--secondary-color', data.secondary_color);
}
// Update Title
if (data.platform_name) {
document.title = `${data.platform_name} | Experiencia de Aprendizaje`;
}
// Update Favicon
if (data.favicon_url) {
// Import getImageUrl logic locally or assume it needs import
// Since I can't easily add import at top with replace_file, I will assume getImageUrl handles the path or do logic here.
// Actually I need to import getImageUrl at the top. Instead of complicating, I'll update imports too.
const faviconUrl = getImageUrl(data.favicon_url);
const link: HTMLLinkElement | null = document.querySelector("link[rel*='icon']");
if (link) {
link.href = faviconUrl;
} else {
const newLink = document.createElement("link");
newLink.rel = "shortcut icon";
newLink.href = faviconUrl;
document.head.appendChild(newLink);
}
}
console.log('Branding loaded in Experience:', data);
} catch (error) {
console.error('Failed to load branding', error);
} finally {
@@ -67,6 +40,38 @@ export const BrandingProvider: React.FC<{ children: React.ReactNode }> = ({ chil
loadBranding();
}, [orgId]);
useEffect(() => {
if (!branding) return;
console.log('Applying branding in Experience for path:', pathname);
// Apply CSS variables
if (branding.primary_color) {
document.documentElement.style.setProperty('--primary-color', branding.primary_color);
}
if (branding.secondary_color) {
document.documentElement.style.setProperty('--secondary-color', branding.secondary_color);
}
// Update Title
if (branding.platform_name) {
document.title = `${branding.platform_name} | Experiencia de Aprendizaje`;
}
// Update Favicon
if (branding.favicon_url) {
const faviconUrl = getImageUrl(branding.favicon_url);
const link: HTMLLinkElement | null = document.querySelector("link[rel*='icon']");
if (link) {
link.href = faviconUrl;
} else {
const newLink = document.createElement("link");
newLink.rel = "shortcut icon";
newLink.href = faviconUrl;
document.head.appendChild(newLink);
}
}
}, [branding, pathname]);
return (
<BrandingContext.Provider value={{ branding, loading }}>
{children}
@@ -0,0 +1,55 @@
'use client';
import React, { createContext, useContext, useEffect, useState } from 'react';
type Theme = 'dark' | 'light';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType>({
theme: 'dark',
toggleTheme: () => { },
setTheme: () => { },
});
export const useTheme = () => useContext(ThemeContext);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setThemeState] = useState<Theme>('dark');
const [mounted, setMounted] = useState(false);
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) {
setThemeState(savedTheme);
}
setMounted(true);
}, []);
useEffect(() => {
if (!mounted) return;
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme, mounted]);
const toggleTheme = () => {
setThemeState(prev => (prev === 'dark' ? 'light' : 'dark'));
};
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};