feat: global platform v2 update

This commit is contained in:
ivannoskov
2025-12-29 10:12:13 +03:00
parent 8e6a1fa83f
commit f64cf02100
84 changed files with 11023 additions and 11486 deletions

View File

@@ -0,0 +1,267 @@
"use client";
// ============================================================================
// Workspace Invites Page
// ============================================================================
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import { Loader2, UserPlus, Mail, Check, X, Clock } from "lucide-react";
import { DashboardHeader } from "@/components/layout/dashboard-header";
import { invitesApi } from "@/lib/api";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import type { WorkspaceInvite } from "@/lib/types/api";
// ============================================================================
// Component
// ============================================================================
export default function InvitesPage() {
const params = useParams();
const workspaceId = params?.workspaceId as string;
const [invites, setInvites] = useState<WorkspaceInvite[]>([]);
const [loading, setLoading] = useState(true);
const [showDialog, setShowDialog] = useState(false);
const [username, setUsername] = useState("");
const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadInvites();
}, [workspaceId]);
const loadInvites = async () => {
try {
setLoading(true);
const response = await invitesApi.list(workspaceId, { size: 100 });
setInvites(response.items);
} catch (err) {
console.error("Failed to load invites:", err);
} finally {
setLoading(false);
}
};
const handleCreateInvite = async () => {
if (!username.trim()) return;
try {
setIsCreating(true);
setError(null);
// Remove @ if present
const cleanUsername = username.trim().replace(/^@/, "");
const invite = await invitesApi.create(workspaceId, {
username: cleanUsername,
});
setInvites((prev) => [invite, ...prev]);
setShowDialog(false);
setUsername("");
} catch (err: any) {
setError(err?.error?.message || "Не удалось отправить приглашение");
} finally {
setIsCreating(false);
}
};
const getStatusIcon = (status: WorkspaceInvite["status"]) => {
switch (status) {
case "pending":
return <Clock className="h-4 w-4 text-yellow-500" />;
case "accepted":
return <Check className="h-4 w-4 text-green-500" />;
case "rejected":
return <X className="h-4 w-4 text-red-500" />;
}
};
const getStatusText = (status: WorkspaceInvite["status"]) => {
switch (status) {
case "pending":
return "Ожидает";
case "accepted":
return "Принято";
case "rejected":
return "Отклонено";
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString("ru-RU", {
day: "numeric",
month: "short",
year: "numeric",
});
};
return (
<>
<DashboardHeader
breadcrumbs={[
{ label: "Главная", href: `/dashboard/${workspaceId}` },
{ label: "Настройки", href: `/dashboard/${workspaceId}/settings` },
{ label: "Приглашения" },
]}
showProjectSelector={false}
/>
<div className="flex flex-1 flex-col gap-4 p-4 pt-0 mt-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">Приглашения</h1>
<p className="text-muted-foreground">
Приглашения в воркспейс через Telegram
</p>
</div>
<Dialog open={showDialog} onOpenChange={setShowDialog}>
<DialogTrigger asChild>
<Button>
<UserPlus className="h-4 w-4 mr-2" />
Пригласить
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Пригласить участника</DialogTitle>
<DialogDescription>
Введите username Telegram пользователя для отправки
приглашения через бота
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="username">Telegram username</Label>
<Input
id="username"
placeholder="@username"
value={username}
onChange={(e) => setUsername(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleCreateInvite();
}
}}
/>
</div>
{error && (
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowDialog(false)}
>
Отмена
</Button>
<Button
onClick={handleCreateInvite}
disabled={!username.trim() || isCreating}
>
{isCreating ? (
<Loader2 className="h-4 w-4 animate-spin mr-2" />
) : (
<Mail className="h-4 w-4 mr-2" />
)}
Отправить
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
) : invites.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Mail className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Нет приглашений</h3>
<p className="text-muted-foreground text-center">
Пригласите участников в воркспейс через Telegram
</p>
</CardContent>
</Card>
) : (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Username</TableHead>
<TableHead>Статус</TableHead>
<TableHead>Дата</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invites.map((invite) => (
<TableRow key={invite.id}>
<TableCell>
<div className="flex items-center gap-2">
<UserPlus className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">@{invite.username}</span>
</div>
</TableCell>
<TableCell>
<Badge
variant={
invite.status === "pending"
? "outline"
: invite.status === "accepted"
? "default"
: "destructive"
}
className="gap-1"
>
{getStatusIcon(invite.status)}
{getStatusText(invite.status)}
</Badge>
</TableCell>
<TableCell className="text-muted-foreground">
{formatDate(invite.created_at)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
)}
</div>
</>
);
}