feat(web): universe dashboard — meshes + incoming invitations
New /dashboard landing that surfaces meshes and invitations-to-you in one view. Replaces the simple mesh grid at /dashboard (preserved at /dashboard/legacy). Backend additions: - GET /api/my/invites/incoming — pending_invite rows addressed to the authed user's email, joined with invite for role + expiry and user/mesh for display. Unaccepted + unrevoked + unexpired only. - DELETE /api/my/invites/incoming/:id — dismiss a pending invite (revokes the pending_invite row only; underlying invite code stays valid so the inviter can re-send). Web additions (all under apps/web/src/modules/dashboard/universe/): - welcome.tsx — editorial serif header with mesh + invite counts - invitations.tsx — client card with Accept (→ /i/:code claim flow) and optimistic Decline - meshes-grid.tsx — hero card + compact grid, linked to mesh detail - reveal.tsx — fade-up motion matching marketing _reveal.tsx Styling uses the existing claudemesh design tokens (--cm-clay, --cm-bg-elevated, Anthropic Sans/Serif/Mono) — nothing redefined. Onboarding redirect (0 meshes → /meshes/new?onboarding=1) preserved, now gated on 0 invitations too so users with pending invites still land on the dashboard. Sidebar icon switched to Atom for the "universe" concept. Standalone prototype saved at prototypes/live-dashboard.html for reference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -164,6 +164,38 @@ export const archiveMyMesh = async ({
|
||||
return updated;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decline an incoming pending invite addressed to this user's email.
|
||||
* Marks the pending_invite row as revoked so it no longer surfaces
|
||||
* in /invites/incoming. The underlying short-code invite is NOT revoked
|
||||
* (inviter may re-send), only this user's copy is dismissed.
|
||||
*/
|
||||
export const declineIncomingInvite = async ({
|
||||
email,
|
||||
pendingInviteId,
|
||||
}: {
|
||||
email: string;
|
||||
pendingInviteId: string;
|
||||
}) => {
|
||||
const [updated] = await db
|
||||
.update(pendingInvite)
|
||||
.set({ revokedAt: new Date() })
|
||||
.where(
|
||||
and(
|
||||
eq(pendingInvite.id, pendingInviteId),
|
||||
eq(pendingInvite.email, email),
|
||||
isNull(pendingInvite.acceptedAt),
|
||||
isNull(pendingInvite.revokedAt),
|
||||
),
|
||||
)
|
||||
.returning({ id: pendingInvite.id });
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Invitation not found or already resolved.");
|
||||
}
|
||||
return updated;
|
||||
};
|
||||
|
||||
export const leaveMyMesh = async ({
|
||||
userId,
|
||||
meshId,
|
||||
|
||||
Reference in New Issue
Block a user