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>
} />
<Route path="/projects/:slug" element={
<Route path="/projects/:id" element={
<AuthGuard>
<ProjectPage />
</AuthGuard>

View File

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

View File

@ -7,11 +7,12 @@ interface Props {
value: string;
onChange: (value: string) => void;
onSubmit: () => void;
onMentionInsert?: (memberId: string, slug: string) => void;
placeholder?: string;
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 [showDropdown, setShowDropdown] = useState(false);
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 newValue = `${before}@${member.slug} ${after}`;
onChange(newValue);
onMentionInsert?.(member.id, member.slug);
setShowDropdown(false);
setMentionStart(-1);
// Focus and set cursor after mention

View File

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

View File

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

View File

@ -27,5 +27,5 @@ export default function HomePage() {
}
// 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() {
const { slug } = useParams<{ slug: string }>();
const { id: projectId } = useParams<{ id: string }>();
const [searchParams, setSearchParams] = useSearchParams();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
@ -36,7 +36,7 @@ export default function ProjectPage() {
.finally(() => setLoading(false));
}, []);
const project = projects.find((p) => p.slug === slug);
const project = projects.find((p) => p.id === projectId);
const handleProjectUpdated = (updated: Project) => {
setProjects((prev) => prev.map((p) => (p.id === updated.id ? updated : p)));
@ -52,7 +52,7 @@ export default function ProjectPage() {
return (
<div className="flex h-[100dvh]">
<Sidebar projects={projects} activeSlug={slug} />
<Sidebar projects={projects} activeId={projectId} />
<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">
<div className="flex items-center justify-between mb-2">