From 81e1830563a28a1d5ce3f4ac4cf0472e82f2b31a Mon Sep 17 00:00:00 2001 From: Nurfog Date: Mon, 2 Mar 2026 11:29:55 -0300 Subject: [PATCH] feat: Implement dark mode styling across UI components and update README with roadmap and system requirements. --- README.md | 42 +- services/cms-service/src/handlers.rs | 107 +++++- services/cms-service/src/main.rs | 1 + web/experience/src/app/page.tsx | 14 +- web/experience/src/components/Leaderboard.tsx | 18 +- web/studio/src/app/admin/layout.tsx | 18 +- .../src/app/admin/organizations/page.tsx | 358 ++++++++++-------- web/studio/src/app/admin/page.tsx | 64 ++-- web/studio/src/app/admin/users/page.tsx | 216 ++++++----- web/studio/src/app/globals.css | 6 +- web/studio/src/app/library/assets/page.tsx | 6 +- web/studio/src/app/page.tsx | 8 +- .../src/components/BrandingSettings.tsx | 64 ++-- .../src/components/CourseEditorLayout.tsx | 6 +- web/studio/src/lib/api.ts | 1 + 15 files changed, 555 insertions(+), 374 deletions(-) diff --git a/README.md b/README.md index 98d5482..f5836e3 100644 --- a/README.md +++ b/README.md @@ -58,13 +58,13 @@ El proyecto ha sido optimizado para reducir la complejidad de la infraestructura OpenCCB es altamente escalable. A continuación se detallan los requisitos recomendados según la carga de usuarios concurrentes: -| Componente | **Pequeño (100 u.)** | **Mediano (500 u.)** | **Grande (1000+ u.)** | +| Componente | **Pequeño (100 u.)** | **Mediano (500 u. concurrentes)** | **Grande (1000+ u.)** | | :--- | :--- | :--- | :--- | -| **CPU** | 4 vCPUs | 8 vCPUs (AVX2) | 16+ vCPUs | -| **RAM** | 8 GB | 16 GB | 32 GB+ | -| **Almacenamiento** | 50 GB SSD | 200 GB NVMe | 500 GB+ NVMe | -| **AI (Opcional)** | N/A (Solo CPU) | NVIDIA 8GB+ VRAM | Multi-GPU / Cloud API | -| **OS** | Ubuntu 22.04 LTS | Ubuntu 22.04+ | Cloud Managed (K8s) | +| **CPU** | 4 vCPUs | 8-12 vCPUs (AVX2/AVX-512) | 16-32+ vCPUs | +| **RAM** | 8 GB | 16-32 GB (Recomendado 24GB+) | 64 GB+ | +| **Almacenamiento** | 50 GB SSD | 250 GB+ NVMe (RAID-1) | 1 TB+ NVMe (S3 Backup) | +| **AI (Opcional)** | N/A (Solo CPU) | NVIDIA RTX 3060+ (12GB VRAM) | Multi-GPU (A100/H100) | +| **OS** | Ubuntu 22.04 LTS | Ubuntu 24.04 LTS / Debian | Cloud Native (K8s / Terraform) | > [!NOTE] > Los requisitos de AI son específicos para la función de transcripción local (Whisper). Si se utiliza una API externa, el requisito de GPU desaparece. @@ -645,5 +645,33 @@ Obtiene una lista de todas las organizaciones registradas. - **Global Asset Manager**: Interfaz avanzada para la administración masiva de archivos con previsualización inteligente y filtros por curso o tipo. - **Predictive Risk Dashboard**: Panel de control para instructores que visualiza el riesgo de deserción escolar mediante semáforos de color y motivos detallados del riesgo. -## 📄 Licencia +## �️ Próximos Pasos (Roadmap 2024-2025) + +OpenCCB evoluciona constantemente. Estos son los pilares de nuestro desarrollo futuro: + +### 📱 Movilidad Nativa +- **Apps Android/iOS**: Aplicaciones nativas desarrolladas con Flutter para aprendizaje offline y notificaciones push críticas. +- **Offline Sync**: Capacidad de descargar lecciones y sincronizar progreso al recuperar conexión. + +### 🧠 Inteligencia Artificial Avanzada +- **AI Proctoring**: Monitoreo basado en visión artificial para exámenes de alta integridad, 100% privado y local. +- **Multimodal Tutoring**: El tutor de IA podrá analizar imágenes y videos subidos por el alumno para dar feedback. +- **Automated Grading for Open Questions**: Evaluación masiva de ensayos y respuestas abiertas con rúbricas personalizadas. + +### 🔌 Interoperabilidad y Estándares +- **SCORM 1.2 / 2004 Support**: Player nativo para contenidos legados de la industria. +- **Advanced xAPI (Tin Can)**: Recolección detallada de experiencias de aprendizaje granulares. +- **Microsoft Teams / Slack Integration**: Recibe anuncios y tareas directamente en tus herramientas de trabajo. + +### 🏗️ Infraestructura y Escalabilidad +- **Multi-Cloud Terraform Provider**: Despliegues automatizados en AWS, GCP y Azure. +- **Edge Content Delivery**: Caché de videos y assets en el borde para mínima latencia global. + +### 🎮 Gamificación y Comunidad +- **Real-time Leaderboards**: Tablas de clasificación en vivo por cohorte y organización. +- **Social Learning Groups**: Grupos de estudio auto-organizados con chats integrados. + +--- + +## �📄 Licencia Este proyecto es código abierto y está disponible bajo los términos de la licencia especificada en el repositorio. \ No newline at end of file diff --git a/services/cms-service/src/handlers.rs b/services/cms-service/src/handlers.rs index cf71ea7..e0c6353 100644 --- a/services/cms-service/src/handlers.rs +++ b/services/cms-service/src/handlers.rs @@ -1721,6 +1721,7 @@ pub struct AdminCreateUserPayload { pub password: String, pub full_name: String, pub role: String, + pub organization_id: Option, } pub async fn register( @@ -1809,6 +1810,15 @@ pub async fn admin_create_user( let password_hash = hash(payload.password, DEFAULT_COST) .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Hashing failed".into()))?; + let is_super_admin = claims.role == "admin" + && claims.org == Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + + let target_org_id = if is_super_admin { + payload.organization_id.unwrap_or(org_ctx.id) + } else { + org_ctx.id + }; + let user = sqlx::query_as::<_, User>( "INSERT INTO users (email, password_hash, full_name, role, organization_id) VALUES ($1, $2, $3, $4, $5) RETURNING *" ) @@ -1816,7 +1826,7 @@ pub async fn admin_create_user( .bind(password_hash) .bind(&payload.full_name) .bind(&payload.role) - .bind(org_ctx.id) + .bind(target_org_id) .fetch_one(&pool) .await .map_err(|e| { @@ -2662,20 +2672,51 @@ pub async fn update_user( .get("organization_id") .and_then(|o| o.as_str()) .and_then(|o| Uuid::parse_str(o).ok()); + let is_super_admin = claims.role == "admin" + && claims.org == Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); - let user = sqlx::query_as::<_, User>( - "UPDATE users SET role = COALESCE($1, role), organization_id = COALESCE($2, organization_id), full_name = COALESCE($3, full_name), avatar_url = COALESCE($4, avatar_url), bio = COALESCE($5, bio), language = COALESCE($6, language) WHERE id = $7 AND organization_id = $8 RETURNING *" - ) - .bind(role) - .bind(organization_id) - .bind(full_name) - .bind(avatar_url) - .bind(bio) - .bind(language) - .bind(id) - .bind(org_ctx.id) - .fetch_one(&pool) - .await + let user = if is_super_admin { + sqlx::query_as::<_, User>( + "UPDATE users SET + role = COALESCE($1, role), + organization_id = COALESCE($2, organization_id), + full_name = COALESCE($3, full_name), + avatar_url = COALESCE($4, avatar_url), + bio = COALESCE($5, bio), + language = COALESCE($6, language) + WHERE id = $7 RETURNING *" + ) + .bind(role) + .bind(organization_id) + .bind(full_name) + .bind(avatar_url) + .bind(bio) + .bind(language) + .bind(id) + .fetch_one(&pool) + .await + } else { + sqlx::query_as::<_, User>( + "UPDATE users SET + role = COALESCE($1, role), + organization_id = COALESCE($2, organization_id), + full_name = COALESCE($3, full_name), + avatar_url = COALESCE($4, avatar_url), + bio = COALESCE($5, bio), + language = COALESCE($6, language) + WHERE id = $7 AND organization_id = $8 RETURNING *" + ) + .bind(role) + .bind(organization_id) + .bind(full_name) + .bind(avatar_url) + .bind(bio) + .bind(language) + .bind(id) + .bind(org_ctx.id) + .fetch_one(&pool) + .await + } .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; log_action( @@ -2790,6 +2831,44 @@ pub async fn create_organization( Ok(Json(org)) } +pub async fn update_organization( + claims: common::auth::Claims, + State(pool): State, + Path(id): Path, + Json(payload): Json, +) -> Result, (StatusCode, String)> { + // Only super admins or admins of the same org? + // Usually editing other orgs is a Super Admin only task. + let is_super_admin = claims.role == "admin" + && claims.org == Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap(); + + if !is_super_admin { + return Err((StatusCode::FORBIDDEN, "Super Admin access required".into())); + } + + let name = payload.get("name").and_then(|v| v.as_str()); + let domain = payload.get("domain").and_then(|v| v.as_str()); + + let org = sqlx::query_as::<_, Organization>( + "UPDATE organizations SET + name = COALESCE($1, name), + domain = COALESCE($2, domain), + updated_at = NOW() + WHERE id = $3 RETURNING *" + ) + .bind(name) + .bind(domain) + .bind(id) + .fetch_one(&pool) + .await + .map_err(|e| { + tracing::error!("Failed to update organization: {}", e); + (StatusCode::INTERNAL_SERVER_ERROR, "Failed to update organization".into()) + })?; + + Ok(Json(org)) +} + pub async fn provision_organization( claims: common::auth::Claims, State(pool): State, diff --git a/services/cms-service/src/main.rs b/services/cms-service/src/main.rs index a46e855..50897f1 100644 --- a/services/cms-service/src/main.rs +++ b/services/cms-service/src/main.rs @@ -171,6 +171,7 @@ async fn main() { "/organizations", get(handlers::get_organizations).post(handlers::create_organization), ) + .route("/organizations/{id}", put(handlers::update_organization)) .route("/admin/provision", post(handlers::provision_organization)) .route( "/webhooks", diff --git a/web/experience/src/app/page.tsx b/web/experience/src/app/page.tsx index c7f3c25..8fe837e 100644 --- a/web/experience/src/app/page.tsx +++ b/web/experience/src/app/page.tsx @@ -126,7 +126,7 @@ export default function CatalogPage() {
-
+
{gamification.level}
@@ -134,19 +134,19 @@ export default function CatalogPage() {
Posición Actual
-

Nivel {gamification.level} Pionero

+

Nivel {gamification.level} Pionero

-
+
{gamification.points} / {Math.pow(gamification.level, 2) * 100} XP
-
+
{Math.floor(((gamification.points - Math.pow(gamification.level - 1, 2) * 100) / (Math.pow(gamification.level, 2) * 100 - Math.pow(gamification.level - 1, 2) * 100)) * 100)}% para el Nivel {gamification.level + 1}
-
+
+
@@ -246,7 +246,7 @@ export default function CatalogPage() {

-
+
{isEnrolled ? ( Continuar Aprendiendo diff --git a/web/experience/src/components/Leaderboard.tsx b/web/experience/src/components/Leaderboard.tsx index 70e38ec..4453096 100644 --- a/web/experience/src/components/Leaderboard.tsx +++ b/web/experience/src/components/Leaderboard.tsx @@ -31,7 +31,7 @@ export default function Leaderboard() { } return ( -
+

@@ -41,24 +41,24 @@ export default function Leaderboard() {
  • -
    +
    {index === 0 ?
    -
    {user.full_name}
    -
    Nivel {user.level || 1}
    +
    {user.full_name}
    +
    Nivel {user.level || 1}
    -
    {user.xp || 0}
    -
    XP
    +
    {user.xp || 0}
    +
    XP
  • ))} diff --git a/web/studio/src/app/admin/layout.tsx b/web/studio/src/app/admin/layout.tsx index 4fac686..8bfc601 100644 --- a/web/studio/src/app/admin/layout.tsx +++ b/web/studio/src/app/admin/layout.tsx @@ -25,16 +25,16 @@ export default function AdminLayout({ children }: { children: React.ReactNode }) ]; return ( -
    +
    {/* Sidebar */} -

    Access Denied

    -

    Only system administrators can access this page.

    +

    Only system administrators can access this page.

    ); } @@ -179,11 +204,11 @@ export default function OrganizationsPage() {
    -

    Organizations

    -

    Manage tenants and isolated environments.

    +

    Organizations

    +

    Manage tenants and isolated environments.

    +
    +
    - {org.domain || 'No custom domain'} + {org.domain || 'No custom domain'}
    -
    -
    +
    +
    -
    +
    - Created: {new Date(org.created_at).toLocaleDateString()} + {new Date(org.created_at).toLocaleDateString()}
    -
    +
    {org.id.split('-')[0]}...
    -
    @@ -263,85 +297,99 @@ export default function OrganizationsPage() {
    )} - {/* Create Organization Modal */} + {/* Create/Edit Organization Modal */} {isModalOpen && ( -
    -
    -

    Create New Organization

    +
    +
    +
    +

    + {isEditing ? 'Edit Organization' : 'Create New Organization'} +

    + +
    +
    - + setNewName(e.target.value)} - className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400" placeholder="e.g. Acme Corp" />
    - - setNewDomain(e.target.value)} - className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm" - placeholder="e.g. acme.com" - /> -
    - -
    -

    Initial Administrator

    -
    -
    - - setAdminFullName(e.target.value)} - className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" - placeholder="e.g. John Doe" - /> -
    -
    - - setAdminEmail(e.target.value)} - className="w-full bg-black/5 dark:bg-black/40 border border-black/10 dark:border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-gray-900 dark:text-white" - placeholder="admin@acme.com" - /> -
    -
    - - setAdminPassword(e.target.value)} - className="w-full bg-black/40 border border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" - placeholder="••••••••" - /> -
    + +
    + + setNewDomain(e.target.value)} + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm text-slate-900 dark:text-white placeholder-slate-400" + placeholder="e.g. acme.com" + />
    + + {!isEditing && ( +
    +

    Initial Administrator

    +
    +
    + + setAdminFullName(e.target.value)} + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400" + placeholder="e.g. John Doe" + /> +
    +
    + + setAdminEmail(e.target.value)} + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400" + placeholder="admin@acme.com" + /> +
    +
    + + setAdminPassword(e.target.value)} + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400" + placeholder="••••••••" + /> +
    +
    +
    + )} +
    @@ -351,14 +399,14 @@ export default function OrganizationsPage() { {/* Branding Management Modal */} {isBrandingModalOpen && selectedOrg && ( -
    -
    +
    +
    -

    Branding Management

    -

    {selectedOrg.name}

    +

    Branding Management

    +

    {selectedOrg.name}

    -
    @@ -367,22 +415,22 @@ export default function OrganizationsPage() {
    {/* Logo Upload */}
    - +
    -
    +
    {selectedOrg.logo_url ? ( Preview ) : ( - + )}
    -
    @@ -390,36 +438,36 @@ export default function OrganizationsPage() { {/* Colors */}
    - +
    setPrimaryColor(e.target.value)} - className="w-10 h-10 rounded cursor-pointer bg-transparent border-none" + className="w-10 h-10 rounded cursor-pointer border border-slate-200 dark:border-white/10 p-1 bg-white dark:bg-black/40" /> setPrimaryColor(e.target.value)} - className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-sm font-mono" + className="flex-1 bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg px-3 py-2 text-sm font-mono text-slate-900 dark:text-white" />
    - +
    setSecondaryColor(e.target.value)} - className="w-10 h-10 rounded cursor-pointer bg-transparent border-none" + className="w-10 h-10 rounded cursor-pointer border border-slate-200 dark:border-white/10 p-1 bg-white dark:bg-black/40" /> setSecondaryColor(e.target.value)} - className="flex-1 bg-black/40 border border-white/10 rounded-lg px-3 py-2 text-sm font-mono" + className="flex-1 bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg px-3 py-2 text-sm font-mono text-slate-900 dark:text-white" />
    @@ -428,10 +476,10 @@ export default function OrganizationsPage() { {/* Live Preview */}
    - -
    + +
    {/* Mock Experience Header */} -
    +
    {selectedOrg.logo_url ? ( @@ -446,24 +494,24 @@ export default function OrganizationsPage() {
    {/* Mock Experience Content */} -
    -
    -
    +
    +
    +
    -
    -
    -
    +
    +
    +
    -
    +
    GET STARTED
    -
    -

    - This is a real-time preview of how the brand identity will apply to the student's learning experience. +

    +

    + Real-time preview of the brand application.

    @@ -472,14 +520,14 @@ export default function OrganizationsPage() {
    -
    -
    -

    Enable OIDC SSO

    -

    Allow users to log in via your identity provider.

    +
    +
    +

    Enable OIDC SSO

    +

    Allow users to log in via your identity provider.

    @@ -523,55 +571,55 @@ export default function OrganizationsPage() {
    - +
    - + setIssuerUrl(e.target.value)} - className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm" - placeholder="https://accounts.google.com or https://okta.com/..." + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm text-slate-900 dark:text-white placeholder-slate-400" + placeholder="https://accounts.google.com" />
    - +
    - + setClientId(e.target.value)} - className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm" + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm text-slate-900 dark:text-white placeholder-slate-400" placeholder="Your OIDC Client ID" />
    - +
    - + setClientSecret(e.target.value)} - className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm" + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm text-slate-900 dark:text-white placeholder-slate-400" placeholder="••••••••••••••••" />
    -
    -
    +
    +
    CONFIGURATION STEPS
    -

    - 1. Register OpenCCB as an application in your Identity Provider (Okta, Google, Azure AD).
    - 2. Set the Redirect URI to: {API_BASE_URL}/auth/sso/callback
    - 3. Copy the Issuer URL, Client ID, and Client Secret here. +

    + 1. Register OpenCCB in your IDP.
    + 2. Redirect URI: {API_BASE_URL}/auth/sso/callback
    + 3. Copy the Issuer, ID and Secret here.

    @@ -580,14 +628,14 @@ export default function OrganizationsPage() {
    - + setSearchTerm(e.target.value)} - className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/50" + className="w-full bg-white dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white shadow-sm dark:shadow-none" />
    - + handleUpdateUser(u.id, e.target.value, u.organization_id || '00000000-0000-0000-0000-000000000000')} - className="bg-black/20 border border-white/5 rounded px-2 py-1 text-xs focus:ring-1 focus:ring-blue-500/50" - > - - - - - - - - - - - - - ))} - - + + + + + +
    + + +
    + + + + + + ))} + + +
    {!loading && filteredUsers.length === 0 && ( -
    +
    No users found matching your search.
    )} @@ -201,72 +207,72 @@ export default function UsersPage() { {/* Create User Modal */} {isModalOpen && ( -
    -
    +
    +
    -
    +
    -

    Add New User

    +

    Add New User

    -
    - +
    - + setNewUser({ ...newUser, full_name: e.target.value })} - className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400" placeholder="e.g. John Doe" />
    - +
    - + setNewUser({ ...newUser, email: e.target.value })} - className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm" + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-mono text-sm text-slate-900 dark:text-white placeholder-slate-400" placeholder="user@example.com" />
    - +
    - + setNewUser({ ...newUser, password: e.target.value })} - className="w-full bg-black/40 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-lg pl-10 pr-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white placeholder-slate-400" placeholder="••••••••" />
    - +
    +
    + + +
    +
    diff --git a/web/studio/src/app/globals.css b/web/studio/src/app/globals.css index 6c2429a..f964f43 100644 --- a/web/studio/src/app/globals.css +++ b/web/studio/src/app/globals.css @@ -11,7 +11,7 @@ --accent-secondary: #6366f1; --glass-bg: rgba(255, 255, 255, 0.8); - --glass-border: rgba(0, 0, 0, 0.1); + --glass-border: rgba(0, 0, 0, 0.15); --glass-blur: blur(16px); } @@ -47,6 +47,10 @@ body { @apply glass rounded-2xl p-4 md:p-6; } +.nav-link-standard { + @apply text-xs font-bold uppercase tracking-widest transition-colors flex items-center gap-2; +} + .btn-premium { @apply relative px-6 py-2.5 rounded-xl font-bold transition-all duration-300; background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); diff --git a/web/studio/src/app/library/assets/page.tsx b/web/studio/src/app/library/assets/page.tsx index 01287d5..e7b2380 100644 --- a/web/studio/src/app/library/assets/page.tsx +++ b/web/studio/src/app/library/assets/page.tsx @@ -141,7 +141,7 @@ export default function AssetLibraryPage() { placeholder="Search by filename..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - className="w-full bg-black/5 dark:bg-white/5 border border-black/10 dark:border-white/10 rounded-2xl pl-12 pr-4 py-4 text-sm font-medium focus:outline-none focus:border-blue-500/50 focus:bg-black/10 dark:focus:bg-white/10 transition-all text-gray-900 dark:text-white" + className="w-full bg-slate-50 dark:bg-black/40 border border-slate-200 dark:border-white/10 rounded-2xl pl-12 pr-4 py-4 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all text-slate-900 dark:text-white" />
    @@ -150,7 +150,7 @@ export default function AssetLibraryPage() { @@ -183,7 +183,7 @@ export default function StudioDashboard() {
    -
    +
    Última actualización: {new Date(course.updated_at).toLocaleDateString()} ID: {course.id.slice(0, 4)}...
    diff --git a/web/studio/src/components/BrandingSettings.tsx b/web/studio/src/components/BrandingSettings.tsx index 0887006..00460aa 100644 --- a/web/studio/src/components/BrandingSettings.tsx +++ b/web/studio/src/components/BrandingSettings.tsx @@ -61,45 +61,45 @@ export default function BrandingSettings() { if (!org) return
    Failed to load organization settings.
    ; return ( -
    -
    - +
    +
    + Brand Identity
    {/* Organization Name */}
    - + setFormData({ ...formData, name: e.target.value })} - className="w-full bg-black/20 border border-white/10 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500/50 transition-colors" + className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-lg px-4 py-3 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" placeholder="My Organization" required /> -

    The official name of your organization.

    +

    The official name of your organization.

    {/* Platform Name */}
    - + setFormData({ ...formData, platform_name: e.target.value })} placeholder={org.name} - className="w-full bg-black/20 border border-white/10 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-blue-500/50 transition-colors" + className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-lg px-4 py-3 text-slate-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all" /> -

    Appears in the browser tab and page titles.

    +

    Appears in the browser tab and page titles.

    {/* Logo Section */}
    - Logo -
    + Logo +
    {org.logo_url ? ( ) : ( - No logo uploaded + No logo uploaded )}
    - Favicon -
    + Favicon +
    {org.favicon_url ? (
    ) : ( - No favicon + No favicon )}
    {/* Logo Variant Selection */} -
    - +
    +
    -
    - +
    + Brand Colors
    {/* Primary Color */}
    - +
    setFormData({ ...formData, primary_color: e.target.value })} - className="w-full bg-black/20 border border-white/10 rounded-lg px-4 py-2 text-white font-mono uppercase" + className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-lg px-4 py-2 text-slate-900 dark:text-white font-mono uppercase focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-bold" aria-describedby="primary-color-desc" />
    -

    Used for main buttons, active states, and highlights.

    +

    Used for main buttons, active states, and highlights.

    {/* Secondary Color */}
    - +
    setFormData({ ...formData, secondary_color: e.target.value })} - className="w-full bg-black/20 border border-white/10 rounded-lg px-4 py-2 text-white font-mono uppercase" + className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-lg px-4 py-2 text-slate-900 dark:text-white font-mono uppercase focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all font-bold" aria-describedby="secondary-color-desc" />
    -

    Used for accents and gradients.

    +

    Used for accents and gradients.

    diff --git a/web/studio/src/components/CourseEditorLayout.tsx b/web/studio/src/components/CourseEditorLayout.tsx index 4e55b13..46f3348 100644 --- a/web/studio/src/components/CourseEditorLayout.tsx +++ b/web/studio/src/components/CourseEditorLayout.tsx @@ -42,9 +42,9 @@ export default function CourseEditorLayout({ children, activeTab }: CourseEditor