UUID refactoring: project URLs by ID, mentions send member IDs, remove slug from JWT decode
This commit is contained in:
parent
407c065b49
commit
79541d133a
@ -21,7 +21,7 @@ function App() {
|
|||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path="/projects/:slug" element={
|
<Route path="/projects/:id" element={
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<ProjectPage />
|
<ProjectPage />
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)]"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 />;
|
||||||
}
|
}
|
||||||
@ -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">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user