Skip to content

Commit 31ed4d2

Browse files
committed
feat: Add time-limited invite link generation to TeamView
- Create Invite Link button to generate shareable URLs - Modal displays invite link with copy-to-clipboard functionality - 7-day expiry notice for time-limited links - Complementary to existing email-based invite system
1 parent d779c99 commit 31ed4d2

File tree

1 file changed

+103
-1
lines changed

1 file changed

+103
-1
lines changed

src/components/DashboardViews/TeamView.tsx

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useState, useEffect, useCallback } from 'react';
22
import { cn } from '../../utils/cn';
33
import { useLanguage } from '../../contexts/LanguageContext';
4-
import { Plus, User, Mail, Shield, Trash2, Check, Loader2 } from 'lucide-react';
4+
import { Plus, User, Mail, Shield, Trash2, Check, Loader2, Link as LinkIcon, Copy } from 'lucide-react';
55
import { motion, AnimatePresence } from 'framer-motion';
66
import { supabase } from '../../config/supabase';
77
import { useAuth } from '../../contexts/AuthContext';
@@ -24,6 +24,9 @@ export default function TeamView({ isDark = true }: { isDark?: boolean }) {
2424
const [inviteSuccess, setInviteSuccess] = useState(false);
2525
const [members, setMembers] = useState<TeamMember[]>([]);
2626
const [isLoadingMembers, setIsLoadingMembers] = useState(true);
27+
const [inviteLink, setInviteLink] = useState<string | null>(null);
28+
const [copiedInvite, setCopiedInvite] = useState(false);
29+
const [currentTeamId, setCurrentTeamId] = useState<string | null>(null);
2730

2831
const getAuthHeaders = async () => {
2932
const { data: { session } } = await supabase.auth.getSession();
@@ -130,6 +133,40 @@ export default function TeamView({ isDark = true }: { isDark?: boolean }) {
130133
}
131134
};
132135

136+
const createInviteLink = async () => {
137+
if (!currentTeamId) {
138+
console.error('No team ID available');
139+
return;
140+
}
141+
142+
try {
143+
const headers = await getAuthHeaders();
144+
const res = await fetch(`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/team-invites`, {
145+
method: 'POST',
146+
headers,
147+
body: JSON.stringify({
148+
team_id: currentTeamId,
149+
days_valid: 7,
150+
}),
151+
});
152+
153+
const result = await res.json();
154+
if (res.ok && result.invite_url) {
155+
setInviteLink(result.invite_url);
156+
}
157+
} catch (err) {
158+
console.error('Failed to create invite link:', err);
159+
}
160+
};
161+
162+
const copyInviteLink = () => {
163+
if (inviteLink) {
164+
navigator.clipboard.writeText(inviteLink);
165+
setCopiedInvite(true);
166+
setTimeout(() => setCopiedInvite(false), 2000);
167+
}
168+
};
169+
133170
return (
134171
<div className="space-y-6 max-w-[1600px] mx-auto pb-10">
135172
{/* Header */}
@@ -223,6 +260,20 @@ export default function TeamView({ isDark = true }: { isDark?: boolean }) {
223260
</>
224261
)}
225262
</button>
263+
264+
<button
265+
type="button"
266+
onClick={createInviteLink}
267+
className={cn(
268+
"py-2.5 px-6 rounded-lg font-medium transition-all flex items-center justify-center gap-2 min-w-[140px]",
269+
isDark
270+
? "bg-purple-600 text-white hover:bg-purple-700"
271+
: "bg-purple-500 text-white hover:bg-purple-600"
272+
)}
273+
>
274+
<LinkIcon className="w-4 h-4" />
275+
<span>Create Link</span>
276+
</button>
226277
</form>
227278
</div>
228279
</div>
@@ -337,6 +388,57 @@ export default function TeamView({ isDark = true }: { isDark?: boolean }) {
337388
</table>
338389
</div>
339390
</div>
391+
392+
{/* Invite Link Modal */}
393+
{inviteLink && (
394+
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60">
395+
<div className={cn(
396+
"rounded-xl p-6 max-w-md",
397+
isDark ? "bg-[#0A0A0A] border border-white/10" : "bg-white border border-black/10"
398+
)}>
399+
<h3 className={cn("text-lg font-semibold mb-2", isDark ? "text-white" : "text-black")}>
400+
Share Invite Link
401+
</h3>
402+
<p className={cn("text-sm mb-4", isDark ? "text-white/60" : "text-gray-600")}>
403+
This link expires in 7 days. Share it with team members to join.
404+
</p>
405+
<div className={cn(
406+
"p-3 rounded-lg mb-4 flex items-center gap-2",
407+
isDark ? "bg-white/5 border border-white/10" : "bg-gray-50 border border-gray-200"
408+
)}>
409+
<input
410+
type="text"
411+
value={inviteLink}
412+
readOnly
413+
className={cn(
414+
"flex-1 bg-transparent text-xs outline-none",
415+
isDark ? "text-white/80" : "text-black"
416+
)}
417+
/>
418+
<button
419+
onClick={copyInviteLink}
420+
className={cn(
421+
"p-2 rounded transition-colors",
422+
copiedInvite
423+
? (isDark ? "bg-green-500/20 text-green-400" : "bg-green-50 text-green-600")
424+
: (isDark ? "hover:bg-white/10 text-white/60" : "hover:bg-gray-100 text-gray-600")
425+
)}
426+
>
427+
{copiedInvite ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
428+
</button>
429+
</div>
430+
<button
431+
onClick={() => setInviteLink(null)}
432+
className={cn(
433+
"w-full py-2 rounded-lg font-medium text-sm",
434+
isDark ? "bg-white text-black hover:bg-white/90" : "bg-black text-white hover:bg-black/90"
435+
)}
436+
>
437+
Done
438+
</button>
439+
</div>
440+
</div>
441+
)}
340442
</div>
341443
);
342444
}

0 commit comments

Comments
 (0)