DGSoft 93b98cfb5c Initial commit: Team Chat System with Code Snippet Library
- 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
2025-12-09 22:25:03 +01:00

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;