From 7691575c2fded8e2e600267bb6078751e30960f5 Mon Sep 17 00:00:00 2001
From: Mathias
Date: Sun, 22 Mar 2026 23:20:48 +0100
Subject: [PATCH] feat(sponsor onboarding): add onboarding page, email
template, form, schema, and server action to collect sponsor details and
notify via email for approval process
---
.../(app)/sponsor/onboarding/page.tsx | 33 ++++
emails/SponsorOnboardingEmail.tsx | 61 +++++++
locales/en.ts | 14 ++
locales/es.ts | 14 ++
locales/fr.ts | 14 ++
locales/pt.ts | 14 ++
locales/ru.ts | 14 ++
locales/zh-CN.ts | 14 ++
src/components/ads/custom/sponsor-dialog.tsx | 13 --
.../onboarding/SponsorOnboardingForm.tsx | 149 ++++++++++++++++++
.../onboarding/sponsor-onboarding.action.ts | 20 +++
.../onboarding/sponsor-onboarding.schema.ts | 12 ++
12 files changed, 359 insertions(+), 13 deletions(-)
create mode 100644 app/[locale]/(app)/sponsor/onboarding/page.tsx
create mode 100644 emails/SponsorOnboardingEmail.tsx
create mode 100644 src/features/sponsor/onboarding/SponsorOnboardingForm.tsx
create mode 100644 src/features/sponsor/onboarding/sponsor-onboarding.action.ts
create mode 100644 src/features/sponsor/onboarding/sponsor-onboarding.schema.ts
diff --git a/app/[locale]/(app)/sponsor/onboarding/page.tsx b/app/[locale]/(app)/sponsor/onboarding/page.tsx
new file mode 100644
index 0000000..21f49fc
--- /dev/null
+++ b/app/[locale]/(app)/sponsor/onboarding/page.tsx
@@ -0,0 +1,33 @@
+import { Metadata } from "next";
+import { Sparkles } from "lucide-react";
+
+import { getI18n } from "locales/server";
+import { SponsorOnboardingForm } from "@/features/sponsor/onboarding/SponsorOnboardingForm";
+
+export const metadata: Metadata = {
+ title: "Sponsor Onboarding | Workout Cool",
+ description: "Complete your sponsorship setup on Workout.cool",
+ robots: { index: false, follow: false },
+};
+
+export default async function SponsorOnboardingPage() {
+ const t = await getI18n();
+
+ return (
+
+
+
+
+
+
{t("ads.onboarding_title")}
+
+
{t("ads.onboarding_description")}
+
+
+
+
+
+
+
+ );
+}
diff --git a/emails/SponsorOnboardingEmail.tsx b/emails/SponsorOnboardingEmail.tsx
new file mode 100644
index 0000000..3556071
--- /dev/null
+++ b/emails/SponsorOnboardingEmail.tsx
@@ -0,0 +1,61 @@
+import * as React from "react";
+import { Body, Container, Head, Heading, Hr, Html, Link, Preview, Section, Text, Tailwind } from "@react-email/components";
+
+interface SponsorOnboardingEmailProps {
+ brandName: string;
+ email: string;
+ websiteUrl: string;
+ logoUrl: string;
+ tagline?: string;
+ notes?: string;
+}
+
+const SponsorOnboardingEmail = ({ brandName, email, websiteUrl, logoUrl, tagline, notes }: SponsorOnboardingEmailProps) => (
+
+
+ New Sponsor: {brandName}
+
+
+
+
+
+ New Sponsor Onboarding
+
+
+ {brandName} just completed the sponsor onboarding form after payment.
+
+
+
+ Brand: {brandName}
+
+
+ Email: {email}
+
+
+ Website: {websiteUrl}
+
+
+ Logo: {logoUrl}
+
+ {tagline && (
+
+ Tagline: {tagline}
+
+ )}
+ {notes && (
+ <>
+
+
+ Additional notes:
+
+ {notes}
+ >
+ )}
+
+
+
+
+
+);
+
+export default SponsorOnboardingEmail;
diff --git a/locales/en.ts b/locales/en.ts
index 7e1f8f0..5d18219 100644
--- a/locales/en.ts
+++ b/locales/en.ts
@@ -1974,5 +1974,19 @@ export default {
feature_targeted: "Targeted fitness audience (18-35)",
feature_premium_placement: "Premium sidebar & banner placement",
cta_book: "Book this spot",
+ onboarding_title: "Sponsor Onboarding",
+ onboarding_description: "Thanks for your payment! Fill in your brand details below and we'll get your ad live within 24h.",
+ onboarding_brand_name: "Brand Name",
+ onboarding_email: "Contact Email",
+ onboarding_website: "Website URL",
+ onboarding_logo: "Logo URL",
+ onboarding_logo_hint: "Direct link to your logo image (PNG or SVG, min 200x200px)",
+ onboarding_tagline: "Tagline",
+ onboarding_notes: "Additional notes",
+ onboarding_notes_placeholder: "Any specific requirements, preferred placement, campaign goals...",
+ onboarding_submit: "Submit my sponsorship",
+ onboarding_success_title: "You're all set!",
+ onboarding_success_description: "We received your brand details. Your ad will be live within 24 hours. We'll send you a confirmation email.",
+ optional: "optional",
},
} as const;
diff --git a/locales/es.ts b/locales/es.ts
index 5008daf..0b3c663 100644
--- a/locales/es.ts
+++ b/locales/es.ts
@@ -1976,5 +1976,19 @@ export default {
feature_targeted: "Audiencia fitness segmentada (18-35)",
feature_premium_placement: "Ubicación premium en sidebar y banners",
cta_book: "Reservar este espacio",
+ onboarding_title: "Onboarding de Sponsor",
+ onboarding_description: "¡Gracias por tu pago! Completa los datos de tu marca abajo y tu anuncio estará activo en 24h.",
+ onboarding_brand_name: "Nombre de la marca",
+ onboarding_email: "Email de contacto",
+ onboarding_website: "URL del sitio web",
+ onboarding_logo: "URL del logo",
+ onboarding_logo_hint: "Enlace directo a tu logo (PNG o SVG, mín 200x200px)",
+ onboarding_tagline: "Eslogan",
+ onboarding_notes: "Notas adicionales",
+ onboarding_notes_placeholder: "Requisitos específicos, ubicación preferida, objetivos de campaña...",
+ onboarding_submit: "Enviar mi patrocinio",
+ onboarding_success_title: "¡Todo listo!",
+ onboarding_success_description: "Recibimos los datos de tu marca. Tu anuncio estará activo en 24 horas. Te enviaremos un email de confirmación.",
+ optional: "opcional",
},
} as const;
diff --git a/locales/fr.ts b/locales/fr.ts
index be7603b..c6dbb3a 100644
--- a/locales/fr.ts
+++ b/locales/fr.ts
@@ -2000,5 +2000,19 @@ export default {
feature_targeted: "Audience fitness ciblée (18-35 ans)",
feature_premium_placement: "Placement premium sidebar et bannière",
cta_book: "Réserver cet emplacement",
+ onboarding_title: "Onboarding Sponsor",
+ onboarding_description: "Merci pour votre paiement ! Remplissez les détails de votre marque ci-dessous et votre pub sera en ligne sous 24h.",
+ onboarding_brand_name: "Nom de la marque",
+ onboarding_email: "Email de contact",
+ onboarding_website: "URL du site web",
+ onboarding_logo: "URL du logo",
+ onboarding_logo_hint: "Lien direct vers votre logo (PNG ou SVG, min 200x200px)",
+ onboarding_tagline: "Slogan",
+ onboarding_notes: "Notes complémentaires",
+ onboarding_notes_placeholder: "Exigences particulières, placement préféré, objectifs de campagne...",
+ onboarding_submit: "Envoyer mon sponsoring",
+ onboarding_success_title: "C'est tout bon !",
+ onboarding_success_description: "Nous avons bien reçu vos informations. Votre pub sera en ligne sous 24 heures. Vous recevrez un email de confirmation.",
+ optional: "optionnel",
},
} as const;
diff --git a/locales/pt.ts b/locales/pt.ts
index b8744e9..51aa2a2 100644
--- a/locales/pt.ts
+++ b/locales/pt.ts
@@ -1976,5 +1976,19 @@ export default {
feature_targeted: "Público fitness segmentado (18-35)",
feature_premium_placement: "Posicionamento premium em sidebar e banners",
cta_book: "Reservar este espaço",
+ onboarding_title: "Onboarding de Patrocinador",
+ onboarding_description: "Obrigado pelo pagamento! Preencha os dados da sua marca abaixo e seu anúncio estará ativo em 24h.",
+ onboarding_brand_name: "Nome da marca",
+ onboarding_email: "Email de contato",
+ onboarding_website: "URL do site",
+ onboarding_logo: "URL do logo",
+ onboarding_logo_hint: "Link direto para o seu logo (PNG ou SVG, mín 200x200px)",
+ onboarding_tagline: "Slogan",
+ onboarding_notes: "Notas adicionais",
+ onboarding_notes_placeholder: "Requisitos específicos, posicionamento preferido, objetivos da campanha...",
+ onboarding_submit: "Enviar meu patrocínio",
+ onboarding_success_title: "Tudo pronto!",
+ onboarding_success_description: "Recebemos os dados da sua marca. Seu anúncio estará ativo em 24 horas. Enviaremos um email de confirmação.",
+ optional: "opcional",
},
} as const;
diff --git a/locales/ru.ts b/locales/ru.ts
index 7285e71..083b457 100644
--- a/locales/ru.ts
+++ b/locales/ru.ts
@@ -1967,5 +1967,19 @@ export default {
feature_targeted: "Целевая фитнес-аудитория (18-35)",
feature_premium_placement: "Премиум-размещение в сайдбаре и баннерах",
cta_book: "Забронировать место",
+ onboarding_title: "Регистрация спонсора",
+ onboarding_description: "Спасибо за оплату! Заполните данные вашего бренда ниже, и ваша реклама будет запущена в течение 24 часов.",
+ onboarding_brand_name: "Название бренда",
+ onboarding_email: "Контактный email",
+ onboarding_website: "URL сайта",
+ onboarding_logo: "URL логотипа",
+ onboarding_logo_hint: "Прямая ссылка на ваш логотип (PNG или SVG, мин 200x200px)",
+ onboarding_tagline: "Слоган",
+ onboarding_notes: "Дополнительные заметки",
+ onboarding_notes_placeholder: "Особые требования, предпочтительное размещение, цели кампании...",
+ onboarding_submit: "Отправить данные спонсора",
+ onboarding_success_title: "Всё готово!",
+ onboarding_success_description: "Мы получили данные вашего бренда. Ваша реклама будет запущена в течение 24 часов. Мы отправим вам подтверждение по email.",
+ optional: "необязательно",
},
} as const;
diff --git a/locales/zh-CN.ts b/locales/zh-CN.ts
index f0814d6..64faf15 100644
--- a/locales/zh-CN.ts
+++ b/locales/zh-CN.ts
@@ -1912,5 +1912,19 @@ export default {
feature_targeted: "精准健身受众 (18-35岁)",
feature_premium_placement: "侧边栏和横幅的高级位置",
cta_book: "预订此广告位",
+ onboarding_title: "赞助商注册",
+ onboarding_description: "感谢您的付款!请在下方填写您的品牌信息,我们将在24小时内上线您的广告。",
+ onboarding_brand_name: "品牌名称",
+ onboarding_email: "联系邮箱",
+ onboarding_website: "网站链接",
+ onboarding_logo: "Logo链接",
+ onboarding_logo_hint: "Logo图片直链(PNG或SVG,最小200x200px)",
+ onboarding_tagline: "标语",
+ onboarding_notes: "补充说明",
+ onboarding_notes_placeholder: "特殊要求、首选位置、活动目标...",
+ onboarding_submit: "提交赞助信息",
+ onboarding_success_title: "一切就绪!",
+ onboarding_success_description: "我们已收到您的品牌信息。您的广告将在24小时内上线。我们会发送确认邮件给您。",
+ optional: "可选",
},
} as const;
diff --git a/src/components/ads/custom/sponsor-dialog.tsx b/src/components/ads/custom/sponsor-dialog.tsx
index b27d52a..bbb21b5 100644
--- a/src/components/ads/custom/sponsor-dialog.tsx
+++ b/src/components/ads/custom/sponsor-dialog.tsx
@@ -1,7 +1,6 @@
"use client";
import { Drawer } from "vaul";
-import { ReactNode } from "react";
import { ExternalLink, Globe, MapPin, Monitor, PieChart, Smartphone, Sparkles, Target, TrendingUp, X } from "lucide-react";
import { useI18n } from "locales/client";
@@ -14,18 +13,6 @@ interface SponsorDialogProps {
onOpenChange: (open: boolean) => void;
}
-function StatCard({ icon, label, value }: { icon: ReactNode; label: string; value: string }) {
- return (
-
-
- {icon}
- {label}
-
-
{value}
-
- );
-}
-
export function SponsorDialog({ open, onOpenChange }: SponsorDialogProps) {
const t = useI18n();
const stripeUrl = env.NEXT_PUBLIC_STRIPE_AD_SPOT_URL ?? "#";
diff --git a/src/features/sponsor/onboarding/SponsorOnboardingForm.tsx b/src/features/sponsor/onboarding/SponsorOnboardingForm.tsx
new file mode 100644
index 0000000..1511b60
--- /dev/null
+++ b/src/features/sponsor/onboarding/SponsorOnboardingForm.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import { useAction } from "next-safe-action/hooks";
+import { CheckCircle, ExternalLink, Loader2 } from "lucide-react";
+
+import { useI18n } from "locales/client";
+import { Input } from "@/components/ui/input";
+import { Textarea } from "@/components/ui/textarea";
+import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
+
+import { SponsorOnboardingSchema, type SponsorOnboardingSchemaType } from "./sponsor-onboarding.schema";
+import { sponsorOnboardingAction } from "./sponsor-onboarding.action";
+
+export function SponsorOnboardingForm() {
+ const t = useI18n();
+ const form = useZodForm({
+ schema: SponsorOnboardingSchema,
+ defaultValues: {
+ brandName: "",
+ email: "",
+ websiteUrl: "",
+ logoUrl: "",
+ tagline: "",
+ notes: "",
+ },
+ });
+
+ const { execute, status } = useAction(sponsorOnboardingAction);
+
+ const onSubmit = (data: SponsorOnboardingSchemaType) => {
+ execute(data);
+ };
+
+ if (status === "hasSucceeded") {
+ return (
+
+
+
{t("ads.onboarding_success_title")}
+
{t("ads.onboarding_success_description")}
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/features/sponsor/onboarding/sponsor-onboarding.action.ts b/src/features/sponsor/onboarding/sponsor-onboarding.action.ts
new file mode 100644
index 0000000..d712f6a
--- /dev/null
+++ b/src/features/sponsor/onboarding/sponsor-onboarding.action.ts
@@ -0,0 +1,20 @@
+"use server";
+
+import SponsorOnboardingEmail from "@emails/SponsorOnboardingEmail";
+import { sendEmail } from "@/shared/lib/mail/sendEmail";
+import { SiteConfig } from "@/shared/config/site-config";
+import { actionClient } from "@/shared/api/safe-actions";
+
+import { SponsorOnboardingSchema } from "./sponsor-onboarding.schema";
+
+export const sponsorOnboardingAction = actionClient.schema(SponsorOnboardingSchema).action(async ({ parsedInput }) => {
+ await sendEmail({
+ from: SiteConfig.email.from,
+ to: SiteConfig.email.contact,
+ subject: `New Sponsor: ${parsedInput.brandName}`,
+ text: `New sponsor onboarding: ${parsedInput.brandName} (${parsedInput.email}) - ${parsedInput.websiteUrl}`,
+ react: SponsorOnboardingEmail(parsedInput),
+ });
+
+ return { message: "Your sponsorship details have been submitted successfully." };
+});
diff --git a/src/features/sponsor/onboarding/sponsor-onboarding.schema.ts b/src/features/sponsor/onboarding/sponsor-onboarding.schema.ts
new file mode 100644
index 0000000..6505c5e
--- /dev/null
+++ b/src/features/sponsor/onboarding/sponsor-onboarding.schema.ts
@@ -0,0 +1,12 @@
+import { z } from "zod";
+
+export const SponsorOnboardingSchema = z.object({
+ brandName: z.string().min(1, "Brand name is required"),
+ email: z.string().email("Invalid email address"),
+ websiteUrl: z.string().url("Invalid URL"),
+ logoUrl: z.string().url("Invalid logo URL"),
+ tagline: z.string().optional(),
+ notes: z.string().optional(),
+});
+
+export type SponsorOnboardingSchemaType = z.infer;