feat(org): add celebration confetti when joining or creating organization
Add a celebratory experience when users join or create an organization: - Add canvas-confetti dependency for visual effects - Store organization name in localStorage after successful join/create - Display celebration dialog with confetti animation on ProfilePage - Clear the celebration flag after showing to prevent repeat displays
This commit is contained in:
Generated
+11
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Celebration dialog for org creation/join */}
|
||||
<Dialog open={showCelebration} onOpenChange={setShowCelebration}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2 text-center text-xl">
|
||||
🎉 Congratulations!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-center text-base">
|
||||
You've joined <span className="font-semibold">{celebrationOrgName}</span>!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="flex justify-center py-4">
|
||||
<Button onClick={() => setShowCelebration(false)}>
|
||||
Get Started
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user