diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 0133be1..bd74508 100755 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -2840,3 +2840,234 @@ textarea.form-input { .nav-sub-item .status-dot-sm { display: inline-block; } + +/* ═══════════════════════════════════════════════════════════════════════════ + Teams — Card Grid + ═══════════════════════════════════════════════════════════════════════════ */ + +.teams-toolbar { + margin-bottom: 1.5rem; +} + +.teams-search { + position: relative; + max-width: 400px; +} + +.teams-search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); + font-size: 0.9rem; +} + +.teams-search-input { + padding-left: 36px !important; + padding-right: 36px !important; +} + +.teams-search-clear { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-muted); + font-size: 1.2rem; + cursor: pointer; + padding: 4px 8px; + line-height: 1; +} + +.teams-search-clear:hover { + color: var(--text-primary); +} + +.teams-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1rem; +} + +.team-card { + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.25rem; + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + animation: teamCardEnter 0.35s ease both; + display: flex; + flex-direction: column; +} + +.team-card:nth-child(1) { animation-delay: 0.00s; } +.team-card:nth-child(2) { animation-delay: 0.05s; } +.team-card:nth-child(3) { animation-delay: 0.10s; } +.team-card:nth-child(4) { animation-delay: 0.15s; } +.team-card:nth-child(5) { animation-delay: 0.20s; } +.team-card:nth-child(6) { animation-delay: 0.25s; } +.team-card:nth-child(7) { animation-delay: 0.30s; } +.team-card:nth-child(8) { animation-delay: 0.35s; } +.team-card:nth-child(9) { animation-delay: 0.40s; } +.team-card:nth-child(10) { animation-delay: 0.45s; } + +.team-card:hover { + transform: translateY(-3px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25); + border-color: var(--accent); +} + +@keyframes teamCardEnter { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.team-card-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.team-card-icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: var(--accent-light); + display: flex; + align-items: center; + justify-content: center; + color: var(--accent); + font-size: 1.1rem; + flex-shrink: 0; +} + +.team-card-title { + margin: 0; + font-size: 1.05rem; + font-weight: 600; + color: var(--text-primary); +} + +.team-card-title a { + color: var(--text-primary); + text-decoration: none; +} + +.team-card-title a:hover { + color: var(--accent); +} + +.team-card-desc { + margin: 0 0 1rem; + font-size: 0.875rem; + color: var(--text-secondary); + line-height: 1.5; + flex: 1; +} + +.team-card-stats { + display: flex; + gap: 1rem; + margin-bottom: 1rem; + font-size: 0.8rem; + color: var(--text-muted); +} + +.team-stat { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.team-stat i { + font-size: 0.75rem; +} + +.team-card-actions { + display: flex; + gap: 0.5rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border-color); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + Autocomplete — User Search + ═══════════════════════════════════════════════════════════════════════════ */ + +.autocomplete { + position: relative; +} + +.autocomplete-input { + width: 100%; +} + +.autocomplete-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 100; + background: var(--bg-card); + border: 1px solid var(--border-color); + border-radius: 0 0 8px 8px; + max-height: 250px; + overflow-y: auto; + display: none; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); +} + +.autocomplete-dropdown.active { + display: block; +} + +.autocomplete-item { + padding: 10px 14px; + cursor: pointer; + transition: background 0.1s; + border-bottom: 1px solid var(--border-color); +} + +.autocomplete-item:last-child { + border-bottom: none; +} + +.autocomplete-item:hover { + background: var(--accent-light); +} + +.autocomplete-item strong { + color: var(--text-primary); + font-size: 0.9rem; +} + +.autocomplete-item small { + color: var(--text-muted); + font-size: 0.8rem; + margin-left: 0.5rem; +} + +.autocomplete-empty { + padding: 14px; + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; +} + +.form-label { + display: block; + margin-bottom: 0.35rem; + font-size: 0.85rem; + font-weight: 500; + color: var(--text-secondary); +} diff --git a/routes/web.php b/routes/web.php index 2bb53a2..8019579 100755 --- a/routes/web.php +++ b/routes/web.php @@ -67,6 +67,7 @@ $router->get('/servers/:id/metrics', [ServerController::class, 'metrics'], [Auth $router->get('/sidebar/servers', [ServerController::class, 'sidebarServers'], [AuthMiddleware::class]); $router->get('/teams', [TeamController::class, 'index'], [AuthMiddleware::class, RoleMiddleware::class]); +$router->get('/teams/search-users', [TeamController::class, 'searchUsers'], [AuthMiddleware::class, RoleMiddleware::class]); $router->get('/teams/create', [TeamController::class, 'create'], [AuthMiddleware::class, RoleMiddleware::class]); $router->post('/teams/create', [TeamController::class, 'store'], [AuthMiddleware::class, CSRFMiddleware::class, RoleMiddleware::class]); $router->get('/teams/:id', [TeamController::class, 'show'], [AuthMiddleware::class, RoleMiddleware::class]); diff --git a/src/Controllers/TeamController.php b/src/Controllers/TeamController.php index bf47522..4af51c1 100644 --- a/src/Controllers/TeamController.php +++ b/src/Controllers/TeamController.php @@ -30,14 +30,53 @@ class TeamController public function index(): void { $userId = (int) Session::get('user_id'); - $teams = $this->teamModel->findByCreator($userId); + $search = $_GET['search'] ?? ''; + + if ($search !== '') { + $teams = $this->teamModel->findByCreator($userId); + $teams = array_filter($teams, function ($team) use ($search) { + $s = mb_strtolower($search); + return mb_strpos(mb_strtolower($team['name'] ?? ''), $s) !== false + || mb_strpos(mb_strtolower($team['description'] ?? ''), $s) !== false; + }); + } else { + $teams = $this->teamModel->findByCreator($userId); + } $this->view->display('teams.index', [ 'title' => 'Teams - ServerManager', 'teams' => $teams, + 'search' => $search, ]); } + public function searchUsers(): void + { + $q = $_GET['q'] ?? ''; + if (mb_strlen($q) < 2) { + $this->view->json(['users' => []]); + return; + } + + $userModel = new User(); + $allUsers = $userModel->getAll(1, 999); + $filtered = array_filter($allUsers['data'], function ($u) use ($q) { + $s = mb_strtolower($q); + return mb_strpos(mb_strtolower($u['username'] ?? ''), $s) !== false + || mb_strpos(mb_strtolower($u['email'] ?? ''), $s) !== false; + }); + + $results = array_map(function ($u) { + return [ + 'id' => (int) $u['id'], + 'username' => $u['username'], + 'email' => $u['email'], + ]; + }, array_slice(array_values($filtered), 0, 20)); + + $this->view->json(['users' => $results]); + } + public function create(): void { $this->view->display('teams.create', [ diff --git a/views/teams/index.php b/views/teams/index.php index b617480..041fbb4 100644 --- a/views/teams/index.php +++ b/views/teams/index.php @@ -1,4 +1,4 @@ - + -
-
- -
- -

No teams created yet.

- Create Your First Team -
- -
- - - - - - - - - - - - - - - - - - - - - -
NameDescriptionMembersServersActions
- - - - -
- - - - -
-
-
+
+
+ + + +
+ +
+
+
+ +
+

+ +

+
+

+ +

+
+ + members + + + servers + +
+
+ + Manage + + +
+
+ +
+ +