diff --git a/apps/docs/api-reference/campaigns/create-campaign.mdx b/apps/docs/api-reference/campaigns/create-campaign.mdx new file mode 100644 index 00000000..911f7138 --- /dev/null +++ b/apps/docs/api-reference/campaigns/create-campaign.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v1/campaigns +--- \ No newline at end of file diff --git a/apps/docs/api-reference/campaigns/get-campaign.mdx b/apps/docs/api-reference/campaigns/get-campaign.mdx new file mode 100644 index 00000000..81803f00 --- /dev/null +++ b/apps/docs/api-reference/campaigns/get-campaign.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /v1/campaigns/{campaignId} +--- \ No newline at end of file diff --git a/apps/docs/api-reference/campaigns/pause-campaign.mdx b/apps/docs/api-reference/campaigns/pause-campaign.mdx new file mode 100644 index 00000000..67ab0dea --- /dev/null +++ b/apps/docs/api-reference/campaigns/pause-campaign.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v1/campaigns/{campaignId}/pause +--- \ No newline at end of file diff --git a/apps/docs/api-reference/campaigns/resume-campaign.mdx b/apps/docs/api-reference/campaigns/resume-campaign.mdx new file mode 100644 index 00000000..d99c75e5 --- /dev/null +++ b/apps/docs/api-reference/campaigns/resume-campaign.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v1/campaigns/{campaignId}/resume +--- \ No newline at end of file diff --git a/apps/docs/api-reference/campaigns/schedule-campaign.mdx b/apps/docs/api-reference/campaigns/schedule-campaign.mdx new file mode 100644 index 00000000..296b68ca --- /dev/null +++ b/apps/docs/api-reference/campaigns/schedule-campaign.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /v1/campaigns/{campaignId}/schedule +--- \ No newline at end of file diff --git a/apps/docs/api-reference/openapi.json b/apps/docs/api-reference/openapi.json index 308164e1..5f88ed49 100644 --- a/apps/docs/api-reference/openapi.json +++ b/apps/docs/api-reference/openapi.json @@ -1910,6 +1910,531 @@ } } } + }, + "/v1/campaigns": { + "post": { + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "from": { + "type": "string", + "minLength": 1 + }, + "subject": { + "type": "string", + "minLength": 1 + }, + "previewText": { + "type": "string" + }, + "contactBookId": { + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1 + }, + "html": { + "type": "string", + "minLength": 1 + }, + "replyTo": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "cc": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "bcc": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "sendNow": { + "type": "boolean" + }, + "scheduledAt": { + "type": "string", + "description": "Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am', 'next monday 10:30')" + }, + "batchSize": { + "type": "integer", + "minimum": 1, + "maximum": 100000 + } + }, + "required": [ + "name", + "from", + "subject", + "contactBookId" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Create a campaign", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "from": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "previewText": { + "type": "string", + "nullable": true + }, + "contactBookId": { + "type": "string", + "nullable": true + }, + "html": { + "type": "string", + "nullable": true + }, + "content": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "scheduledAt": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "batchSize": { + "type": "integer" + }, + "batchWindowMinutes": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "sent": { + "type": "integer" + }, + "delivered": { + "type": "integer" + }, + "opened": { + "type": "integer" + }, + "clicked": { + "type": "integer" + }, + "unsubscribed": { + "type": "integer" + }, + "bounced": { + "type": "integer" + }, + "hardBounced": { + "type": "integer" + }, + "complained": { + "type": "integer" + }, + "replyTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "cc": { + "type": "array", + "items": { + "type": "string" + } + }, + "bcc": { + "type": "array", + "items": { + "type": "string" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "from", + "subject", + "previewText", + "contactBookId", + "html", + "content", + "status", + "scheduledAt", + "batchSize", + "batchWindowMinutes", + "total", + "sent", + "delivered", + "opened", + "clicked", + "unsubscribed", + "bounced", + "hardBounced", + "complained", + "replyTo", + "cc", + "bcc", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/v1/campaigns/{campaignId}": { + "get": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "cmp_123" + }, + "required": true, + "name": "campaignId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Get campaign details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "from": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "previewText": { + "type": "string", + "nullable": true + }, + "contactBookId": { + "type": "string", + "nullable": true + }, + "html": { + "type": "string", + "nullable": true + }, + "content": { + "type": "string", + "nullable": true + }, + "status": { + "type": "string" + }, + "scheduledAt": { + "type": "string", + "nullable": true, + "format": "date-time" + }, + "batchSize": { + "type": "integer" + }, + "batchWindowMinutes": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "sent": { + "type": "integer" + }, + "delivered": { + "type": "integer" + }, + "opened": { + "type": "integer" + }, + "clicked": { + "type": "integer" + }, + "unsubscribed": { + "type": "integer" + }, + "bounced": { + "type": "integer" + }, + "hardBounced": { + "type": "integer" + }, + "complained": { + "type": "integer" + }, + "replyTo": { + "type": "array", + "items": { + "type": "string" + } + }, + "cc": { + "type": "array", + "items": { + "type": "string" + } + }, + "bcc": { + "type": "array", + "items": { + "type": "string" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "id", + "name", + "from", + "subject", + "previewText", + "contactBookId", + "html", + "content", + "status", + "scheduledAt", + "batchSize", + "batchWindowMinutes", + "total", + "sent", + "delivered", + "opened", + "clicked", + "unsubscribed", + "bounced", + "hardBounced", + "complained", + "replyTo", + "cc", + "bcc", + "createdAt", + "updatedAt" + ] + } + } + } + } + } + } + }, + "/v1/campaigns/{campaignId}/schedule": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "cmp_123" + }, + "required": true, + "name": "campaignId", + "in": "path" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "scheduledAt": { + "type": "string", + "description": "Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am', 'next monday 10:30')" + }, + "batchSize": { + "type": "integer", + "minimum": 1, + "maximum": 100000 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Schedule a campaign", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] + } + } + } + } + } + } + }, + "/v1/campaigns/{campaignId}/pause": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "cmp_123" + }, + "required": true, + "name": "campaignId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Pause a campaign", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] + } + } + } + } + } + } + }, + "/v1/campaigns/{campaignId}/resume": { + "post": { + "parameters": [ + { + "schema": { + "type": "string", + "minLength": 1, + "example": "cmp_123" + }, + "required": true, + "name": "campaignId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Resume a campaign", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] + } + } + } + } + } + } } } } \ No newline at end of file diff --git a/apps/docs/docs.json b/apps/docs/docs.json index c98135a9..4c190608 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -84,6 +84,16 @@ "api-reference/domains/verify-domain", "api-reference/domains/delete-domain" ] + }, + { + "group": "Campaigns", + "pages": [ + "api-reference/campaigns/create-campaign", + "api-reference/campaigns/get-campaign", + "api-reference/campaigns/schedule-campaign", + "api-reference/campaigns/pause-campaign", + "api-reference/campaigns/resume-campaign" + ] } ] } diff --git a/apps/web/prisma/migrations/20251013114734_add_api_to_campaign/migration.sql b/apps/web/prisma/migrations/20251013114734_add_api_to_campaign/migration.sql new file mode 100644 index 00000000..9b04304f --- /dev/null +++ b/apps/web/prisma/migrations/20251013114734_add_api_to_campaign/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Campaign" ADD COLUMN "isApi" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index 19cc4636..772d23b1 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -365,6 +365,7 @@ model Campaign { bounced Int @default(0) hardBounced Int @default(0) complained Int @default(0) + isApi Boolean @default(false) status CampaignStatus @default(DRAFT) batchSize Int @default(500) batchWindowMinutes Int @default(0) diff --git a/apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx b/apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx index 3fb1cba3..e445731c 100644 --- a/apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx +++ b/apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx @@ -97,6 +97,7 @@ function CampaignEditor({ campaign: Campaign & { imageUploadSupported: boolean }; }) { const router = useRouter(); + const isApiCampaign = campaign.isApi; const contactBooksQuery = api.contacts.getContactBooks.useQuery({}); const utils = api.useUtils(); @@ -124,6 +125,9 @@ function CampaignEditor({ const getUploadUrl = api.campaign.generateImagePresignedUrl.useMutation(); function updateEditorContent() { + if (isApiCampaign) { + return; + } updateCampaignMutation.mutate({ campaignId: campaign.id, content: JSON.stringify(json), @@ -142,8 +146,6 @@ function CampaignEditor({ ); } - console.log("file type: ", file.type); - const { uploadUrl, imageUrl } = await getUploadUrl.mutateAsync({ name: file.name, type: file.type, @@ -175,7 +177,12 @@ function CampaignEditor({ value={name} onChange={(e) => setName(e.target.value)} className=" border-0 focus:ring-0 focus:outline-none px-0.5 w-[300px]" + disabled={isApiCampaign} + readOnly={isApiCampaign} onBlur={() => { + if (isApiCampaign) { + return; + } if (name === campaign.name || !name) { return; } @@ -228,6 +235,9 @@ function CampaignEditor({ setSubject(e.target.value); }} onBlur={() => { + if (isApiCampaign) { + return; + } if (subject === campaign.subject || !subject) { return; } @@ -245,6 +255,8 @@ function CampaignEditor({ ); }} className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent focus:border-border bg-transparent" + disabled={isApiCampaign} + readOnly={isApiCampaign} /> @@ -263,6 +275,9 @@ function CampaignEditor({ className="mt-1 py-1 w-full text-sm outline-none border-b border-transparent focus:border-border bg-transparent" placeholder="Friendly name" onBlur={() => { + if (isApiCampaign) { + return; + } if (from === campaign.from || !from) { return; } @@ -279,6 +294,8 @@ function CampaignEditor({ } ); }} + disabled={isApiCampaign} + readOnly={isApiCampaign} />
@@ -294,6 +311,9 @@ function CampaignEditor({ className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent bg-transparent focus:border-border" placeholder="hello@example.com" onBlur={() => { + if (isApiCampaign) { + return; + } if (replyTo === campaign.replyTo[0]) { return; } @@ -310,6 +330,8 @@ function CampaignEditor({ } ); }} + disabled={isApiCampaign} + readOnly={isApiCampaign} />
@@ -324,6 +346,9 @@ function CampaignEditor({ setPreviewText(e.target.value); }} onBlur={() => { + if (isApiCampaign) { + return; + } if ( previewText === campaign.previewText || !previewText @@ -344,6 +369,8 @@ function CampaignEditor({ ); }} className="mt-1 py-1 text-sm block w-full outline-none border-b border-transparent bg-transparent focus:border-border" + disabled={isApiCampaign} + readOnly={isApiCampaign} />
@@ -355,7 +382,11 @@ function CampaignEditor({ ) : (