feat: ad interface to upload logo and favicon

This commit is contained in:
2026-01-23 11:43:17 -03:00
parent 3ae67b23c9
commit 60e2af72f0
9 changed files with 346 additions and 38 deletions
@@ -13,6 +13,11 @@ export default function OrganizationsPage() {
const [newName, setNewName] = useState('');
const [newDomain, setNewDomain] = useState('');
// Admin User States
const [adminFullName, setAdminFullName] = useState('');
const [adminEmail, setAdminEmail] = useState('');
const [adminPassword, setAdminPassword] = useState('');
// Branding States
const [isBrandingModalOpen, setIsBrandingModalOpen] = useState(false);
const [selectedOrg, setSelectedOrg] = useState<Organization | null>(null);
@@ -49,13 +54,23 @@ export default function OrganizationsPage() {
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault();
try {
await cmsApi.createOrganization(newName, newDomain || undefined);
await cmsApi.provisionOrganization({
org_name: newName,
org_domain: newDomain || undefined,
admin_full_name: adminFullName,
admin_email: adminEmail,
admin_password: adminPassword
});
setNewName('');
setNewDomain('');
setAdminFullName('');
setAdminEmail('');
setAdminPassword('');
setIsModalOpen(false);
loadOrganizations();
} catch (error) {
console.error('Failed to create organization', error);
alert('Failed to provision organization. Please ensure the email is unique.');
}
};
@@ -271,10 +286,49 @@ export default function OrganizationsPage() {
type="text"
value={newDomain}
onChange={(e) => 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"
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"
/>
</div>
<div className="pt-4 border-t border-white/5">
<h3 className="text-xs font-black uppercase tracking-widest text-blue-500 mb-4">Initial Administrator</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1.5">Admin Full Name</label>
<input
type="text"
required
value={adminFullName}
onChange={(e) => 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"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1.5">Admin Email</label>
<input
type="email"
required
value={adminEmail}
onChange={(e) => setAdminEmail(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="admin@acme.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1.5">Admin Password</label>
<input
type="password"
required
value={adminPassword}
onChange={(e) => 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="••••••••"
/>
</div>
</div>
</div>
<div className="flex gap-3 mt-8">
<button
type="button"
+130 -4
View File
@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react';
import { cmsApi, User, Organization } from '@/lib/api';
import { useAuth } from '@/context/AuthContext';
import { UserCog, Mail, Search, Filter, ShieldCheck } from 'lucide-react';
import { UserCog, Mail, Search, Filter, ShieldCheck, Plus, X, UserPlus, Key, User as UserIcon } from 'lucide-react';
export default function UsersPage() {
const [users, setUsers] = useState<User[]>([]);
@@ -13,6 +13,15 @@ export default function UsersPage() {
const [roleFilter, setRoleFilter] = useState('');
const { user: currentUser } = useAuth();
// Create User States
const [isModalOpen, setIsModalOpen] = useState(false);
const [newUser, setNewUser] = useState({
email: '',
password: '',
full_name: '',
role: 'student'
});
useEffect(() => {
loadData();
}, []);
@@ -41,6 +50,19 @@ export default function UsersPage() {
}
};
const handleCreateUser = async (e: React.FormEvent) => {
e.preventDefault();
try {
await cmsApi.createUser(newUser);
setNewUser({ email: '', password: '', full_name: '', role: 'student' });
setIsModalOpen(false);
loadData();
} catch (error) {
console.error('Failed to create user', error);
alert('Failed to create user. Ensure email is unique.');
}
};
const filteredUsers = users.filter(u => {
const matchesSearch = u.full_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
u.email.toLowerCase().includes(searchTerm.toLowerCase());
@@ -62,9 +84,18 @@ export default function UsersPage() {
return (
<div className="space-y-8 animate-in fade-in duration-500">
<div>
<h1 className="text-3xl font-bold tracking-tight">User Management</h1>
<p className="text-gray-400 mt-1">Manage global users, roles, and organization assignments.</p>
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold tracking-tight">User Management</h1>
<p className="text-gray-400 mt-1">Manage users, roles, and organization assignments.</p>
</div>
<button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-all shadow-lg shadow-blue-500/20"
>
<Plus className="w-4 h-4" />
Add User
</button>
</div>
<div className="flex flex-col md:flex-row gap-4">
@@ -167,6 +198,101 @@ export default function UsersPage() {
</div>
)}
</div>
{/* Create User Modal */}
{isModalOpen && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200">
<div className="w-full max-w-md glass border border-white/10 rounded-2xl p-8 shadow-2xl">
<div className="flex justify-between items-center mb-6">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-400">
<UserPlus className="w-5 h-5" />
</div>
<h2 className="text-xl font-bold">Add New User</h2>
</div>
<button onClick={() => setIsModalOpen(false)} className="p-2 hover:bg-white/5 rounded-full transition-colors text-gray-500">
<X className="w-5 h-5" />
</button>
</div>
<form onSubmit={handleCreateUser} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-400 mb-1.5">Full Name</label>
<div className="relative">
<UserIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="text"
required
value={newUser.full_name}
onChange={(e) => 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"
placeholder="e.g. John Doe"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1.5">Email Address</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="email"
required
value={newUser.email}
onChange={(e) => 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"
placeholder="user@example.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1.5">Initial Password</label>
<div className="relative">
<Key className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="password"
required
value={newUser.password}
onChange={(e) => 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"
placeholder="••••••••"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-400 mb-1.5">User Role</label>
<select
value={newUser.role}
onChange={(e) => setNewUser({ ...newUser, role: 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 appearance-none cursor-pointer"
>
<option value="student">Student</option>
<option value="instructor">Instructor</option>
<option value="admin">Administrator (Organization Admin)</option>
</select>
</div>
<div className="flex gap-3 mt-8">
<button
type="button"
onClick={() => setIsModalOpen(false)}
className="flex-1 px-4 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg transition-all"
>
Cancel
</button>
<button
type="submit"
className="flex-[2] px-4 py-2.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-all shadow-lg shadow-blue-500/20 font-bold"
>
Create User
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}
+5 -2
View File
@@ -16,6 +16,7 @@ import {
LogOut,
Trash2
} from "lucide-react";
import Image from "next/image";
export default function ProfilePage() {
const { t, setLanguage: setContextLanguage } = useTranslation();
@@ -102,10 +103,12 @@ export default function ProfilePage() {
<div className="relative mb-6">
<div className="w-32 h-32 rounded-full bg-blue-600/20 border-4 border-white/5 flex items-center justify-center overflow-hidden shadow-2xl relative">
{avatarUrl ? (
<img
<Image
src={getImageUrl(avatarUrl)}
alt={fullName}
className="w-full h-full object-cover"
fill
className="object-cover"
sizes="128px"
/>
) : (
<span className="text-5xl font-black text-blue-400">