mirror of
https://github.com/OHV-IT/collabrix.git
synced 2025-12-15 16:48:36 +01:00
- Complete chat application similar to Microsoft Teams - Code snippet library with syntax highlighting - Real-time messaging with WebSockets - File upload with Office integration - Department-based permissions - Dark/Light theme support - Production deployment with SSL/Reverse Proxy - Docker containerization - PostgreSQL database with SQLModel ORM
194 lines
6.7 KiB
TypeScript
194 lines
6.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Outlet, Link, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '../../contexts/AuthContext';
|
|
import { useTheme } from '../../contexts/ThemeContext';
|
|
import { departmentsAPI } from '../../services/api';
|
|
import type { Department } from '../../types';
|
|
|
|
const Layout: React.FC = () => {
|
|
const { user, logout } = useAuth();
|
|
const { theme, toggleTheme } = useTheme();
|
|
const location = useLocation();
|
|
const [hasSnippetAccess, setHasSnippetAccess] = useState(false);
|
|
const [userMenuOpen, setUserMenuOpen] = useState(false);
|
|
|
|
const getInitials = () => {
|
|
if (!user) return '?';
|
|
if (user.full_name) {
|
|
const parts = user.full_name.trim().split(' ');
|
|
if (parts.length >= 2) {
|
|
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
}
|
|
return parts[0][0].toUpperCase();
|
|
}
|
|
return user.username.charAt(0).toUpperCase();
|
|
};
|
|
|
|
// Close menu when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
const target = event.target as HTMLElement;
|
|
if (userMenuOpen && !target.closest('.user-menu-container')) {
|
|
setUserMenuOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, [userMenuOpen]);
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const determineSnippetAccess = async () => {
|
|
if (!user) {
|
|
if (isMounted) {
|
|
setHasSnippetAccess(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (user.is_admin) {
|
|
if (isMounted) {
|
|
setHasSnippetAccess(true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const departments: Department[] = await departmentsAPI.getMy();
|
|
if (!isMounted) {
|
|
return;
|
|
}
|
|
const enabled = departments.some((dept) => dept.snippets_enabled);
|
|
setHasSnippetAccess(enabled);
|
|
} catch (error) {
|
|
console.error('Failed to determine snippet permissions:', error);
|
|
if (isMounted) {
|
|
setHasSnippetAccess(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
determineSnippetAccess();
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [user?.id, user?.is_admin]);
|
|
|
|
return (
|
|
<div className="h-screen flex flex-col bg-gray-50 dark:bg-gray-900">
|
|
{/* Header */}
|
|
<header className="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-3 py-2">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
<h1 className="text-base font-bold text-gray-900 dark:text-white">
|
|
Collabrix
|
|
</h1>
|
|
<nav className="flex space-x-1.5">
|
|
<Link
|
|
to="/"
|
|
className={`px-3 py-1.5 text-sm rounded ${
|
|
location.pathname === '/'
|
|
? 'bg-blue-500 text-white'
|
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
Chat
|
|
</Link>
|
|
{(user?.is_admin || hasSnippetAccess) && (
|
|
<Link
|
|
to="/snippets"
|
|
className={`px-3 py-1.5 text-sm rounded ${
|
|
location.pathname === '/snippets'
|
|
? 'bg-blue-500 text-white'
|
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
Snippets
|
|
</Link>
|
|
)}
|
|
{user?.is_admin && (
|
|
<Link
|
|
to="/admin"
|
|
className={`px-3 py-1.5 text-sm rounded ${
|
|
location.pathname === '/admin'
|
|
? 'bg-blue-500 text-white'
|
|
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
}`}
|
|
>
|
|
🔧 Admin
|
|
</Link>
|
|
)}
|
|
</nav>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2.5 relative user-menu-container">
|
|
<button
|
|
onClick={() => setUserMenuOpen(!userMenuOpen)}
|
|
className="flex items-center space-x-2 px-2 py-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
>
|
|
{user?.profile_picture ? (
|
|
<img
|
|
src={`http://localhost:8000/${user.profile_picture}`}
|
|
alt={user.username}
|
|
className="w-8 h-8 rounded-full object-cover"
|
|
/>
|
|
) : (
|
|
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm font-bold">
|
|
{getInitials()}
|
|
</div>
|
|
)}
|
|
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
|
{user?.username}
|
|
</span>
|
|
<svg className="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{userMenuOpen && (
|
|
<div className="absolute right-0 top-full mt-1 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 z-50">
|
|
<Link
|
|
to="/profile"
|
|
onClick={() => setUserMenuOpen(false)}
|
|
className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
>
|
|
Profil
|
|
</Link>
|
|
<button
|
|
onClick={() => {
|
|
toggleTheme();
|
|
setUserMenuOpen(false);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
>
|
|
{theme === 'light' ? 'Dark Mode' : 'Light Mode'}
|
|
</button>
|
|
<hr className="my-1 border-gray-200 dark:border-gray-700" />
|
|
<button
|
|
onClick={() => {
|
|
logout();
|
|
setUserMenuOpen(false);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
>
|
|
Logout
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<main className="flex-1 min-h-0 overflow-y-auto">
|
|
<Outlet />
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Layout;
|