Submission Workflow
This guide walks through the entire item submission lifecycle -- from the user filling out the form, through API validation, admin review, and final publication.
Workflow Overview
User fills form --> POST /api/client/items --> Item stored as PENDING
|
Admin reviews item
|
POST /api/admin/items/[id]/review
|
Approved? --+--> Published
|
Rejected (with reason)
Submission Statuses
Items move through a defined set of statuses, declared in lib/constants/payment.ts:
export enum SubmissionStatus {
DRAFT = 'draft',
PENDING = 'pending',
APPROVED = 'approved',
REJECTED = 'rejected',
PUBLISHED = 'published',
ARCHIVED = 'archived',
}
| Status | Meaning |
|---|---|
draft | Saved by user but not yet submitted |
pending | Submitted and waiting for admin review |
approved | Passed review, ready for publication |
rejected | Declined by admin (reason stored) |
published | Live and visible on the directory |
archived | Removed from public view but still in the database |
Payment Flows
Submissions can follow one of two payment flows configured in lib/config/payment-flows.ts:
Pay at Start
{
id: PaymentFlow.PAY_AT_START,
title: "Pay First",
subtitle: "Instant Publication",
description: "Pay upfront and get published immediately",
features: [
"Immediate payment required",
"Instant publication",
"No review delays",
"Guaranteed listing"
],
}
The user pays before submitting details. Once payment is confirmed the item is published immediately without admin review.
Pay at End (Default)
{
id: PaymentFlow.PAY_AT_END,
title: "Pay Later",
subtitle: "Review First",
description: "Submit details first, pay after approval",
features: [
"Submit without payment",
"Review before payment",
"Pay only if approved",
"Save your work"
],
isDefault: true,
}
The user submits first. An admin reviews and approves the item. Payment is collected only after approval.
Use getDefaultPaymentFlow() and getPaymentFlowConfig(flowId) to read the active configuration.
Submit Form Component
The client-side form is in components/submit/submit-form-client.tsx. It wraps the shared DetailsForm component and handles submission:
export function SubmitFormClient({ initialData, locale }: SubmitFormClientProps) {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFormSubmit = async (data: FormData) => {
const payload: ClientCreateItemRequest = {
name: data.name,
description: data.description,
source_url: sourceUrl,
category: data.category,
tags: data.tags || [],
};
const response = await fetch('/api/client/items', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
// ...handle response
};
return (
<DetailsForm
onSubmit={handleFormSubmit}
onBack={handleBack}
listingProps={initialData}
isSubmitting={isSubmitting}
/>
);
}
Props
| Prop | Type | Description |
|---|---|---|
initialData.items | ItemData[] | Existing items (for duplicate detection) |
initialData.categories | Category[] | Available categories for the dropdown |
initialData.tags | Tag[] | Available tags for multi-select |
locale | string | Current locale for redirect URLs |
Form Fields
The DetailsForm component collects:
- Name -- item title (required)
- Description -- rich text description (required)
- Source URL -- primary link (extracted from
linksarray orlinkfield) - Category -- single selection from available categories
- Tags -- multi-select from available tags
- Links -- additional links with type classification
Submission Payload
The form data is transformed into a ClientCreateItemRequest:
interface ClientCreateItemRequest {
name: string;
description: string;
source_url: string;
category: string;
tags: string[];
}
API Route: Create Item
POST /api/client/items receives the payload, validates it, and creates a new item record:
- Authenticate the user via
auth(). - Validate the request body.
- Insert the item with status
pending. - Return the created item.
The route enforces that only authenticated users can submit items.
Admin Review Process
Admins review pending items through the admin dashboard:
- Navigate to
/admin/itemsand filter by statuspending. - Click an item to view details.
- Use the review endpoint to approve or reject.
Review API
POST /api/admin/items/[id]/review
{
"action": "approve"
}
or
{
"action": "reject",
"reason": "Duplicate entry - this tool is already listed."
}
The endpoint updates the item status and triggers a notification to the submitter.
Post-Submission Redirect
After a successful submission the user is redirected to their submissions list:
router.push(`/${locale}/client/submissions`);
This page shows all items the user has submitted, grouped by status.
Error Handling
The submit form handles errors at multiple levels:
- Validation -- the
DetailsFormuses Zod schemas to validate fields before submission. - API errors -- non-2xx responses display the error message in a toast notification.
- Network errors -- caught in the try/catch block with a generic fallback message.
catch (error) {
const errorMessage = error instanceof Error
? error.message
: 'Error submitting form. Please try again.';
toast.error(errorMessage);
}
Item Lifecycle After Publication
Once published, items can be:
- Edited by the owner via
PUT /api/client/items/[id] - Soft-deleted via
DELETE /api/client/items/[id] - Restored via
POST /api/client/items/[id]/restore - Archived by an admin via the admin items API
Related Pages
- Item Submissions -- feature overview
- Payment Configuration -- payment provider setup
- Client Portal Guide -- client-facing pages
- Admin Dashboard Architecture -- admin review interface
- Item Categories -- category management