diff --git a/src/app/s/[slug]/join/page.tsx b/src/app/s/[slug]/join/page.tsx
index 0b21935..bbe9680 100644
--- a/src/app/s/[slug]/join/page.tsx
+++ b/src/app/s/[slug]/join/page.tsx
@@ -1,52 +1,76 @@
"use client";
import { useState, useEffect } from "react";
-import { useParams, useSearchParams, useRouter } from "next/navigation";
+import { useParams, useSearchParams } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { toast } from "sonner";
-import { Users, LogIn } from "lucide-react";
+import { Users, LogIn, CheckCircle2, AlertCircle, Clock, UserCheck } from "lucide-react";
import Link from "next/link";
+interface InviteInfo {
+ space: { name: string; slug: string; description: string | null; startingCredits?: number };
+ expired: boolean;
+ maxedOut: boolean;
+ valid: boolean;
+}
+
+type PageState =
+ | { type: "loading" }
+ | { type: "error"; message: string; icon: typeof AlertCircle }
+ | { type: "invite"; info: InviteInfo }
+ | { type: "joined"; spaceName: string; startingCredits: number }
+ | { type: "already_member"; spaceName: string };
+
export default function JoinSpacePage() {
const params = useParams();
const searchParams = useSearchParams();
- const router = useRouter();
const slug = params.slug as string;
const token = searchParams.get("token");
- const [inviteInfo, setInviteInfo] = useState<{ spaceName: string; uses: number; maxUses?: number } | null>(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState("");
+ const [state, setState] = useState
({ type: "loading" });
+ const [joining, setJoining] = useState(false);
useEffect(() => {
- if (token) {
- fetch(`/api/spaces/join/${token}`)
- .then((res) => res.json())
- .then((data) => {
- if (data.error) {
- setError(data.error);
- } else {
- setInviteInfo(data);
- }
- })
- .catch(() => setError("Failed to load invite"));
- } else {
- setError("No invite token provided");
+ if (!token) {
+ setState({ type: "error", message: "No invite token provided.", icon: AlertCircle });
+ return;
}
+
+ fetch(`/api/spaces/join/${token}`)
+ .then((res) => res.json())
+ .then((data) => {
+ if (data.error) {
+ setState({ type: "error", message: data.error, icon: AlertCircle });
+ } else if (data.expired) {
+ setState({ type: "error", message: "This invite link has expired.", icon: Clock });
+ } else if (data.maxedOut) {
+ setState({ type: "error", message: "This invite link has reached its maximum number of uses.", icon: Users });
+ } else if (data.valid) {
+ setState({ type: "invite", info: data });
+ } else {
+ setState({ type: "error", message: "This invite link is not valid.", icon: AlertCircle });
+ }
+ })
+ .catch(() => setState({ type: "error", message: "Failed to load invite. Please try again.", icon: AlertCircle }));
}, [token]);
async function acceptInvite() {
if (!token) return;
- setLoading(true);
+ setJoining(true);
try {
const res = await fetch(`/api/spaces/join/${token}`, { method: "POST" });
const data = await res.json();
- if (res.ok) {
- toast.success("Welcome to the space!");
- router.push("/");
+ if (res.ok && data.alreadyMember) {
+ setState({ type: "already_member", spaceName: data.space.name });
+ } else if (res.ok) {
+ setState({
+ type: "joined",
+ spaceName: data.space.name,
+ startingCredits: data.space.startingCredits ?? 0,
+ });
} else if (res.status === 401) {
- // Not logged in - redirect to signin with invite token preserved
+ // Not logged in — redirect to signin with invite URL preserved
const rootDomain = process.env.NEXT_PUBLIC_ROOT_DOMAIN || "rvote.online";
const protocol = window.location.protocol;
window.location.href = `${protocol}//${rootDomain}/auth/signin?callbackUrl=${encodeURIComponent(window.location.href)}`;
@@ -54,52 +78,94 @@ export default function JoinSpacePage() {
toast.error(data.error || "Failed to join space");
}
} finally {
- setLoading(false);
+ setJoining(false);
}
}
- if (error) {
- return (
-
+ return (
+
+ {state.type === "loading" && (
+
+
+
+
+
+ )}
+
+ {state.type === "error" && (
- {error}
+
+
+
+ {state.message}
-
- );
- }
+ )}
- if (!inviteInfo) {
- return (
-
- Loading invite...
-
- );
- }
+ {state.type === "invite" && (
+
+
+
+
+
+ Join {state.info.space.name}
+
+ You've been invited to join this space.
+
+ {state.info.space.description && (
+ {state.info.space.description}
+ )}
+
+
+
+
+
+ )}
- return (
-
-
-
-
-
-
- Join {inviteInfo.spaceName}
-
- You've been invited to join this space.
-
-
-
-
-
-
+ {state.type === "joined" && (
+
+
+
+
+
+ Welcome to {state.spaceName}!
+
+ You've been given {state.startingCredits} credits to start voting on proposals.
+
+
+
+
+ )}
+
+ {state.type === "already_member" && (
+
+
+
+
+
+ Already a member
+
+ You're already a member of {state.spaceName}.
+
+
+
+
+ )}
);
}
diff --git a/src/app/s/[slug]/layout.tsx b/src/app/s/[slug]/layout.tsx
index ea60688..d579fbc 100644
--- a/src/app/s/[slug]/layout.tsx
+++ b/src/app/s/[slug]/layout.tsx
@@ -52,14 +52,14 @@ export default async function SpaceLayout({
>
-
{space.name}
+
{space.name}
{space.description && (
{space.description}
)}
-
+
{children}
diff --git a/src/app/s/[slug]/members/page.tsx b/src/app/s/[slug]/members/page.tsx
index 2822733..127bab9 100644
--- a/src/app/s/[slug]/members/page.tsx
+++ b/src/app/s/[slug]/members/page.tsx
@@ -3,6 +3,7 @@ import { prisma } from "@/lib/prisma";
import { notFound, redirect } from "next/navigation";
import { MemberList } from "@/components/MemberList";
import { InviteDialog } from "@/components/InviteDialog";
+import { InviteList } from "@/components/InviteList";
import { Badge } from "@/components/ui/badge";
export default async function SpaceMembersPage({
@@ -38,7 +39,7 @@ export default async function SpaceMembersPage({
return (
-
+
Members
@@ -54,6 +55,13 @@ export default async function SpaceMembersPage({
isAdmin={membership.role === "ADMIN"}
currentUserId={session.user.id}
/>
+
+ {membership.role === "ADMIN" && (
+
+
Active Invites
+
+
+ )}
);
}
diff --git a/src/app/s/[slug]/page.tsx b/src/app/s/[slug]/page.tsx
index f5cb063..61e1ce0 100644
--- a/src/app/s/[slug]/page.tsx
+++ b/src/app/s/[slug]/page.tsx
@@ -36,25 +36,25 @@ export default async function SpaceDashboard({
- {rankingCount}
+ {rankingCount}
Being Ranked
- {votingCount}
+ {votingCount}
In Voting
- {passedCount}
+ {passedCount}
Passed
- {memberCount}
+ {memberCount}
Members
@@ -62,7 +62,7 @@ export default async function SpaceDashboard({
{/* Top proposals */}
-
+
Top Proposals