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 ( +
+ ( + + {t("ads.onboarding_brand_name")} + + + + + + )} + /> + + ( + + {t("ads.onboarding_email")} + + + + + + )} + /> + + ( + + {t("ads.onboarding_website")} + + + + + + )} + /> + + ( + + {t("ads.onboarding_logo")} + + + +

{t("ads.onboarding_logo_hint")}

+ +
+ )} + /> + + ( + + + {t("ads.onboarding_tagline")} ({t("ads.optional")}) + + + + + + + )} + /> + + ( + + + {t("ads.onboarding_notes")} ({t("ads.optional")}) + + +