mirror of
https://github.com/Snouzy/workout-cool.git
synced 2026-05-19 14:40:35 +00:00
feat(sponsor onboarding): add onboarding page, email template, form, schema, and server action to collect sponsor details and notify via email for approval process
This commit is contained in:
@@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center px-4 py-12">
|
||||
<div className="w-full max-w-md space-y-6">
|
||||
<div className="text-center space-y-2">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Sparkles className="w-6 h-6 text-[#4F8EF7]" />
|
||||
<h1 className="text-2xl font-bold text-slate-900 dark:text-white">{t("ads.onboarding_title")}</h1>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">{t("ads.onboarding_description")}</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 p-6 shadow-sm">
|
||||
<SponsorOnboardingForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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) => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>New Sponsor: {brandName}</Preview>
|
||||
<Tailwind>
|
||||
<Body className="mx-auto my-auto bg-white font-sans">
|
||||
<Container className="mx-auto my-[40px] w-[465px] rounded border border-solid border-[#eaeaea] p-[20px]">
|
||||
<Section className="mt-[32px]">
|
||||
<Heading className="mx-0 my-[30px] p-0 text-center text-[24px] font-normal text-black">
|
||||
New Sponsor Onboarding
|
||||
</Heading>
|
||||
<Text className="text-[14px] leading-[24px] text-black">
|
||||
<strong>{brandName}</strong> just completed the sponsor onboarding form after payment.
|
||||
</Text>
|
||||
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
|
||||
<Text className="text-[14px] leading-[24px] text-black">
|
||||
<strong>Brand:</strong> {brandName}
|
||||
</Text>
|
||||
<Text className="text-[14px] leading-[24px] text-black">
|
||||
<strong>Email:</strong> {email}
|
||||
</Text>
|
||||
<Text className="text-[14px] leading-[24px] text-black">
|
||||
<strong>Website:</strong> <Link href={websiteUrl}>{websiteUrl}</Link>
|
||||
</Text>
|
||||
<Text className="text-[14px] leading-[24px] text-black">
|
||||
<strong>Logo:</strong> <Link href={logoUrl}>{logoUrl}</Link>
|
||||
</Text>
|
||||
{tagline && (
|
||||
<Text className="text-[14px] leading-[24px] text-black">
|
||||
<strong>Tagline:</strong> {tagline}
|
||||
</Text>
|
||||
)}
|
||||
{notes && (
|
||||
<>
|
||||
<Hr className="mx-0 my-[26px] w-full border border-solid border-[#eaeaea]" />
|
||||
<Text className="text-[14px] leading-[24px] text-black">
|
||||
<strong>Additional notes:</strong>
|
||||
</Text>
|
||||
<Text className="whitespace-pre-wrap text-[14px] leading-[24px] text-black">{notes}</Text>
|
||||
</>
|
||||
)}
|
||||
</Section>
|
||||
</Container>
|
||||
</Body>
|
||||
</Tailwind>
|
||||
</Html>
|
||||
);
|
||||
|
||||
export default SponsorOnboardingEmail;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<div className="rounded-lg border border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 p-3">
|
||||
<div className="flex items-center gap-1.5 mb-1">
|
||||
{icon}
|
||||
<span className="text-[10px] uppercase tracking-wide text-slate-500 dark:text-slate-400">{label}</span>
|
||||
</div>
|
||||
<p className="font-semibold text-sm text-slate-800 dark:text-slate-200">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SponsorDialog({ open, onOpenChange }: SponsorDialogProps) {
|
||||
const t = useI18n();
|
||||
const stripeUrl = env.NEXT_PUBLIC_STRIPE_AD_SPOT_URL ?? "#";
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex flex-col items-center gap-4 py-12 text-center">
|
||||
<CheckCircle className="w-16 h-16 text-emerald-500" />
|
||||
<h2 className="text-2xl font-bold text-slate-900 dark:text-white">{t("ads.onboarding_success_title")}</h2>
|
||||
<p className="text-slate-500 dark:text-slate-400 max-w-md">{t("ads.onboarding_success_description")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form className="space-y-5" form={form} onSubmit={onSubmit}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="brandName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("ads.onboarding_brand_name")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Acme Inc." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("ads.onboarding_email")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="sponsor@example.com" type="email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="websiteUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("ads.onboarding_website")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://example.com" type="url" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="logoUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("ads.onboarding_logo")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://example.com/logo.png" type="url" {...field} />
|
||||
</FormControl>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">{t("ads.onboarding_logo_hint")}</p>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="tagline"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("ads.onboarding_tagline")} <span className="text-slate-400 font-normal">({t("ads.optional")})</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your catchy tagline here" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="notes"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("ads.onboarding_notes")} <span className="text-slate-400 font-normal">({t("ads.optional")})</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder={t("ads.onboarding_notes_placeholder")} rows={3} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<button
|
||||
className="flex items-center justify-center gap-2 w-full bg-gradient-to-r from-[#4F8EF7] to-[#25CB78] text-white font-semibold py-3 rounded-lg hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
disabled={status === "executing"}
|
||||
type="submit"
|
||||
>
|
||||
{status === "executing" ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
)}
|
||||
{t("ads.onboarding_submit")}
|
||||
</button>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -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." };
|
||||
});
|
||||
@@ -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<typeof SponsorOnboardingSchema>;
|
||||
Reference in New Issue
Block a user