UUID refactoring: project URLs by ID, mentions send member IDs, remove slug from JWT decode

This commit is contained in:
Markov 2026-03-02 09:03:01 +01:00
parent 407c065b49
commit 79541d133a
7 changed files with 22 additions and 14 deletions

View File

@ -21,7 +21,7 @@ function App() {
</AuthGuard> </AuthGuard>
} /> } />
<Route path="/projects/:slug" element={ <Route path="/projects/:id" element={
<AuthGuard> <AuthGuard>
<ProjectPage /> <ProjectPage />
</AuthGuard> </AuthGuard>

View File

@ -38,6 +38,7 @@ function isImageMime(mime: string | null): boolean {
export default function ChatPanel({ chatId, projectId }: Props) { export default function ChatPanel({ chatId, projectId }: Props) {
const [messages, setMessages] = useState<Message[]>([]); const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const mentionMapRef = useRef<Map<string, string>>(new Map()); // slug → member_id
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [loadingOlder, setLoadingOlder] = useState(false); const [loadingOlder, setLoadingOlder] = useState(false);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
@ -197,11 +198,15 @@ export default function ChatPanel({ chatId, projectId }: Props) {
})); }));
setInput(""); setInput("");
setPendingFiles([]); setPendingFiles([]);
mentionMapRef.current.clear();
setSending(true); setSending(true);
try { try {
// Extract @mentions from text // Extract @mentions from text → resolve to member IDs
const mentionMatches = text.match(/@(\w+)/g); const mentionMatches = text.match(/@(\w+)/g);
const mentions = mentionMatches ? [...new Set(mentionMatches.map(m => m.slice(1)))] : []; const mentionSlugs = mentionMatches ? [...new Set(mentionMatches.map(m => m.slice(1)))] : [];
const mentions = mentionSlugs
.map(slug => mentionMapRef.current.get(slug))
.filter((id): id is string => !!id);
const msg = await sendMessage({ const msg = await sendMessage({
chat_id: chatId, chat_id: chatId,
@ -422,6 +427,7 @@ export default function ChatPanel({ chatId, projectId }: Props) {
value={input} value={input}
onChange={setInput} onChange={setInput}
onSubmit={handleSend} onSubmit={handleSend}
onMentionInsert={(id, slug) => mentionMapRef.current.set(slug, id)}
placeholder="Сообщение... (@mention)" placeholder="Сообщение... (@mention)"
disabled={sending} disabled={sending}
/> />

View File

@ -7,11 +7,12 @@ interface Props {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onSubmit: () => void; onSubmit: () => void;
onMentionInsert?: (memberId: string, slug: string) => void;
placeholder?: string; placeholder?: string;
disabled?: boolean; disabled?: boolean;
} }
export default function MentionInput({ projectId, value, onChange, onSubmit, placeholder, disabled }: Props) { export default function MentionInput({ projectId, value, onChange, onSubmit, onMentionInsert, placeholder, disabled }: Props) {
const [members, setMembers] = useState<ProjectMember[]>([]); const [members, setMembers] = useState<ProjectMember[]>([]);
const [showDropdown, setShowDropdown] = useState(false); const [showDropdown, setShowDropdown] = useState(false);
const [filter, setFilter] = useState(""); const [filter, setFilter] = useState("");
@ -37,6 +38,7 @@ export default function MentionInput({ projectId, value, onChange, onSubmit, pla
const after = value.slice(inputRef.current?.selectionStart ?? value.length); const after = value.slice(inputRef.current?.selectionStart ?? value.length);
const newValue = `${before}@${member.slug} ${after}`; const newValue = `${before}@${member.slug} ${after}`;
onChange(newValue); onChange(newValue);
onMentionInsert?.(member.id, member.slug);
setShowDropdown(false); setShowDropdown(false);
setMentionStart(-1); setMentionStart(-1);
// Focus and set cursor after mention // Focus and set cursor after mention

View File

@ -6,10 +6,10 @@ import { logout } from "@/lib/auth-client";
interface Props { interface Props {
projects: Project[]; projects: Project[];
activeSlug?: string; activeId?: string;
} }
export default function Sidebar({ projects, activeSlug }: Props) { export default function Sidebar({ projects, activeId }: Props) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const nav = ( const nav = (
@ -42,10 +42,10 @@ export default function Sidebar({ projects, activeSlug }: Props) {
{projects.map((p) => ( {projects.map((p) => (
<Link <Link
key={p.id} key={p.id}
to={`/projects/${p.slug}`} to={`/projects/${p.id}`}
onClick={() => setOpen(false)} onClick={() => setOpen(false)}
className={`block px-3 py-2 rounded text-sm transition-colors ${ className={`block px-3 py-2 rounded text-sm transition-colors ${
activeSlug === p.slug activeId === p.id
? "bg-[var(--accent)]/10 text-[var(--accent)]" ? "bg-[var(--accent)]/10 text-[var(--accent)]"
: "hover:bg-white/5 text-[var(--fg)]" : "hover:bg-white/5 text-[var(--fg)]"
}`} }`}

View File

@ -9,12 +9,12 @@ function getToken(): string | null {
return localStorage.getItem("tb_token"); return localStorage.getItem("tb_token");
} }
export function getCurrentSlug(): string { export function getCurrentMemberId(): string {
const token = getToken(); const token = getToken();
if (!token) return "unknown"; if (!token) return "unknown";
try { try {
const payload = JSON.parse(atob(token.split(".")[1])); const payload = JSON.parse(atob(token.split(".")[1]));
return payload.slug || payload.sub || "unknown"; return payload.sub || "unknown";
} catch { } catch {
return "unknown"; return "unknown";
} }

View File

@ -27,5 +27,5 @@ export default function HomePage() {
} }
// Redirect to first project // Redirect to first project
return <Navigate to={`/projects/${projects[0].slug}`} replace />; return <Navigate to={`/projects/${projects[0].id}`} replace />;
} }

View File

@ -16,7 +16,7 @@ const TABS = [
]; ];
export default function ProjectPage() { export default function ProjectPage() {
const { slug } = useParams<{ slug: string }>(); const { id: projectId } = useParams<{ id: string }>();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -36,7 +36,7 @@ export default function ProjectPage() {
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);
const project = projects.find((p) => p.slug === slug); const project = projects.find((p) => p.id === projectId);
const handleProjectUpdated = (updated: Project) => { const handleProjectUpdated = (updated: Project) => {
setProjects((prev) => prev.map((p) => (p.id === updated.id ? updated : p))); setProjects((prev) => prev.map((p) => (p.id === updated.id ? updated : p)));
@ -52,7 +52,7 @@ export default function ProjectPage() {
return ( return (
<div className="flex h-[100dvh]"> <div className="flex h-[100dvh]">
<Sidebar projects={projects} activeSlug={slug} /> <Sidebar projects={projects} activeId={projectId} />
<main className="flex-1 flex flex-col overflow-hidden"> <main className="flex-1 flex flex-col overflow-hidden">
<header className="border-b border-[var(--border)] px-6 py-3 pl-14 md:pl-6"> <header className="border-b border-[var(--border)] px-6 py-3 pl-14 md:pl-6">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">