mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
refactor(frontend): migrate waitlist admin components to generated API hooks
- Convert WaitlistTable to use generated React Query hooks directly - Convert CreateWaitlistButton to use generated hooks - Update WaitlistDetailModal to use generated types and design system Dialog - Remove deprecated waitlist types from types.ts - Remove deprecated waitlist methods from BackendAPI client - Delete actions.ts server actions (no longer needed) - Replace lucide-react icons with Phosphor icons Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,68 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import type {
|
||||
WaitlistAdminResponse,
|
||||
WaitlistAdminListResponse,
|
||||
WaitlistSignupListResponse,
|
||||
WaitlistCreateRequest,
|
||||
WaitlistUpdateRequest,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
|
||||
export async function getWaitlistsAdmin(): Promise<WaitlistAdminListResponse> {
|
||||
const api = new BackendAPI();
|
||||
const response = await api.getWaitlistsAdmin();
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function getWaitlistAdmin(
|
||||
waitlistId: string,
|
||||
): Promise<WaitlistAdminResponse> {
|
||||
const api = new BackendAPI();
|
||||
const response = await api.getWaitlistAdmin(waitlistId);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function createWaitlist(
|
||||
data: WaitlistCreateRequest,
|
||||
): Promise<WaitlistAdminResponse> {
|
||||
const api = new BackendAPI();
|
||||
const response = await api.createWaitlist(data);
|
||||
revalidatePath("/admin/waitlist");
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function updateWaitlist(
|
||||
waitlistId: string,
|
||||
data: WaitlistUpdateRequest,
|
||||
): Promise<WaitlistAdminResponse> {
|
||||
const api = new BackendAPI();
|
||||
const response = await api.updateWaitlist(waitlistId, data);
|
||||
revalidatePath("/admin/waitlist");
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function deleteWaitlist(waitlistId: string): Promise<void> {
|
||||
const api = new BackendAPI();
|
||||
await api.deleteWaitlist(waitlistId);
|
||||
revalidatePath("/admin/waitlist");
|
||||
}
|
||||
|
||||
export async function getWaitlistSignups(
|
||||
waitlistId: string,
|
||||
): Promise<WaitlistSignupListResponse> {
|
||||
const api = new BackendAPI();
|
||||
const response = await api.getWaitlistSignups(waitlistId);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function linkWaitlistToListing(
|
||||
waitlistId: string,
|
||||
storeListingId: string,
|
||||
): Promise<WaitlistAdminResponse> {
|
||||
const api = new BackendAPI();
|
||||
const response = await api.linkWaitlistToListing(waitlistId, storeListingId);
|
||||
revalidatePath("/admin/waitlist");
|
||||
return response;
|
||||
}
|
||||
@@ -1,29 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Input } from "@/components/atoms/Input/Input";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/__legacy__/ui/dialog";
|
||||
import { Input } from "@/components/__legacy__/ui/input";
|
||||
import { Label } from "@/components/__legacy__/ui/label";
|
||||
import { Textarea } from "@/components/__legacy__/ui/textarea";
|
||||
import { createWaitlist } from "../actions";
|
||||
usePostV2CreateWaitlist,
|
||||
getGetV2ListAllWaitlistsQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/admin/admin";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Plus } from "lucide-react";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
|
||||
export function CreateWaitlistButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const createWaitlistMutation = usePostV2CreateWaitlist({
|
||||
mutation: {
|
||||
onSuccess: (response) => {
|
||||
if (response.status === 200) {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Waitlist created successfully",
|
||||
});
|
||||
setOpen(false);
|
||||
setFormData({
|
||||
name: "",
|
||||
slug: "",
|
||||
subHeading: "",
|
||||
description: "",
|
||||
categories: "",
|
||||
imageUrls: "",
|
||||
videoUrl: "",
|
||||
agentOutputDemoUrl: "",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListAllWaitlistsQueryKey(),
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to create waitlist",
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error creating waitlist:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to create waitlist",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
@@ -36,12 +69,10 @@ export function CreateWaitlistButton() {
|
||||
agentOutputDemoUrl: "",
|
||||
});
|
||||
|
||||
function handleChange(
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) {
|
||||
function handleInputChange(id: string, value: string) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[e.target.name]: e.target.value,
|
||||
[id]: value,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -52,12 +83,11 @@ export function CreateWaitlistButton() {
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
await createWaitlist({
|
||||
createWaitlistMutation.mutate({
|
||||
data: {
|
||||
name: formData.name,
|
||||
slug: formData.slug || generateSlug(formData.name),
|
||||
subHeading: formData.subHeading,
|
||||
@@ -70,164 +100,118 @@ export function CreateWaitlistButton() {
|
||||
: [],
|
||||
videoUrl: formData.videoUrl || null,
|
||||
agentOutputDemoUrl: formData.agentOutputDemoUrl || null,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Waitlist created successfully",
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
setFormData({
|
||||
name: "",
|
||||
slug: "",
|
||||
subHeading: "",
|
||||
description: "",
|
||||
categories: "",
|
||||
imageUrls: "",
|
||||
videoUrl: "",
|
||||
agentOutputDemoUrl: "",
|
||||
});
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error("Error creating waitlist:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to create waitlist",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Create Waitlist
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Waitlist</DialogTitle>
|
||||
<DialogDescription>
|
||||
<>
|
||||
<Button onClick={() => setOpen(true)}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Create Waitlist
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
title="Create New Waitlist"
|
||||
controlled={{
|
||||
isOpen: open,
|
||||
set: async (isOpen) => setOpen(isOpen),
|
||||
}}
|
||||
onClose={() => setOpen(false)}
|
||||
styling={{ maxWidth: "600px" }}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<p className="mb-4 text-sm text-zinc-500">
|
||||
Create a new waitlist for an upcoming agent. Users can sign up to be
|
||||
notified when it launches.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="name">Name *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
placeholder="SEO Analysis Agent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</p>
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
label="Name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||
placeholder="SEO Analysis Agent"
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="slug">Slug</Label>
|
||||
<Input
|
||||
id="slug"
|
||||
name="slug"
|
||||
value={formData.slug}
|
||||
onChange={handleChange}
|
||||
placeholder="seo-analysis-agent (auto-generated if empty)"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="slug"
|
||||
label="Slug"
|
||||
value={formData.slug}
|
||||
onChange={(e) => handleInputChange("slug", e.target.value)}
|
||||
placeholder="seo-analysis-agent (auto-generated if empty)"
|
||||
/>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="subHeading">Subheading *</Label>
|
||||
<Input
|
||||
id="subHeading"
|
||||
name="subHeading"
|
||||
value={formData.subHeading}
|
||||
onChange={handleChange}
|
||||
placeholder="Analyze your website's SEO in minutes"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="subHeading"
|
||||
label="Subheading"
|
||||
value={formData.subHeading}
|
||||
onChange={(e) => handleInputChange("subHeading", e.target.value)}
|
||||
placeholder="Analyze your website's SEO in minutes"
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="description">Description *</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
name="description"
|
||||
value={formData.description}
|
||||
onChange={handleChange}
|
||||
placeholder="Detailed description of what this agent does..."
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="description"
|
||||
label="Description"
|
||||
type="textarea"
|
||||
value={formData.description}
|
||||
onChange={(e) => handleInputChange("description", e.target.value)}
|
||||
placeholder="Detailed description of what this agent does..."
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="categories">Categories (comma-separated)</Label>
|
||||
<Input
|
||||
id="categories"
|
||||
name="categories"
|
||||
value={formData.categories}
|
||||
onChange={handleChange}
|
||||
placeholder="SEO, Marketing, Analysis"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="categories"
|
||||
label="Categories (comma-separated)"
|
||||
value={formData.categories}
|
||||
onChange={(e) => handleInputChange("categories", e.target.value)}
|
||||
placeholder="SEO, Marketing, Analysis"
|
||||
/>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="imageUrls">Image URLs (comma-separated)</Label>
|
||||
<Input
|
||||
id="imageUrls"
|
||||
name="imageUrls"
|
||||
value={formData.imageUrls}
|
||||
onChange={handleChange}
|
||||
placeholder="https://example.com/image1.jpg, https://example.com/image2.jpg"
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="imageUrls"
|
||||
label="Image URLs (comma-separated)"
|
||||
value={formData.imageUrls}
|
||||
onChange={(e) => handleInputChange("imageUrls", e.target.value)}
|
||||
placeholder="https://example.com/image1.jpg, https://example.com/image2.jpg"
|
||||
/>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="videoUrl">Video URL (optional)</Label>
|
||||
<Input
|
||||
id="videoUrl"
|
||||
name="videoUrl"
|
||||
value={formData.videoUrl}
|
||||
onChange={handleChange}
|
||||
placeholder="https://youtube.com/watch?v=..."
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
id="videoUrl"
|
||||
label="Video URL (optional)"
|
||||
value={formData.videoUrl}
|
||||
onChange={(e) => handleInputChange("videoUrl", e.target.value)}
|
||||
placeholder="https://youtube.com/watch?v=..."
|
||||
/>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="agentOutputDemoUrl">
|
||||
Output Demo URL (optional)
|
||||
</Label>
|
||||
<Input
|
||||
id="agentOutputDemoUrl"
|
||||
name="agentOutputDemoUrl"
|
||||
value={formData.agentOutputDemoUrl}
|
||||
onChange={handleChange}
|
||||
placeholder="https://example.com/demo-output.mp4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
id="agentOutputDemoUrl"
|
||||
label="Output Demo URL (optional)"
|
||||
value={formData.agentOutputDemoUrl}
|
||||
onChange={(e) =>
|
||||
handleInputChange("agentOutputDemoUrl", e.target.value)
|
||||
}
|
||||
placeholder="https://example.com/demo-output.mp4"
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
Create Waitlist
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog.Footer>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" loading={createWaitlistMutation.isPending}>
|
||||
Create Waitlist
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -10,59 +11,58 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/__legacy__/ui/table";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { getWaitlistsAdmin, deleteWaitlist } from "../actions";
|
||||
import type { WaitlistAdminResponse } from "@/lib/autogpt-server-api/types";
|
||||
import {
|
||||
useGetV2ListAllWaitlists,
|
||||
useDeleteV2DeleteWaitlist,
|
||||
getGetV2ListAllWaitlistsQueryKey,
|
||||
} from "@/app/api/__generated__/endpoints/admin/admin";
|
||||
import type { WaitlistAdminResponse } from "@/app/api/__generated__/models/waitlistAdminResponse";
|
||||
import { EditWaitlistDialog } from "./EditWaitlistDialog";
|
||||
import { WaitlistSignupsDialog } from "./WaitlistSignupsDialog";
|
||||
import { Trash, PencilSimple, Users, Link } from "@phosphor-icons/react";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
|
||||
export function WaitlistTable() {
|
||||
const [waitlists, setWaitlists] = useState<WaitlistAdminResponse[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingWaitlist, setEditingWaitlist] =
|
||||
useState<WaitlistAdminResponse | null>(null);
|
||||
const [viewingSignups, setViewingSignups] = useState<string | null>(null);
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
async function loadWaitlists() {
|
||||
try {
|
||||
const response = await getWaitlistsAdmin();
|
||||
setWaitlists(response.waitlists);
|
||||
} catch (error) {
|
||||
console.error("Error loading waitlists:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to load waitlists",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const { data: response, isLoading, error } = useGetV2ListAllWaitlists();
|
||||
|
||||
const deleteWaitlistMutation = useDeleteV2DeleteWaitlist({
|
||||
mutation: {
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Waitlist deleted successfully",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListAllWaitlistsQueryKey(),
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Error deleting waitlist:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to delete waitlist",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function handleDelete(waitlistId: string) {
|
||||
if (!confirm("Are you sure you want to delete this waitlist?")) return;
|
||||
deleteWaitlistMutation.mutate({ waitlistId });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadWaitlists();
|
||||
}, []);
|
||||
|
||||
async function handleDelete(waitlistId: string) {
|
||||
if (!confirm("Are you sure you want to delete this waitlist?")) return;
|
||||
|
||||
try {
|
||||
await deleteWaitlist(waitlistId);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Waitlist deleted successfully",
|
||||
});
|
||||
loadWaitlists();
|
||||
} catch (error) {
|
||||
console.error("Error deleting waitlist:", error);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: "Failed to delete waitlist",
|
||||
});
|
||||
}
|
||||
function handleWaitlistSaved() {
|
||||
setEditingWaitlist(null);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2ListAllWaitlistsQueryKey(),
|
||||
});
|
||||
}
|
||||
|
||||
function formatStatus(status: string) {
|
||||
@@ -91,10 +91,20 @@ export function WaitlistTable() {
|
||||
}).format(new Date(dateStr));
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
if (isLoading) {
|
||||
return <div className="py-10 text-center">Loading waitlists...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="py-10 text-center text-red-500">
|
||||
Error loading waitlists. Please try again.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const waitlists = response?.status === 200 ? response.data.waitlists : [];
|
||||
|
||||
if (waitlists.length === 0) {
|
||||
return (
|
||||
<div className="py-10 text-center text-gray-500">
|
||||
@@ -165,6 +175,7 @@ export function WaitlistTable() {
|
||||
size="small"
|
||||
onClick={() => handleDelete(waitlist.id)}
|
||||
title="Delete"
|
||||
disabled={deleteWaitlistMutation.isPending}
|
||||
>
|
||||
<Trash size={16} className="text-red-500" />
|
||||
</Button>
|
||||
@@ -180,10 +191,7 @@ export function WaitlistTable() {
|
||||
<EditWaitlistDialog
|
||||
waitlist={editingWaitlist}
|
||||
onClose={() => setEditingWaitlist(null)}
|
||||
onSave={() => {
|
||||
setEditingWaitlist(null);
|
||||
loadWaitlists();
|
||||
}}
|
||||
onSave={handleWaitlistSaved}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,14 +2,9 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/__legacy__/ui/dialog";
|
||||
import { StoreWaitlistEntry } from "@/lib/autogpt-server-api/types";
|
||||
import { Check } from "lucide-react";
|
||||
import { Dialog } from "@/components/molecules/Dialog/Dialog";
|
||||
import type { StoreWaitlistEntry } from "@/app/api/__generated__/models/storeWaitlistEntry";
|
||||
import { Check } from "@phosphor-icons/react";
|
||||
|
||||
interface WaitlistDetailModalProps {
|
||||
waitlist: StoreWaitlistEntry;
|
||||
@@ -25,12 +20,18 @@ export function WaitlistDetailModal({
|
||||
onJoin,
|
||||
}: WaitlistDetailModalProps) {
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[700px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{waitlist.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Dialog
|
||||
title={waitlist.name}
|
||||
controlled={{
|
||||
isOpen: true,
|
||||
set: async (open) => {
|
||||
if (!open) onClose();
|
||||
},
|
||||
}}
|
||||
onClose={onClose}
|
||||
styling={{ maxWidth: "700px" }}
|
||||
>
|
||||
<Dialog.Content>
|
||||
<div className="space-y-6">
|
||||
{/* Main Image */}
|
||||
{waitlist.imageUrls.length > 0 && (
|
||||
@@ -109,24 +110,26 @@ export function WaitlistDetailModal({
|
||||
)}
|
||||
|
||||
{/* Join 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>
|
||||
)}
|
||||
<Dialog.Footer>
|
||||
{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 size={16} className="mr-2" />
|
||||
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>
|
||||
)}
|
||||
</Dialog.Footer>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog.Content>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -67,13 +67,6 @@ import type {
|
||||
User,
|
||||
UserPasswordCredentials,
|
||||
UsersBalanceHistoryResponse,
|
||||
StoreWaitlistEntry,
|
||||
StoreWaitlistsAllResponse,
|
||||
WaitlistAdminListResponse,
|
||||
WaitlistAdminResponse,
|
||||
WaitlistCreateRequest,
|
||||
WaitlistSignupListResponse,
|
||||
WaitlistUpdateRequest,
|
||||
WebSocketNotification,
|
||||
} from "./types";
|
||||
|
||||
@@ -623,67 +616,6 @@ export default class BackendAPI {
|
||||
return this._get(url);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
///////// Waitlist Admin API ////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
getWaitlistsAdmin(): Promise<WaitlistAdminListResponse> {
|
||||
return this._get("/store/admin/waitlist");
|
||||
}
|
||||
|
||||
getWaitlistAdmin(waitlistId: string): Promise<WaitlistAdminResponse> {
|
||||
return this._get(`/store/admin/waitlist/${waitlistId}`);
|
||||
}
|
||||
|
||||
createWaitlist(data: WaitlistCreateRequest): Promise<WaitlistAdminResponse> {
|
||||
return this._request("POST", "/store/admin/waitlist", data);
|
||||
}
|
||||
|
||||
updateWaitlist(
|
||||
waitlistId: string,
|
||||
data: WaitlistUpdateRequest,
|
||||
): Promise<WaitlistAdminResponse> {
|
||||
return this._request("PUT", `/store/admin/waitlist/${waitlistId}`, data);
|
||||
}
|
||||
|
||||
deleteWaitlist(waitlistId: string): Promise<void> {
|
||||
return this._request("DELETE", `/store/admin/waitlist/${waitlistId}`);
|
||||
}
|
||||
|
||||
getWaitlistSignups(waitlistId: string): Promise<WaitlistSignupListResponse> {
|
||||
return this._get(`/store/admin/waitlist/${waitlistId}/signups`);
|
||||
}
|
||||
|
||||
linkWaitlistToListing(
|
||||
waitlistId: string,
|
||||
storeListingId: string,
|
||||
): Promise<WaitlistAdminResponse> {
|
||||
return this._request("POST", `/store/admin/waitlist/${waitlistId}/link`, {
|
||||
store_listing_id: storeListingId,
|
||||
});
|
||||
}
|
||||
|
||||
/////////////////////////////////////////
|
||||
///////// Public Waitlist API ///////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
getWaitlists(): Promise<StoreWaitlistsAllResponse> {
|
||||
return this._get("/store/waitlist");
|
||||
}
|
||||
|
||||
joinWaitlist(
|
||||
waitlistId: string,
|
||||
email?: string,
|
||||
): Promise<StoreWaitlistEntry> {
|
||||
return this._request("POST", `/store/waitlist/${waitlistId}/join`, {
|
||||
email: email || null,
|
||||
});
|
||||
}
|
||||
|
||||
getMyWaitlistMemberships(): Promise<string[]> {
|
||||
return this._get("/store/waitlist/my-memberships");
|
||||
}
|
||||
|
||||
////////////////////////////////////////
|
||||
//////////// V2 LIBRARY API ////////////
|
||||
////////////////////////////////////////
|
||||
|
||||
@@ -1103,85 +1103,6 @@ export type AddUserCreditsResponse = {
|
||||
transaction_key: string;
|
||||
};
|
||||
|
||||
// Waitlist Admin Types
|
||||
export type WaitlistAdminResponse = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
subHeading: string;
|
||||
description: string;
|
||||
categories: string[];
|
||||
imageUrls: string[];
|
||||
videoUrl: string | null;
|
||||
agentOutputDemoUrl: string | null;
|
||||
status: string;
|
||||
votes: number;
|
||||
signupCount: number;
|
||||
storeListingId: string | null;
|
||||
owningUserId: string;
|
||||
};
|
||||
|
||||
export type WaitlistAdminListResponse = {
|
||||
waitlists: WaitlistAdminResponse[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export type WaitlistSignup = {
|
||||
type: "user" | "email";
|
||||
userId: string | null;
|
||||
email: string | null;
|
||||
username: string | null;
|
||||
};
|
||||
|
||||
export type WaitlistSignupListResponse = {
|
||||
waitlistId: string;
|
||||
signups: WaitlistSignup[];
|
||||
totalCount: number;
|
||||
};
|
||||
|
||||
export type WaitlistCreateRequest = {
|
||||
name: string;
|
||||
slug: string;
|
||||
subHeading: string;
|
||||
description: string;
|
||||
categories?: string[];
|
||||
imageUrls?: string[];
|
||||
videoUrl?: string | null;
|
||||
agentOutputDemoUrl?: string | null;
|
||||
};
|
||||
|
||||
export type WaitlistUpdateRequest = {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
subHeading?: string;
|
||||
description?: string;
|
||||
categories?: string[];
|
||||
imageUrls?: string[];
|
||||
videoUrl?: string | null;
|
||||
agentOutputDemoUrl?: string | null;
|
||||
status?: string;
|
||||
storeListingId?: string | null;
|
||||
};
|
||||
|
||||
// Public Waitlist Types
|
||||
export type StoreWaitlistEntry = {
|
||||
waitlistId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
subHeading: string;
|
||||
description: string;
|
||||
categories: string[];
|
||||
imageUrls: string[];
|
||||
videoUrl: string | null;
|
||||
agentOutputDemoUrl: string | null;
|
||||
};
|
||||
|
||||
export type StoreWaitlistsAllResponse = {
|
||||
listings: StoreWaitlistEntry[];
|
||||
};
|
||||
|
||||
const _stringFormatToDataTypeMap: Partial<Record<string, DataType>> = {
|
||||
date: DataType.DATE,
|
||||
time: DataType.TIME,
|
||||
|
||||
Reference in New Issue
Block a user