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-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -3955,6 +3956,16 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"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": {
|
"node_modules/chai": {
|
||||||
"version": "5.3.3",
|
"version": "5.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
|
"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-toggle-group": "^1.1.10",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tanstack/react-query": "^5.83.0",
|
"@tanstack/react-query": "^5.83.0",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.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
|
// Store the token manually since we're not using the normal login flow
|
||||||
localStorage.setItem("secuird_token", result.token);
|
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 refreshUser();
|
||||||
await checkOrgAdmin();
|
await checkOrgAdmin();
|
||||||
navigate("/profile");
|
navigate("/profile");
|
||||||
|
|||||||
@@ -99,11 +99,14 @@ export default function OrgSetupPage() {
|
|||||||
setOrgSlug(value.toLowerCase().replace(/[^a-z0-9-]/g, ""));
|
setOrgSlug(value.toLowerCase().replace(/[^a-z0-9-]/g, ""));
|
||||||
};
|
};
|
||||||
|
|
||||||
const done = async () => {
|
const done = async (orgName?: string) => {
|
||||||
await refreshUser();
|
await refreshUser();
|
||||||
await checkOrgAdmin();
|
await checkOrgAdmin();
|
||||||
queryClient.invalidateQueries({ queryKey: ['organizations'] });
|
queryClient.invalidateQueries({ queryKey: ['organizations'] });
|
||||||
navigate("/profile", { replace: true });
|
if (orgName) {
|
||||||
|
localStorage.setItem('justJoinedOrg', orgName);
|
||||||
|
}
|
||||||
|
navigate('/profile', { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Accept an invite ───────────────────────────────────────────────────────
|
// ── Accept an invite ───────────────────────────────────────────────────────
|
||||||
@@ -113,7 +116,7 @@ export default function OrgSetupPage() {
|
|||||||
try {
|
try {
|
||||||
const result = await api.invites.accept(invite.token);
|
const result = await api.invites.accept(invite.token);
|
||||||
if (result.token) tokenManager.setToken(result.token, result.expires_at ?? null);
|
if (result.token) tokenManager.setToken(result.token, result.expires_at ?? null);
|
||||||
await done();
|
await done(invite.organization.name);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setJoinError(err instanceof ApiError ? err.message : "Failed to join organisation. Please try again.");
|
setJoinError(err instanceof ApiError ? err.message : "Failed to join organisation. Please try again.");
|
||||||
setJoiningToken(null);
|
setJoiningToken(null);
|
||||||
@@ -129,7 +132,7 @@ export default function OrgSetupPage() {
|
|||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
try {
|
try {
|
||||||
await api.organizations.create(orgName.trim(), orgSlug.trim());
|
await api.organizations.create(orgName.trim(), orgSlug.trim());
|
||||||
await done();
|
await done(orgName.trim());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setCreateError(err instanceof ApiError ? err.message : "Failed to create organisation. Please try again.");
|
setCreateError(err instanceof ApiError ? err.message : "Failed to create organisation. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -18,7 +18,21 @@ import {
|
|||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { api, ApiError, PendingInvite } from "@/lib/api";
|
import { api, ApiError, PendingInvite } from "@/lib/api";
|
||||||
import { toast } from "@/hooks/use-toast";
|
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() {
|
function ProfileSkeleton() {
|
||||||
return (
|
return (
|
||||||
@@ -68,6 +82,7 @@ function ProfileSkeleton() {
|
|||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { user, isLoading: authLoading, refreshUser, logout } = useAuth();
|
const { user, isLoading: authLoading, refreshUser, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -79,6 +94,10 @@ export default function ProfilePage() {
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const [confirmEmail, setConfirmEmail] = useState("");
|
const [confirmEmail, setConfirmEmail] = useState("");
|
||||||
|
|
||||||
|
// Celebration dialog state
|
||||||
|
const [showCelebration, setShowCelebration] = useState(false);
|
||||||
|
const [celebrationOrgName, setCelebrationOrgName] = useState("");
|
||||||
|
|
||||||
// Sync local name state with user data
|
// Sync local name state with user data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.full_name) {
|
if (user?.full_name) {
|
||||||
@@ -86,6 +105,17 @@ export default function ProfilePage() {
|
|||||||
}
|
}
|
||||||
}, [user?.full_name]);
|
}, [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
|
// Fetch pending invitations for this user
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
@@ -477,6 +507,25 @@ export default function ProfilePage() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user