diff --git a/package-lock.json b/package-lock.json index 0b9f315..e38f2b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -3955,6 +3956,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", diff --git a/package.json b/package.json index 0dafc92..0feb4a2 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-toggle-group": "^1.1.10", "@radix-ui/react-tooltip": "^1.2.7", "@tanstack/react-query": "^5.83.0", + "canvas-confetti": "^1.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", diff --git a/src/pages/auth/InviteAcceptPage.tsx b/src/pages/auth/InviteAcceptPage.tsx index b4f905f..82e01d1 100644 --- a/src/pages/auth/InviteAcceptPage.tsx +++ b/src/pages/auth/InviteAcceptPage.tsx @@ -63,6 +63,10 @@ export default function InviteAcceptPage() { // Store the token manually since we're not using the normal login flow localStorage.setItem("secuird_token", result.token); } + // Store org name for celebration on ProfilePage + if (inviteData?.organization?.name) { + localStorage.setItem('justJoinedOrg', inviteData.organization.name); + } await refreshUser(); await checkOrgAdmin(); navigate("/profile"); diff --git a/src/pages/auth/OrgSetupPage.tsx b/src/pages/auth/OrgSetupPage.tsx index 1bf3060..c81699e 100644 --- a/src/pages/auth/OrgSetupPage.tsx +++ b/src/pages/auth/OrgSetupPage.tsx @@ -99,11 +99,14 @@ export default function OrgSetupPage() { setOrgSlug(value.toLowerCase().replace(/[^a-z0-9-]/g, "")); }; - const done = async () => { + const done = async (orgName?: string) => { await refreshUser(); await checkOrgAdmin(); queryClient.invalidateQueries({ queryKey: ['organizations'] }); - navigate("/profile", { replace: true }); + if (orgName) { + localStorage.setItem('justJoinedOrg', orgName); + } + navigate('/profile', { replace: true }); }; // ── Accept an invite ─────────────────────────────────────────────────────── @@ -113,7 +116,7 @@ export default function OrgSetupPage() { try { const result = await api.invites.accept(invite.token); if (result.token) tokenManager.setToken(result.token, result.expires_at ?? null); - await done(); + await done(invite.organization.name); } catch (err) { setJoinError(err instanceof ApiError ? err.message : "Failed to join organisation. Please try again."); setJoiningToken(null); @@ -129,7 +132,7 @@ export default function OrgSetupPage() { setIsCreating(true); try { await api.organizations.create(orgName.trim(), orgSlug.trim()); - await done(); + await done(orgName.trim()); } catch (err) { setCreateError(err instanceof ApiError ? err.message : "Failed to create organisation. Please try again."); } finally { diff --git a/src/pages/user/ProfilePage.tsx b/src/pages/user/ProfilePage.tsx index 32884d3..2756082 100644 --- a/src/pages/user/ProfilePage.tsx +++ b/src/pages/user/ProfilePage.tsx @@ -18,7 +18,21 @@ import { import { useAuth } from "@/contexts/AuthContext"; import { api, ApiError, PendingInvite } from "@/lib/api"; import { toast } from "@/hooks/use-toast"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useLocation } from "react-router-dom"; +import confetti from "canvas-confetti"; + +// Wrapper to handle confetti failures gracefully +const fireConfetti = () => { + try { + confetti({ + particleCount: 100, + spread: 70, + origin: { y: 0.6 }, + }); + } catch (e) { + console.warn('Confetti failed:', e); + } +}; function ProfileSkeleton() { return ( @@ -68,6 +82,7 @@ function ProfileSkeleton() { export default function ProfilePage() { const { user, isLoading: authLoading, refreshUser, logout } = useAuth(); const navigate = useNavigate(); + const location = useLocation(); const [name, setName] = useState(""); const [isEditing, setIsEditing] = useState(false); const [isSaving, setIsSaving] = useState(false); @@ -79,6 +94,10 @@ export default function ProfilePage() { const [isDeleting, setIsDeleting] = useState(false); const [confirmEmail, setConfirmEmail] = useState(""); + // Celebration dialog state + const [showCelebration, setShowCelebration] = useState(false); + const [celebrationOrgName, setCelebrationOrgName] = useState(""); + // Sync local name state with user data useEffect(() => { if (user?.full_name) { @@ -86,6 +105,17 @@ export default function ProfilePage() { } }, [user?.full_name]); + // Check for org creation/join celebration + useEffect(() => { + const fromStorage = localStorage.getItem('justJoinedOrg'); + if (fromStorage) { + setCelebrationOrgName(fromStorage); + setShowCelebration(true); + fireConfetti(); + localStorage.removeItem('justJoinedOrg'); + } + }, [location.pathname]); + // Fetch pending invitations for this user useEffect(() => { if (!user) return; @@ -477,6 +507,25 @@ export default function ProfilePage() { + + {/* Celebration dialog for org creation/join */} + + + + + 🎉 Congratulations! + + + You've joined {celebrationOrgName}! + + +
+ +
+
+
); }