trypost/resources/js/pages/workspaces/Invites.vue

286 lines
11 KiB
Vue

<script setup lang="ts">
import { Head, useForm, router } from '@inertiajs/vue3';
import { UserPlus, Users, Mail, Clock, Trash2, Crown, User, Shield } from 'lucide-vue-next';
import AppLayout from '@/layouts/AppLayout.vue';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { members } from '@/routes';
import { destroy as destroyInvite, store as storeInvite } from '@/routes/invites';
import { remove as removeMember } from '@/routes/members';
import { type BreadcrumbItemType } from '@/types';
interface Workspace {
id: string;
name: string;
}
interface Member {
id: string;
name: string;
email: string;
role: string;
}
interface Invite {
id: string;
email: string;
role: { value: string; label: string };
status: { value: string; label: string; color: string };
expires_at: string;
inviter: { name: string };
}
interface Role {
value: string;
label: string;
}
interface Props {
workspace: Workspace;
owner: Member;
members: Member[];
invites: Invite[];
roles: Role[];
}
const props = defineProps<Props>();
const breadcrumbs: BreadcrumbItemType[] = [
{ title: 'Members', href: members.url() },
];
const form = useForm({
email: '',
role: 'member',
});
function submitInvite() {
form.post(storeInvite.url(), {
preserveScroll: true,
onSuccess: () => {
form.reset();
},
});
}
function cancelInvite(inviteId: string) {
if (confirm('Are you sure you want to cancel this invite?')) {
router.delete(destroyInvite.url(inviteId), {
preserveScroll: true,
});
}
}
function handleRemoveMember(memberId: string) {
if (confirm('Are you sure you want to remove this member?')) {
router.delete(removeMember.url(memberId), {
preserveScroll: true,
});
}
}
function formatDate(dateString: string): string {
return new Date(dateString).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
function getStatusColor(color: string): string {
const colors: Record<string, string> = {
yellow: 'bg-yellow-100 text-yellow-800',
green: 'bg-green-100 text-green-800',
gray: 'bg-gray-100 text-gray-800',
red: 'bg-red-100 text-red-800',
};
return colors[color] || 'bg-gray-100 text-gray-800';
}
function getRoleIcon(role: string) {
if (role === 'admin') return Shield;
return User;
}
</script>
<template>
<Head :title="`Team - ${workspace.name}`" />
<AppLayout :breadcrumbs="breadcrumbs">
<div class="flex flex-col gap-6 p-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">Team</h1>
<p class="text-muted-foreground">
Manage members and invites for this workspace
</p>
</div>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<UserPlus class="h-5 w-5" />
Invite Member
</CardTitle>
<CardDescription>
Send an email invite to add collaborators
</CardDescription>
</CardHeader>
<CardContent>
<form @submit.prevent="submitInvite" class="space-y-4">
<div class="space-y-2">
<Label for="email">Email</Label>
<Input
id="email"
v-model="form.email"
type="email"
placeholder="collaborator@email.com"
:class="{ 'border-red-500': form.errors.email }"
/>
<p v-if="form.errors.email" class="text-sm text-red-500">
{{ form.errors.email }}
</p>
</div>
<div class="space-y-2">
<Label for="role">Role</Label>
<Select v-model="form.role">
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="role in roles"
:key="role.value"
:value="role.value"
>
{{ role.label }}
</SelectItem>
</SelectContent>
</Select>
</div>
<Button type="submit" :disabled="form.processing" class="w-full">
<Mail class="mr-2 h-4 w-4" />
Send Invite
</Button>
</form>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Clock class="h-5 w-5" />
Pending Invites
</CardTitle>
<CardDescription>
Invites awaiting acceptance
</CardDescription>
</CardHeader>
<CardContent>
<div v-if="invites.length === 0" class="text-center py-6 text-muted-foreground">
No pending invites
</div>
<div v-else class="space-y-3">
<div
v-for="invite in invites"
:key="invite.id"
class="flex items-center justify-between p-3 rounded-lg border"
>
<div class="flex-1 min-w-0">
<p class="font-medium truncate">{{ invite.email }}</p>
<div class="flex items-center gap-2 mt-1">
<Badge variant="outline" class="text-xs">
{{ invite.role.label }}
</Badge>
<Badge :class="getStatusColor(invite.status.color)" class="text-xs">
{{ invite.status.label }}
</Badge>
</div>
<p class="text-xs text-muted-foreground mt-1">
Expires {{ formatDate(invite.expires_at) }}
</p>
</div>
<Button
v-if="invite.status.value === 'pending'"
variant="ghost"
size="icon"
@click="cancelInvite(invite.id)"
>
<Trash2 class="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Users class="h-5 w-5" />
Members
</CardTitle>
<CardDescription>
People with access to this workspace
</CardDescription>
</CardHeader>
<CardContent>
<div class="space-y-3">
<div class="flex items-center justify-between p-3 rounded-lg border bg-muted/50">
<div class="flex items-center gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary text-primary-foreground">
<Crown class="h-5 w-5" />
</div>
<div>
<p class="font-medium">{{ owner.name }}</p>
<p class="text-sm text-muted-foreground">{{ owner.email }}</p>
</div>
</div>
<Badge>Owner</Badge>
</div>
<div
v-for="member in members"
:key="member.id"
class="flex items-center justify-between p-3 rounded-lg border"
>
<div class="flex items-center gap-3">
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-secondary text-secondary-foreground">
<component :is="getRoleIcon(member.role)" class="h-5 w-5" />
</div>
<div>
<p class="font-medium">{{ member.name }}</p>
<p class="text-sm text-muted-foreground">{{ member.email }}</p>
</div>
</div>
<div class="flex items-center gap-2">
<Badge variant="outline" class="capitalize">{{ member.role }}</Badge>
<Button
variant="ghost"
size="icon"
@click="handleRemoveMember(member.id)"
>
<Trash2 class="h-4 w-4 text-red-500" />
</Button>
</div>
</div>
<div v-if="members.length === 0" class="text-center py-6 text-muted-foreground">
No members besides the owner
</div>
</div>
</CardContent>
</Card>
</div>
</AppLayout>
</template>