UUID everywhere: all API calls use ID instead of slug
- Project API: project.id for all CRUD + files + members - Member API: member.id for get/update/regenerate/revoke - ProjectSettings: member selection by id - ChatPanel/MentionInput: projectId prop - AgentModal: agent.id for API, slug for display only
This commit is contained in:
parent
fd3e454dff
commit
52be59dc9f
@ -28,7 +28,7 @@ export default function AgentModal({ agent, onClose, onUpdated }: Props) {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const caps = capabilities.split(",").map((c) => c.trim()).filter(Boolean);
|
const caps = capabilities.split(",").map((c) => c.trim()).filter(Boolean);
|
||||||
const updated = await updateMember(agent.slug, {
|
const updated = await updateMember(agent.id, {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
agent_config: {
|
agent_config: {
|
||||||
capabilities: caps,
|
capabilities: caps,
|
||||||
@ -49,7 +49,7 @@ export default function AgentModal({ agent, onClose, onUpdated }: Props) {
|
|||||||
|
|
||||||
const handleRegenerate = async () => {
|
const handleRegenerate = async () => {
|
||||||
try {
|
try {
|
||||||
const { token } = await regenerateToken(agent.slug);
|
const { token } = await regenerateToken(agent.id);
|
||||||
setCurrentToken(token);
|
setCurrentToken(token);
|
||||||
setConfirmRegen(false);
|
setConfirmRegen(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -59,7 +59,7 @@ export default function AgentModal({ agent, onClose, onUpdated }: Props) {
|
|||||||
|
|
||||||
const handleRevoke = async () => {
|
const handleRevoke = async () => {
|
||||||
try {
|
try {
|
||||||
await revokeToken(agent.slug);
|
await revokeToken(agent.id);
|
||||||
setCurrentToken(null);
|
setCurrentToken(null);
|
||||||
setConfirmRevoke(false);
|
setConfirmRevoke(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ interface StreamingState {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
projectSlug?: string;
|
projectId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatFileSize(bytes: number): string {
|
function formatFileSize(bytes: number): string {
|
||||||
@ -35,7 +35,7 @@ function isImageMime(mime: string | null): boolean {
|
|||||||
return !!mime && mime.startsWith("image/");
|
return !!mime && mime.startsWith("image/");
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChatPanel({ chatId, projectSlug }: 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 [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
@ -399,7 +399,7 @@ export default function ChatPanel({ chatId, projectSlug }: Props) {
|
|||||||
{uploading ? "⏳" : "📎"}
|
{uploading ? "⏳" : "📎"}
|
||||||
</button>
|
</button>
|
||||||
<MentionInput
|
<MentionInput
|
||||||
projectSlug={projectSlug || ""}
|
projectId={projectId || ""}
|
||||||
value={input}
|
value={input}
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
onSubmit={handleSend}
|
onSubmit={handleSend}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type { ProjectMember } from "@/lib/api";
|
|||||||
import { getProjectMembers } from "@/lib/api";
|
import { getProjectMembers } from "@/lib/api";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectSlug: string;
|
projectId: string;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
@ -11,7 +11,7 @@ interface Props {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MentionInput({ projectSlug, value, onChange, onSubmit, placeholder, disabled }: Props) {
|
export default function MentionInput({ projectId, value, onChange, onSubmit, 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("");
|
||||||
@ -22,9 +22,9 @@ export default function MentionInput({ projectSlug, value, onChange, onSubmit, p
|
|||||||
|
|
||||||
// Load members once
|
// Load members once
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectSlug) return;
|
if (!projectId) return;
|
||||||
getProjectMembers(projectSlug).then(setMembers).catch(() => {});
|
getProjectMembers(projectId).then(setMembers).catch(() => {});
|
||||||
}, [projectSlug]);
|
}, [projectId]);
|
||||||
|
|
||||||
const filtered = members.filter((m) =>
|
const filtered = members.filter((m) =>
|
||||||
m.name.toLowerCase().includes(filter.toLowerCase()) ||
|
m.name.toLowerCase().includes(filter.toLowerCase()) ||
|
||||||
|
|||||||
@ -47,14 +47,14 @@ export default function ProjectFiles({ project }: Props) {
|
|||||||
|
|
||||||
const loadFiles = useCallback(async () => {
|
const loadFiles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await getProjectFiles(project.slug, search || undefined);
|
const data = await getProjectFiles(project.id, search || undefined);
|
||||||
setFiles(data);
|
setFiles(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load files:", e);
|
console.error("Failed to load files:", e);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [project.slug, search]);
|
}, [project.id, search]);
|
||||||
|
|
||||||
// Debounced search
|
// Debounced search
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -67,7 +67,7 @@ export default function ProjectFiles({ project }: Props) {
|
|||||||
setUploading(true);
|
setUploading(true);
|
||||||
try {
|
try {
|
||||||
for (const file of Array.from(fileList)) {
|
for (const file of Array.from(fileList)) {
|
||||||
await uploadProjectFile(project.slug, file, uploadDesc || undefined);
|
await uploadProjectFile(project.id, file, uploadDesc || undefined);
|
||||||
}
|
}
|
||||||
setUploadDesc("");
|
setUploadDesc("");
|
||||||
setShowUploadDesc(false);
|
setShowUploadDesc(false);
|
||||||
@ -83,7 +83,7 @@ export default function ProjectFiles({ project }: Props) {
|
|||||||
const handleDelete = async (f: ProjectFile) => {
|
const handleDelete = async (f: ProjectFile) => {
|
||||||
if (!confirm(`Удалить ${f.filename}?`)) return;
|
if (!confirm(`Удалить ${f.filename}?`)) return;
|
||||||
try {
|
try {
|
||||||
await deleteProjectFile(project.slug, f.id);
|
await deleteProjectFile(project.id, f.id);
|
||||||
setFiles((prev) => prev.filter((x) => x.id !== f.id));
|
setFiles((prev) => prev.filter((x) => x.id !== f.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Delete failed:", e);
|
console.error("Delete failed:", e);
|
||||||
@ -92,7 +92,7 @@ export default function ProjectFiles({ project }: Props) {
|
|||||||
|
|
||||||
const handleSaveDesc = async (f: ProjectFile) => {
|
const handleSaveDesc = async (f: ProjectFile) => {
|
||||||
try {
|
try {
|
||||||
const updated = await updateProjectFile(project.slug, f.id, { description: editDesc });
|
const updated = await updateProjectFile(project.id, f.id, { description: editDesc });
|
||||||
setFiles((prev) => prev.map((x) => (x.id === f.id ? updated : x)));
|
setFiles((prev) => prev.map((x) => (x.id === f.id ? updated : x)));
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -171,7 +171,7 @@ export default function ProjectFiles({ project }: Props) {
|
|||||||
{!loading && files.length > 0 && (
|
{!loading && files.length > 0 && (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{files.map((f) => {
|
{files.map((f) => {
|
||||||
const url = getProjectFileUrl(project.slug, f.id);
|
const url = getProjectFileUrl(project.id, f.id);
|
||||||
const isImage = f.mime_type?.startsWith("image/");
|
const isImage = f.mime_type?.startsWith("image/");
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
const loadMembers = async () => {
|
const loadMembers = async () => {
|
||||||
try {
|
try {
|
||||||
const [projectMembers, allMembersList] = await Promise.all([
|
const [projectMembers, allMembersList] = await Promise.all([
|
||||||
getProjectMembers(project.slug),
|
getProjectMembers(project.id),
|
||||||
getMembers()
|
getMembers()
|
||||||
]);
|
]);
|
||||||
setMembers(projectMembers);
|
setMembers(projectMembers);
|
||||||
@ -58,8 +58,8 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
const handleAddMember = async () => {
|
const handleAddMember = async () => {
|
||||||
if (!selectedMember) return;
|
if (!selectedMember) return;
|
||||||
try {
|
try {
|
||||||
await addProjectMember(project.slug, selectedMember);
|
await addProjectMember(project.id, selectedMember);
|
||||||
const newMember = allMembers.find((m) => m.slug === selectedMember);
|
const newMember = allMembers.find((m) => m.id === selectedMember);
|
||||||
if (newMember) {
|
if (newMember) {
|
||||||
setMembers([...members, {
|
setMembers([...members, {
|
||||||
id: newMember.id,
|
id: newMember.id,
|
||||||
@ -75,10 +75,10 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveMember = async (memberSlug: string) => {
|
const handleRemoveMember = async (memberId: string) => {
|
||||||
try {
|
try {
|
||||||
await removeProjectMember(project.slug, memberSlug);
|
await removeProjectMember(project.id, memberId);
|
||||||
setMembers(members.filter((m) => m.slug !== memberSlug));
|
setMembers(members.filter((m) => m.id !== memberId));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to remove member:", e);
|
console.error("Failed to remove member:", e);
|
||||||
}
|
}
|
||||||
@ -88,7 +88,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
const urls = repoUrls.split("\n").map((u) => u.trim()).filter(Boolean);
|
const urls = repoUrls.split("\n").map((u) => u.trim()).filter(Boolean);
|
||||||
const updated = await updateProject(project.slug, {
|
const updated = await updateProject(project.id, {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
description: description.trim() || null,
|
description: description.trim() || null,
|
||||||
repo_urls: urls,
|
repo_urls: urls,
|
||||||
@ -106,7 +106,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
try {
|
try {
|
||||||
await deleteProject(project.slug);
|
await deleteProject(project.id);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@ -197,7 +197,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
{member.role !== "owner" && (
|
{member.role !== "owner" && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveMember(member.slug)}
|
onClick={() => handleRemoveMember(member.id)}
|
||||||
className="px-2 py-1 text-xs text-red-400 hover:bg-red-500/10 rounded"
|
className="px-2 py-1 text-xs text-red-400 hover:bg-red-500/10 rounded"
|
||||||
>
|
>
|
||||||
Убрать
|
Убрать
|
||||||
@ -217,7 +217,7 @@ export default function ProjectSettings({ project, onUpdated }: Props) {
|
|||||||
>
|
>
|
||||||
<option value="">Добавить участника...</option>
|
<option value="">Добавить участника...</option>
|
||||||
{availableMembers.map((member) => (
|
{availableMembers.map((member) => (
|
||||||
<option key={member.slug} value={member.slug}>
|
<option key={member.id} value={member.id}>
|
||||||
{member.name} ({member.slug}) - {member.type}
|
{member.name} ({member.slug}) - {member.type}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -169,20 +169,20 @@ export async function getProjects(): Promise<Project[]> {
|
|||||||
return request("/api/v1/projects");
|
return request("/api/v1/projects");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProject(slug: string): Promise<Project> {
|
export async function getProject(projectId: string): Promise<Project> {
|
||||||
return request(`/api/v1/projects/${slug}`);
|
return request(`/api/v1/projects/${projectId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createProject(data: { name: string; slug: string; description?: string }): Promise<Project> {
|
export async function createProject(data: { name: string; slug: string; description?: string }): Promise<Project> {
|
||||||
return request("/api/v1/projects", { method: "POST", body: JSON.stringify(data) });
|
return request("/api/v1/projects", { method: "POST", body: JSON.stringify(data) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProject(slug: string, data: Partial<Pick<Project, "name" | "description" | "repo_urls" | "status">>): Promise<Project> {
|
export async function updateProject(projectId: string, data: Partial<Pick<Project, "name" | "slug" | "description" | "repo_urls" | "status">>): Promise<Project> {
|
||||||
return request(`/api/v1/projects/${slug}`, { method: "PATCH", body: JSON.stringify(data) });
|
return request(`/api/v1/projects/${projectId}`, { method: "PATCH", body: JSON.stringify(data) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProject(slug: string): Promise<void> {
|
export async function deleteProject(projectId: string): Promise<void> {
|
||||||
await request(`/api/v1/projects/${slug}`, { method: "DELETE" });
|
await request(`/api/v1/projects/${projectId}`, { method: "DELETE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Tasks ---
|
// --- Tasks ---
|
||||||
@ -287,8 +287,8 @@ export async function getMembers(): Promise<Member[]> {
|
|||||||
return request("/api/v1/members");
|
return request("/api/v1/members");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getMember(slug: string): Promise<Member> {
|
export async function getMember(memberId: string): Promise<Member> {
|
||||||
return request(`/api/v1/members/${slug}`);
|
return request(`/api/v1/members/${memberId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createMember(data: {
|
export async function createMember(data: {
|
||||||
@ -300,21 +300,21 @@ export async function createMember(data: {
|
|||||||
return request("/api/v1/members", { method: "POST", body: JSON.stringify(data) });
|
return request("/api/v1/members", { method: "POST", body: JSON.stringify(data) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateMember(slug: string, data: {
|
export async function updateMember(memberId: string, data: {
|
||||||
name?: string;
|
name?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
agent_config?: Partial<AgentConfig>;
|
agent_config?: Partial<AgentConfig>;
|
||||||
}): Promise<Member> {
|
}): Promise<Member> {
|
||||||
return request(`/api/v1/members/${slug}`, { method: "PATCH", body: JSON.stringify(data) });
|
return request(`/api/v1/members/${memberId}`, { method: "PATCH", body: JSON.stringify(data) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function regenerateToken(slug: string): Promise<{ token: string }> {
|
export async function regenerateToken(memberId: string): Promise<{ token: string }> {
|
||||||
return request(`/api/v1/members/${slug}/regenerate-token`, { method: "POST" });
|
return request(`/api/v1/members/${memberId}/regenerate-token`, { method: "POST" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function revokeToken(slug: string): Promise<void> {
|
export async function revokeToken(memberId: string): Promise<void> {
|
||||||
await request(`/api/v1/members/${slug}/revoke-token`, { method: "POST" });
|
await request(`/api/v1/members/${memberId}/revoke-token`, { method: "POST" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Project Files ---
|
// --- Project Files ---
|
||||||
@ -330,18 +330,18 @@ export interface ProjectFile {
|
|||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProjectFiles(slug: string, search?: string): Promise<ProjectFile[]> {
|
export async function getProjectFiles(projectId: string, search?: string): Promise<ProjectFile[]> {
|
||||||
const qs = search ? `?search=${encodeURIComponent(search)}` : "";
|
const qs = search ? `?search=${encodeURIComponent(search)}` : "";
|
||||||
return request(`/api/v1/projects/${slug}/files${qs}`);
|
return request(`/api/v1/projects/${projectId}/files${qs}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadProjectFile(slug: string, file: File, description?: string): Promise<ProjectFile> {
|
export async function uploadProjectFile(projectId: string, file: File, description?: string): Promise<ProjectFile> {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
if (description) formData.append("description", description);
|
if (description) formData.append("description", description);
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE}/api/v1/projects/${slug}/files`, {
|
const res = await fetch(`${API_BASE}/api/v1/projects/${projectId}/files`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
body: formData,
|
body: formData,
|
||||||
@ -358,18 +358,18 @@ export async function uploadProjectFile(slug: string, file: File, description?:
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProjectFileUrl(slug: string, fileId: string): string {
|
export function getProjectFileUrl(projectId: string, fileId: string): string {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const qs = token ? `?token=${encodeURIComponent(token)}` : "";
|
const qs = token ? `?token=${encodeURIComponent(token)}` : "";
|
||||||
return `${API_BASE}/api/v1/projects/${slug}/files/${fileId}/download${qs}`;
|
return `${API_BASE}/api/v1/projects/${projectId}/files/${fileId}/download${qs}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateProjectFile(slug: string, fileId: string, data: { description?: string }): Promise<ProjectFile> {
|
export async function updateProjectFile(projectId: string, fileId: string, data: { description?: string }): Promise<ProjectFile> {
|
||||||
return request(`/api/v1/projects/${slug}/files/${fileId}`, { method: "PATCH", body: JSON.stringify(data) });
|
return request(`/api/v1/projects/${projectId}/files/${fileId}`, { method: "PATCH", body: JSON.stringify(data) });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProjectFile(slug: string, fileId: string): Promise<void> {
|
export async function deleteProjectFile(projectId: string, fileId: string): Promise<void> {
|
||||||
await request(`/api/v1/projects/${slug}/files/${fileId}`, { method: "DELETE" });
|
await request(`/api/v1/projects/${projectId}/files/${fileId}`, { method: "DELETE" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Chat File Upload ---
|
// --- Chat File Upload ---
|
||||||
@ -412,17 +412,17 @@ export function getAttachmentUrl(attachmentId: string): string {
|
|||||||
|
|
||||||
// --- Project Members ---
|
// --- Project Members ---
|
||||||
|
|
||||||
export async function getProjectMembers(slug: string): Promise<ProjectMember[]> {
|
export async function getProjectMembers(projectId: string): Promise<ProjectMember[]> {
|
||||||
return request(`/api/v1/projects/${slug}/members`);
|
return request(`/api/v1/projects/${projectId}/members`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addProjectMember(slug: string, memberSlug: string): Promise<{ ok: boolean }> {
|
export async function addProjectMember(projectId: string, memberId: string): Promise<{ ok: boolean }> {
|
||||||
return request(`/api/v1/projects/${slug}/members`, {
|
return request(`/api/v1/projects/${projectId}/members`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ slug: memberSlug })
|
body: JSON.stringify({ member_id: memberId })
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeProjectMember(slug: string, memberSlug: string): Promise<{ ok: boolean }> {
|
export async function removeProjectMember(projectId: string, memberId: string): Promise<{ ok: boolean }> {
|
||||||
return request(`/api/v1/projects/${slug}/members/${memberSlug}`, { method: "DELETE" });
|
return request(`/api/v1/projects/${projectId}/members/${memberId}`, { method: "DELETE" });
|
||||||
}
|
}
|
||||||
@ -101,7 +101,7 @@ export default function ProjectPage() {
|
|||||||
<KanbanBoard projectId={project.id} projectSlug={project.slug} />
|
<KanbanBoard projectId={project.id} projectSlug={project.slug} />
|
||||||
)}
|
)}
|
||||||
{activeTab === "chat" && project.chat_id && (
|
{activeTab === "chat" && project.chat_id && (
|
||||||
<ChatPanel chatId={project.chat_id} projectSlug={project.slug} />
|
<ChatPanel chatId={project.chat_id} projectId={project.id} />
|
||||||
)}
|
)}
|
||||||
{activeTab === "chat" && !project.chat_id && (
|
{activeTab === "chat" && !project.chat_id && (
|
||||||
<div className="flex items-center justify-center h-full text-[var(--muted)] text-sm">
|
<div className="flex items-center justify-center h-full text-[var(--muted)] text-sm">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user