Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
267 changes: 244 additions & 23 deletions console/src/views/campaign/CreateCampaign.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router"
import { ProjectContext } from "@/contexts"
import { useContext, useState } from "react"
import { useCallback, useContext, useMemo, useState } from "react"
import type { ReactNode } from "react"

import { useResolver } from "@/hooks"
import api from "@/api"
import { Button } from "@/components/ui/button"
import { ArrowRight, Mail, MessageSquareDot, PlusIcon, Smartphone } from "lucide-react"
import type { ChannelType } from "@/types"
import type { components } from "@/oapi/management.generated"

import {
Item,
Expand All @@ -25,6 +28,18 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { oapiClient } from "@/oapi/client"

type Subscription = components["schemas"]["Subscription"]

interface Channel {
key: ChannelType
Expand All @@ -45,24 +60,85 @@ export function CreateCampaign({ open = false, onBeforeCreate, trigger }: Create
const navigate = useNavigate()
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(open)
const [selectingChannel, setSelectingChannel] = useState<ChannelType | null>(null)
const [selectedChannel, setSelectedChannel] = useState<ChannelType | null>(null)
const [transactional, setTransactional] = useState(false)
const [subscriptionId, setSubscriptionId] = useState<string>("")

const [subscriptions] = useResolver(
useCallback(async (): Promise<Subscription[]> => {
if (!project?.id) return []

const response = await oapiClient.GET("/api/admin/projects/{projectID}/subscriptions", {
params: {
path: {
projectID: project.id,
},
query: {
limit: 100,
},
},
})

if (response.error || !response.data?.results) {
return []
}

async function selectChannel(channel: ChannelType) {
if (!project?.id || selectingChannel) {
return response.data.results
}, [project?.id]),
)

const filteredSubscriptions = useMemo(() => {
if (!selectedChannel) return []
return (subscriptions ?? []).filter(
(subscription) => subscription.channel === selectedChannel,
)
}, [selectedChannel, subscriptions])

const subscriptionsLoading = subscriptions === null
async function create() {
if (!project?.id || !selectedChannel) {
return
}

setSelectingChannel(channel)
if (onBeforeCreate) {
await onBeforeCreate()
}

try {
if (onBeforeCreate) {
await onBeforeCreate()
}
const body: {
name: string
channel: components["schemas"]["Channel"]
transactional?: boolean
subscription_id?: string
} = {
name: generateProjectName(),
channel: selectedChannel,
transactional,
}

setIsOpen(false)
navigate(`/projects/${project.id}/campaigns/new/${channel}`)
} finally {
setSelectingChannel(null)
if (transactional) {
body.subscription_id = undefined
} else if (subscriptionId) {
body.subscription_id = subscriptionId
}

const campaign = await oapiClient.POST("/api/admin/projects/{projectID}/campaigns", {
params: {
path: {
projectID: project.id,
},
},
body,
})

if (campaign.data?.id) {
const template = await api.campaigns.templates.create(project.id, campaign.data.id, {
locale: project.locale,
data: {},
})

navigate(
`/projects/${project.id}/campaigns/${campaign.data.id}/templates/${template.id}`,
)
}
}

Expand Down Expand Up @@ -111,8 +187,10 @@ export function CreateCampaign({ open = false, onBeforeCreate, trigger }: Create
<button
type="button"
className="cursor-pointer text-left"
disabled={selectingChannel !== null}
onClick={() => void selectChannel(channel.key)}
onClick={() => {
setSelectedChannel(channel.key)
setSubscriptionId("")
}}
>
<ItemMedia variant="icon" className={channel.color}>
{channel.icon}
Expand All @@ -122,19 +200,162 @@ export function CreateCampaign({ open = false, onBeforeCreate, trigger }: Create
<ItemDescription>{channel.description}</ItemDescription>
</ItemContent>
<ItemActions>
{selectingChannel === channel.key ? (
<span className="text-sm text-muted-foreground">
{t("loading", "Loading...")}
</span>
) : (
<ArrowRight strokeWidth={1} />
)}
<ArrowRight strokeWidth={1} />
</ItemActions>
</button>
</Item>
))}
</ItemGroup>

{selectedChannel && (
<div className="mt-4 space-y-4">
<div className="flex items-center justify-between rounded-md border p-3">
<div className="space-y-1">
<Label htmlFor="transactional-toggle">
{t("campaign.transactional", "Transactional")}
</Label>
<p className="text-sm text-muted-foreground">
{t(
"campaign.transactional.help",
"When enabled, subscription preference is ignored.",
)}
</p>
</div>
<Switch
id="transactional-toggle"
checked={transactional}
onCheckedChange={(checked) => {
setTransactional(checked)
if (checked) setSubscriptionId("")
}}
/>
</div>

{!transactional && (
<div className="space-y-2">
<Label htmlFor="subscription-select">
{t("campaign.subscription", "Subscription")}
</Label>
<Select value={subscriptionId} onValueChange={setSubscriptionId}>
<SelectTrigger id="subscription-select">
<SelectValue
placeholder={t(
"campaign.subscription.placeholder",
"Select subscription",
)}
/>
</SelectTrigger>
<SelectContent className="z-[1100]">
{subscriptionsLoading && (
<SelectItem value="__loading" disabled>
{t("loading", "Loading...")}
</SelectItem>
)}
{!subscriptionsLoading &&
filteredSubscriptions.length === 0 && (
<SelectItem value="__empty" disabled>
{t(
"campaign.subscription.empty",
"No subscriptions for this channel",
)}
</SelectItem>
)}
{filteredSubscriptions.map((subscription) => (
<SelectItem
key={subscription.id}
value={subscription.id}
>
{subscription.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
)}

<Button onClick={() => create()}>{t("campaign.create.action")}</Button>
</div>
)}
</DialogContent>
</Dialog>
)
}

const adjectives = [
"adaptive",
"bold",
"bright",
"calm",
"clear",
"confident",
"connected",
"consistent",
"conversational",
"curated",
"direct",
"dynamic",
"effective",
"elegant",
"engaging",
"focused",
"friendly",
"impactful",
"informative",
"insightful",
"intentional",
"modern",
"personal",
"polished",
"proactive",
"relevant",
"responsive",
"smart",
"smooth",
"strategic",
"targeted",
"timely",
"trusted",
"unified",
"useful",
"warm",
]

const names = [
"announcement",
"beacon",
"broadcast",
"bulletin",
"campaign",
"cascade",
"connect",
"conversation",
"dispatch",
"engagement",
"experience",
"feature",
"followup",
"highlight",
"insight",
"invitation",
"journey",
"launch",
"message",
"moment",
"nudge",
"outreach",
"pulse",
"release",
"reminder",
"signal",
"spotlight",
"story",
"touchpoint",
"update",
"wave",
]

function generateProjectName() {
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)]
const name = names[Math.floor(Math.random() * names.length)]
return `${adjective} ${name}`
}
Loading