mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 15:17:59 -05:00
feat(platform): Show "On the waitlist" status for joined users
- Add GET /api/store/waitlist/my-memberships endpoint to fetch user's joined waitlists - Add get_user_waitlist_memberships() db function - Update useWaitlistSection hook to fetch memberships when logged in - Update WaitlistCard to show green "On the waitlist" button for members - Update WaitlistDetailModal to show member status - Add onSuccess callback to JoinWaitlistModal for optimistic UI updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -2016,6 +2016,21 @@ async def get_waitlist() -> list[store_model.StoreWaitlistEntry]:
|
||||
raise DatabaseError("Failed to fetch waitlists") from e
|
||||
|
||||
|
||||
async def get_user_waitlist_memberships(user_id: str) -> list[str]:
|
||||
"""Get all waitlist IDs that a user has joined."""
|
||||
try:
|
||||
user = await prisma.models.User.prisma().find_unique(
|
||||
where={"id": user_id},
|
||||
include={"joinedWaitlists": True},
|
||||
)
|
||||
if not user or not user.joinedWaitlists:
|
||||
return []
|
||||
return [w.id for w in user.joinedWaitlists]
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching user waitlist memberships: {e}")
|
||||
raise DatabaseError("Failed to fetch waitlist memberships") from e
|
||||
|
||||
|
||||
async def add_user_to_waitlist(
|
||||
waitlist_id: str, user_id: str | None, email: str | None
|
||||
) -> store_model.StoreWaitlistEntry:
|
||||
@@ -2342,7 +2357,8 @@ async def link_waitlist_to_listing_admin(
|
||||
|
||||
waitlist = await prisma.models.WaitlistEntry.prisma().update(
|
||||
where={"id": waitlist_id},
|
||||
data={"storeListingId": store_listing_id}, # type: ignore[arg-type]
|
||||
# TODO: fix this properly in prisma by making sure this is setable. surely theres somewhere else we've done something like this
|
||||
data={"storeListingId": store_listing_id},
|
||||
include={"joinedUsers": True},
|
||||
)
|
||||
|
||||
|
||||
@@ -96,6 +96,18 @@ async def get_waitlist():
|
||||
return store_model.StoreWaitlistsAllResponse(listings=waitlists)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/waitlist/my-memberships",
|
||||
summary="Get waitlist IDs the current user has joined",
|
||||
tags=["store", "private"],
|
||||
)
|
||||
async def get_my_waitlist_memberships(
|
||||
user_id: str = fastapi.Security(autogpt_libs.auth.get_user_id),
|
||||
) -> list[str]:
|
||||
"""Returns list of waitlist IDs the authenticated user has joined."""
|
||||
return await store_db.get_user_waitlist_memberships(user_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
path="/waitlist/{waitlist_id}/join",
|
||||
summary="Add self to the agent waitlist",
|
||||
|
||||
@@ -21,11 +21,13 @@ import { Check } from "lucide-react";
|
||||
interface JoinWaitlistModalProps {
|
||||
waitlist: StoreWaitlistEntry;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
}
|
||||
|
||||
export function JoinWaitlistModal({
|
||||
waitlist,
|
||||
onClose,
|
||||
onSuccess,
|
||||
}: JoinWaitlistModalProps) {
|
||||
const { user } = useSupabaseStore();
|
||||
const [email, setEmail] = useState("");
|
||||
@@ -47,6 +49,7 @@ export function JoinWaitlistModal({
|
||||
|
||||
// Close after a short delay to show success state
|
||||
setTimeout(() => {
|
||||
onSuccess?.();
|
||||
onClose();
|
||||
}, 1500);
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
interface WaitlistCardProps {
|
||||
name: string;
|
||||
subHeading: string;
|
||||
description: string;
|
||||
imageUrl: string | null;
|
||||
isMember?: boolean;
|
||||
onCardClick: () => void;
|
||||
onJoinClick: (e: React.MouseEvent) => void;
|
||||
}
|
||||
@@ -17,12 +19,15 @@ export function WaitlistCard({
|
||||
subHeading,
|
||||
description,
|
||||
imageUrl,
|
||||
isMember = false,
|
||||
onCardClick,
|
||||
onJoinClick,
|
||||
}: WaitlistCardProps) {
|
||||
function handleJoinClick(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
onJoinClick(e);
|
||||
if (!isMember) {
|
||||
onJoinClick(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -79,12 +84,22 @@ export function WaitlistCard({
|
||||
|
||||
{/* Join Waitlist Button */}
|
||||
<div className="mt-4 w-full pb-4">
|
||||
<Button
|
||||
onClick={handleJoinClick}
|
||||
className="w-full rounded-full bg-neutral-800 text-white hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Join waitlist
|
||||
</Button>
|
||||
{isMember ? (
|
||||
<Button
|
||||
disabled
|
||||
className="w-full rounded-full bg-green-600 text-white hover:bg-green-600 dark:bg-green-700 dark:hover:bg-green-700"
|
||||
>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
On the waitlist
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={handleJoinClick}
|
||||
className="w-full rounded-full bg-neutral-800 text-white hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Join waitlist
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,16 +9,18 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/__legacy__/ui/dialog";
|
||||
import { StoreWaitlistEntry } from "@/lib/autogpt-server-api/types";
|
||||
import { X } from "lucide-react";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
interface WaitlistDetailModalProps {
|
||||
waitlist: StoreWaitlistEntry;
|
||||
isMember?: boolean;
|
||||
onClose: () => void;
|
||||
onJoin: () => void;
|
||||
}
|
||||
|
||||
export function WaitlistDetailModal({
|
||||
waitlist,
|
||||
isMember = false,
|
||||
onClose,
|
||||
onJoin,
|
||||
}: WaitlistDetailModalProps) {
|
||||
@@ -26,17 +28,7 @@ export function WaitlistDetailModal({
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[700px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-between">
|
||||
<span>{waitlist.name}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onClose}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
<DialogTitle>{waitlist.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
@@ -117,12 +109,22 @@ export function WaitlistDetailModal({
|
||||
)}
|
||||
|
||||
{/* Join Button */}
|
||||
<Button
|
||||
onClick={onJoin}
|
||||
className="w-full rounded-full bg-neutral-800 text-white hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Join waitlist
|
||||
</Button>
|
||||
{isMember ? (
|
||||
<Button
|
||||
disabled
|
||||
className="w-full rounded-full bg-green-600 text-white hover:bg-green-600 dark:bg-green-700 dark:hover:bg-green-700"
|
||||
>
|
||||
<Check className="mr-2 h-4 w-4" />
|
||||
You're on the waitlist
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onJoin}
|
||||
className="w-full rounded-full bg-neutral-800 text-white hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600"
|
||||
>
|
||||
Join waitlist
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -13,7 +13,8 @@ import { StoreWaitlistEntry } from "@/lib/autogpt-server-api/types";
|
||||
import { useWaitlistSection } from "./useWaitlistSection";
|
||||
|
||||
export function WaitlistSection() {
|
||||
const { waitlists, isLoading, hasError } = useWaitlistSection();
|
||||
const { waitlists, joinedWaitlistIds, isLoading, hasError, markAsJoined } =
|
||||
useWaitlistSection();
|
||||
const [selectedWaitlist, setSelectedWaitlist] =
|
||||
useState<StoreWaitlistEntry | null>(null);
|
||||
const [joiningWaitlist, setJoiningWaitlist] =
|
||||
@@ -34,6 +35,11 @@ export function WaitlistSection() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleJoinSuccess(waitlistId: string) {
|
||||
markAsJoined(waitlistId);
|
||||
setJoiningWaitlist(null);
|
||||
}
|
||||
|
||||
// Don't render if loading, error, or no waitlists
|
||||
if (isLoading || hasError || !waitlists || waitlists.length === 0) {
|
||||
return null;
|
||||
@@ -71,6 +77,7 @@ export function WaitlistSection() {
|
||||
subHeading={waitlist.subHeading}
|
||||
description={waitlist.description}
|
||||
imageUrl={waitlist.imageUrls[0] || null}
|
||||
isMember={joinedWaitlistIds.has(waitlist.waitlist_id)}
|
||||
onCardClick={() => handleCardClick(waitlist)}
|
||||
onJoinClick={() => handleJoinClick(waitlist)}
|
||||
/>
|
||||
@@ -88,6 +95,7 @@ export function WaitlistSection() {
|
||||
subHeading={waitlist.subHeading}
|
||||
description={waitlist.description}
|
||||
imageUrl={waitlist.imageUrls[0] || null}
|
||||
isMember={joinedWaitlistIds.has(waitlist.waitlist_id)}
|
||||
onCardClick={() => handleCardClick(waitlist)}
|
||||
onJoinClick={() => handleJoinClick(waitlist)}
|
||||
/>
|
||||
@@ -99,6 +107,7 @@ export function WaitlistSection() {
|
||||
{selectedWaitlist && (
|
||||
<WaitlistDetailModal
|
||||
waitlist={selectedWaitlist}
|
||||
isMember={joinedWaitlistIds.has(selectedWaitlist.waitlist_id)}
|
||||
onClose={() => setSelectedWaitlist(null)}
|
||||
onJoin={handleJoinFromDetail}
|
||||
/>
|
||||
@@ -109,6 +118,7 @@ export function WaitlistSection() {
|
||||
<JoinWaitlistModal
|
||||
waitlist={joiningWaitlist}
|
||||
onClose={() => setJoiningWaitlist(null)}
|
||||
onSuccess={() => handleJoinSuccess(joiningWaitlist.waitlist_id)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,18 +3,36 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import BackendAPI from "@/lib/autogpt-server-api/client";
|
||||
import { StoreWaitlistEntry } from "@/lib/autogpt-server-api/types";
|
||||
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
|
||||
|
||||
export function useWaitlistSection() {
|
||||
const { user } = useSupabaseStore();
|
||||
const [waitlists, setWaitlists] = useState<StoreWaitlistEntry[]>([]);
|
||||
const [joinedWaitlistIds, setJoinedWaitlistIds] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchWaitlists() {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const api = new BackendAPI();
|
||||
|
||||
// Fetch waitlists
|
||||
const response = await api.getWaitlists();
|
||||
setWaitlists(response.listings);
|
||||
|
||||
// Fetch memberships if logged in
|
||||
if (user) {
|
||||
try {
|
||||
const memberships = await api.getMyWaitlistMemberships();
|
||||
setJoinedWaitlistIds(new Set(memberships));
|
||||
} catch (error) {
|
||||
// Don't fail the whole component if membership fetch fails
|
||||
console.error("Error fetching waitlist memberships:", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching waitlists:", error);
|
||||
setHasError(true);
|
||||
@@ -23,8 +41,13 @@ export function useWaitlistSection() {
|
||||
}
|
||||
}
|
||||
|
||||
fetchWaitlists();
|
||||
}, []);
|
||||
fetchData();
|
||||
}, [user]);
|
||||
|
||||
return { waitlists, isLoading, hasError };
|
||||
// Function to add a waitlist ID to joined set (called after successful join)
|
||||
function markAsJoined(waitlistId: string) {
|
||||
setJoinedWaitlistIds((prev) => new Set([...prev, waitlistId]));
|
||||
}
|
||||
|
||||
return { waitlists, joinedWaitlistIds, isLoading, hasError, markAsJoined };
|
||||
}
|
||||
|
||||
@@ -6064,6 +6064,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/store/waitlist/my-memberships": {
|
||||
"get": {
|
||||
"tags": ["v2", "store", "private"],
|
||||
"summary": "Get waitlist IDs the current user has joined",
|
||||
"description": "Returns list of waitlist IDs the authenticated user has joined.",
|
||||
"operationId": "getV2Get waitlist ids the current user has joined",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": { "type": "string" },
|
||||
"type": "array",
|
||||
"title": "Response Getv2Get Waitlist Ids The Current User Has Joined"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
}
|
||||
},
|
||||
"security": [{ "HTTPBearerJWT": [] }]
|
||||
}
|
||||
},
|
||||
"/api/store/waitlist/{waitlist_id}/join": {
|
||||
"post": {
|
||||
"tags": ["v2", "store", "public"],
|
||||
@@ -6088,9 +6114,7 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"description": "Email address for unauthenticated users",
|
||||
"title": "Email"
|
||||
"$ref": "#/components/schemas/Body_postV2Add_self_to_the_agent_waitlist"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6862,6 +6886,17 @@
|
||||
"required": ["store_listing_version_id"],
|
||||
"title": "Body_postV2Add marketplace agent"
|
||||
},
|
||||
"Body_postV2Add_self_to_the_agent_waitlist": {
|
||||
"properties": {
|
||||
"email": {
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Email",
|
||||
"description": "Email address for unauthenticated users"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"title": "Body_postV2Add self to the agent waitlist"
|
||||
},
|
||||
"Body_postV2Execute_a_preset": {
|
||||
"properties": {
|
||||
"inputs": {
|
||||
@@ -12223,7 +12258,7 @@
|
||||
"anyOf": [{ "type": "string" }, { "type": "null" }],
|
||||
"title": "Agentoutputdemourl"
|
||||
},
|
||||
"status": { "type": "string", "title": "Status" },
|
||||
"status": { "$ref": "#/components/schemas/WaitlistExternalStatus" },
|
||||
"votes": { "type": "integer", "title": "Votes" },
|
||||
"signupCount": { "type": "integer", "title": "Signupcount" },
|
||||
"storeListingId": {
|
||||
@@ -12283,6 +12318,11 @@
|
||||
"title": "WaitlistCreateRequest",
|
||||
"description": "Request model for creating a new waitlist."
|
||||
},
|
||||
"WaitlistExternalStatus": {
|
||||
"type": "string",
|
||||
"enum": ["DONE", "NOT_STARTED", "CANCELED", "WORK_IN_PROGRESS"],
|
||||
"title": "WaitlistExternalStatus"
|
||||
},
|
||||
"WaitlistSignup": {
|
||||
"properties": {
|
||||
"type": { "type": "string", "title": "Type" },
|
||||
|
||||
@@ -680,6 +680,10 @@ export default class BackendAPI {
|
||||
});
|
||||
}
|
||||
|
||||
getMyWaitlistMemberships(): Promise<string[]> {
|
||||
return this._get("/store/waitlist/my-memberships");
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
//////////// V2 LIBRARY API ////////////
|
||||
////////////////////////////////////////
|
||||
|
||||
Reference in New Issue
Block a user