Files
task-team/apps/tasks/components/GroupSelector.tsx
Admin 867482c674 UI redesign: compact header, group dropdown, slim task cards
- Removed logo/brand from header
- Group selector as dropdown (not horizontal scroll)
- Compact task cards (single line, less padding)
- Status filter pills smaller
- Sticky header 44px

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:44:41 +00:00

116 lines
4.2 KiB
TypeScript

"use client";
import { useState, useRef, useEffect, useCallback } from "react";
import { Group } from "@/lib/api";
import { useTranslation } from "@/lib/i18n";
interface GroupSelectorProps {
groups: Group[];
selected: string | null;
onSelect: (id: string | null) => void;
}
export default function GroupSelector({
groups,
selected,
onSelect,
}: GroupSelectorProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const selectedGroup = selected ? groups.find((g) => g.id === selected) : null;
const handleSelect = useCallback((id: string | null) => {
onSelect(id);
setOpen(false);
}, [onSelect]);
// Close on outside click
useEffect(() => {
if (!open) return;
function handleClick(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [open]);
// Close on escape
useEffect(() => {
if (!open) return;
function handleKey(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
document.addEventListener("keydown", handleKey);
return () => document.removeEventListener("keydown", handleKey);
}, [open]);
return (
<div ref={dropdownRef} className="relative px-4">
{/* Dropdown trigger */}
<button
onClick={() => setOpen(!open)}
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors text-sm font-medium min-h-[40px]"
>
{selectedGroup ? (
<>
{selectedGroup.icon && <span className="text-base">{selectedGroup.icon}</span>}
<span style={{ color: selectedGroup.color }}>{selectedGroup.name}</span>
</>
) : (
<span>{t("tasks.all")}</span>
)}
<svg
className={`w-4 h-4 text-muted transition-transform ${open ? "rotate-180" : ""}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Dropdown menu */}
{open && (
<div className="absolute left-4 top-full mt-1 z-50 min-w-[200px] bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-xl py-1 animate-fadeIn">
<button
onClick={() => handleSelect(null)}
className={`w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors ${
selected === null ? "font-semibold bg-gray-50 dark:bg-gray-800/50" : ""
}`}
>
<span className="w-5 text-center text-base">*</span>
<span>{t("tasks.all")}</span>
{selected === null && (
<svg className="w-4 h-4 ml-auto text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)}
</button>
{groups.map((g) => (
<button
key={g.id}
onClick={() => handleSelect(g.id)}
className={`w-full flex items-center gap-2.5 px-4 py-2.5 text-sm text-left hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors ${
selected === g.id ? "font-semibold bg-gray-50 dark:bg-gray-800/50" : ""
}`}
>
<span className="w-5 text-center text-base">{g.icon || ""}</span>
<span style={{ color: g.color }}>{g.name}</span>
{selected === g.id && (
<svg className="w-4 h-4 ml-auto text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
)}
</button>
))}
</div>
)}
</div>
);
}