[web] sponsorships/request: add coupon flow in sponsorships request form (#1493)
(cherry picked from commit d24a7c0f60)
This commit is contained in:
parent
b14990e04c
commit
a81cf8a73b
|
|
@ -19,3 +19,23 @@ export function priceRoundToNearestNiceNumber(priceParam: number) {
|
|||
// Round to nearest 1000.
|
||||
return Math.ceil(price / 1000) * 1000;
|
||||
}
|
||||
|
||||
export function getDiscountedPrice({
|
||||
amountOff,
|
||||
percentOff,
|
||||
price,
|
||||
}: Readonly<{
|
||||
amountOff?: number | null;
|
||||
percentOff?: number | null;
|
||||
price: number;
|
||||
}>) {
|
||||
if (amountOff) {
|
||||
return price - amountOff;
|
||||
}
|
||||
|
||||
if (percentOff) {
|
||||
return price - (price * percentOff) / 100;
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
export const SponsorsPromoCodeConfig: Record<
|
||||
string,
|
||||
{
|
||||
code: string;
|
||||
percentOff: number;
|
||||
}
|
||||
> = {
|
||||
TRIAL25: {
|
||||
code: 'TRIAL25',
|
||||
percentOff: 25,
|
||||
},
|
||||
} as const;
|
||||
|
|
@ -19,6 +19,7 @@ import Text from '~/components/ui/Text';
|
|||
|
||||
import SponsorsAdvertiseRequestForm from '../request/SponsorsAdvertiseRequestForm';
|
||||
import SponsorsAdvertiseRequestReadonly from '../request/SponsorsAdvertiseRequestReadonly';
|
||||
import { SponsorsPromoCodeConfig } from '../SponsorsPromoCodeConfig';
|
||||
import SponsorsAdminAdRequestStatus from './SponsorsAdminAdRequestStatus';
|
||||
|
||||
type Props = Readonly<{
|
||||
|
|
@ -111,6 +112,9 @@ export default function SponsorsAdminAdRequestPage({ adRequestId }: Props) {
|
|||
ads,
|
||||
company,
|
||||
emails: adRequest.emails,
|
||||
promoCode: adRequest.promoCode
|
||||
? SponsorsPromoCodeConfig[adRequest.promoCode]
|
||||
: null,
|
||||
}}
|
||||
mode="edit"
|
||||
requestId={adRequest.id}
|
||||
|
|
@ -134,6 +138,9 @@ export default function SponsorsAdminAdRequestPage({ adRequestId }: Props) {
|
|||
company,
|
||||
createdAt: adRequest.createdAt,
|
||||
emails: adRequest.emails,
|
||||
promoCode: adRequest.promoCode
|
||||
? SponsorsPromoCodeConfig[adRequest.promoCode]
|
||||
: null,
|
||||
review,
|
||||
updatedAt: adRequest.updatedAt,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import TextInput from '~/components/ui/TextInput';
|
|||
|
||||
import { useI18nRouter } from '~/next-i18nostic/src';
|
||||
|
||||
import { SponsorsPromoCodeConfig } from '../SponsorsPromoCodeConfig';
|
||||
import SponsorsAdvertiseRequestForm from './SponsorsAdvertiseRequestForm';
|
||||
import SponsorsAdvertiseRequestReadonly from './SponsorsAdvertiseRequestReadonly';
|
||||
import type {
|
||||
|
|
@ -144,6 +145,9 @@ export default function SponsorsAdvertiseRequestEditPage({ adRequest }: Props) {
|
|||
ads,
|
||||
company,
|
||||
emails: adRequest.emails,
|
||||
promoCode: adRequest.promoCode
|
||||
? SponsorsPromoCodeConfig[adRequest.promoCode]
|
||||
: null,
|
||||
}}
|
||||
mode={adRequest.status === 'PENDING' ? 'edit' : 'readonly'}
|
||||
requestId={adRequest.id}
|
||||
|
|
@ -176,6 +180,9 @@ export default function SponsorsAdvertiseRequestEditPage({ adRequest }: Props) {
|
|||
company,
|
||||
createdAt: adRequest.createdAt,
|
||||
emails: adRequest.emails,
|
||||
promoCode: adRequest.promoCode
|
||||
? SponsorsPromoCodeConfig[adRequest.promoCode]
|
||||
: null,
|
||||
review: null,
|
||||
updatedAt: adRequest.updatedAt,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ export default function SponsorsAdvertiseRequestForm({
|
|||
company: formData.company!,
|
||||
emails: formData.emails,
|
||||
id: props.requestId!,
|
||||
promoCode: formData.promoCode ? formData.promoCode : undefined,
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
|
|
@ -204,6 +205,7 @@ export default function SponsorsAdvertiseRequestForm({
|
|||
agreement,
|
||||
company: formData.company!,
|
||||
emails: formData.emails,
|
||||
promoCode: formData.promoCode ? formData.promoCode : undefined,
|
||||
},
|
||||
{
|
||||
onError: (error) => {
|
||||
|
|
@ -306,8 +308,12 @@ export default function SponsorsAdvertiseRequestForm({
|
|||
<SponsorsAdvertiseRequestFormAdsSection
|
||||
ads={formData.ads}
|
||||
mode={mode}
|
||||
promoCode={formData.promoCode}
|
||||
sessionId={formData.sessionId}
|
||||
updateAds={(ads) => setFormData((prev) => ({ ...prev, ads }))}
|
||||
updatePromoCode={(promoCode) =>
|
||||
setFormData((prev) => ({ ...prev, promoCode }))
|
||||
}
|
||||
updateStepStatus={(status) =>
|
||||
setStepsStatus((prev) => ({ ...prev, ads: status }))
|
||||
}
|
||||
|
|
@ -342,6 +348,7 @@ export default function SponsorsAdvertiseRequestForm({
|
|||
ads: formData.ads,
|
||||
company: formData.company!,
|
||||
emails: formData.emails,
|
||||
promoCode: formData.promoCode,
|
||||
}}
|
||||
isSubmitting={
|
||||
adRequestCreateMutation.isLoading ||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,15 @@ import { urlAddHttpsIfMissing } from '~/lib/urlValidation';
|
|||
import RelativeTimestamp from '~/components/common/datetime/RelativeTimestamp';
|
||||
import InterviewsMarketingHeroBrowserWindowFrame from '~/components/interviews/marketing/embed/InterviewsMarketingHeroBrowserWindowFrame';
|
||||
import { FormattedMessage, useIntl } from '~/components/intl';
|
||||
import { getDiscountedPrice } from '~/components/purchase/PurchasePricingUtils';
|
||||
import SponsorsAdFormatGlobalBanner from '~/components/sponsors/ads/SponsorsAdFormatGlobalBanner';
|
||||
import SponsorsAdFormatInContent from '~/components/sponsors/ads/SponsorsAdFormatInContent';
|
||||
import SponsorsAdFormatInContentBodyRenderer from '~/components/sponsors/ads/SponsorsAdFormatInContentBodyRenderer';
|
||||
import SponsorsAdFormatSpotlight from '~/components/sponsors/ads/SponsorsAdFormatSpotlight';
|
||||
import type { SponsorsAdFormatFormItem } from '~/components/sponsors/request/types';
|
||||
import type {
|
||||
SponsorsAdFormatFormItem,
|
||||
SponsorsPromoCode,
|
||||
} from '~/components/sponsors/request/types';
|
||||
import type { AdvertiseRequestFormValues } from '~/components/sponsors/request/useSponsorsAdvertiseRequestFormData';
|
||||
import {
|
||||
SponsorAdFormatConfigs,
|
||||
|
|
@ -59,8 +63,16 @@ export default function SponsorsAdvertiseRequestReadonly({
|
|||
onEdit,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const { ads, agreement, company, createdAt, emails, review, updatedAt } =
|
||||
data;
|
||||
const {
|
||||
ads,
|
||||
agreement,
|
||||
company,
|
||||
createdAt,
|
||||
emails,
|
||||
promoCode,
|
||||
review,
|
||||
updatedAt,
|
||||
} = data;
|
||||
const { address, signatoryName, signatoryTitle } = company!;
|
||||
const addressString = [
|
||||
[address.line1, address.line2].filter(Boolean).join(', '),
|
||||
|
|
@ -72,8 +84,22 @@ export default function SponsorsAdvertiseRequestReadonly({
|
|||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
const totalPrice = ads.reduce(
|
||||
(acc, curr) =>
|
||||
acc +
|
||||
curr.weeks.length * SponsorAdFormatConfigs[curr.format].pricePerWeekUSD,
|
||||
0,
|
||||
);
|
||||
|
||||
const discountedPrice = promoCode
|
||||
? getDiscountedPrice({
|
||||
percentOff: promoCode.percentOff,
|
||||
price: totalPrice,
|
||||
})
|
||||
: totalPrice;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
<div className="flex w-full flex-col gap-10">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
|
|
@ -188,7 +214,7 @@ export default function SponsorsAdvertiseRequestReadonly({
|
|||
<div className="flex flex-col items-start">
|
||||
<ul className="flex w-full flex-col gap-4">
|
||||
{ads.map((ad) => (
|
||||
<AdFormatCard key={ad.id} ad={ad} />
|
||||
<AdFormatCard key={ad.id} ad={ad} promoCode={promoCode} />
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-4 flex w-full justify-end gap-4">
|
||||
|
|
@ -198,14 +224,7 @@ export default function SponsorsAdvertiseRequestReadonly({
|
|||
description="Total price label"
|
||||
id="0kDCAp"
|
||||
values={{
|
||||
total: ads.reduce(
|
||||
(acc, curr) =>
|
||||
acc +
|
||||
curr.weeks.length *
|
||||
SponsorAdFormatConfigs[curr.format]
|
||||
.pricePerWeekUSD,
|
||||
0,
|
||||
),
|
||||
total: discountedPrice,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
|
@ -327,7 +346,10 @@ export default function SponsorsAdvertiseRequestReadonly({
|
|||
);
|
||||
}
|
||||
|
||||
function AdFormatCard({ ad }: Readonly<{ ad: SponsorsAdFormatFormItem }>) {
|
||||
function AdFormatCard({
|
||||
ad,
|
||||
promoCode,
|
||||
}: Readonly<{ ad: SponsorsAdFormatFormItem; promoCode: SponsorsPromoCode }>) {
|
||||
const intl = useIntl();
|
||||
const adFormatData = useSponsorsAdFormatData();
|
||||
|
||||
|
|
@ -447,7 +469,12 @@ function AdFormatCard({ ad }: Readonly<{ ad: SponsorsAdFormatFormItem }>) {
|
|||
<Text color="secondary" size="body2">
|
||||
$
|
||||
{ad.weeks.length *
|
||||
SponsorAdFormatConfigs[ad.format].pricePerWeekUSD}
|
||||
(promoCode
|
||||
? getDiscountedPrice({
|
||||
percentOff: promoCode.percentOff,
|
||||
price: SponsorAdFormatConfigs[ad.format].pricePerWeekUSD,
|
||||
})
|
||||
: SponsorAdFormatConfigs[ad.format].pricePerWeekUSD)}
|
||||
</Text>
|
||||
</AdDetailRow>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
import ConfirmationDialog from '~/components/common/ConfirmationDialog';
|
||||
import type { StepsTabItemStatus } from '~/components/common/StepsTabs';
|
||||
import { FormattedMessage, useIntl } from '~/components/intl';
|
||||
import { getDiscountedPrice } from '~/components/purchase/PurchasePricingUtils';
|
||||
import { useSponsorsAdFormatData } from '~/components/sponsors/SponsorsAdFormatConfigs';
|
||||
import Badge from '~/components/ui/Badge';
|
||||
import Button from '~/components/ui/Button';
|
||||
|
|
@ -31,18 +32,24 @@ import {
|
|||
|
||||
import { themeBackgroundElementEmphasizedStateColor_Hover } from '../../../ui/theme';
|
||||
import { SponsorAdFormatConfigs } from '../../SponsorsAdFormatConfigs';
|
||||
import type { SponsorsAdFormatFormItem } from '../types';
|
||||
import type { SponsorsAdFormatFormItem, SponsorsPromoCode } from '../types';
|
||||
import SponsorsAdvertiseRequestFormAdsSectionGlobalBanner from './formats/SponsorsAdvertiseRequestFormAdsSectionGlobalBanner';
|
||||
import SponsorsAdvertiseRequestFormAdsSectionInContent from './formats/SponsorsAdvertiseRequestFormAdsSectionInContent';
|
||||
import SponsorsAdvertiseRequestFormAdsSectionSpotlight from './formats/SponsorsAdvertiseRequestFormAdsSectionSpotlight';
|
||||
import SponsorsAdvertiseRequestPromoCode from './SponsorsAdvertiseRequestPromoCode';
|
||||
|
||||
type Props = Readonly<{
|
||||
ads: Array<SponsorsAdFormatFormItem>;
|
||||
mode: 'create' | 'edit' | 'readonly';
|
||||
onPrevious: () => void;
|
||||
onSubmit: () => void;
|
||||
promoCode: SponsorsPromoCode;
|
||||
sessionId: string;
|
||||
updateAds(ads: Array<SponsorsAdFormatFormItem>): void;
|
||||
updatePromoCode: ({
|
||||
code,
|
||||
percentOff,
|
||||
}: Readonly<{ code: string; percentOff: number }>) => void;
|
||||
updateStepStatus(status: StepsTabItemStatus): void;
|
||||
}>;
|
||||
|
||||
|
|
@ -51,8 +58,10 @@ export default function SponsorsAdvertiseRequestFormAdsSection({
|
|||
mode,
|
||||
onPrevious,
|
||||
onSubmit,
|
||||
promoCode,
|
||||
sessionId,
|
||||
updateAds,
|
||||
updatePromoCode,
|
||||
updateStepStatus,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
|
|
@ -152,11 +161,32 @@ export default function SponsorsAdvertiseRequestFormAdsSection({
|
|||
/>
|
||||
</Text>
|
||||
<div className="flex items-center gap-2 max-md:order-4 max-md:-mb-0.5 max-md:-mr-1">
|
||||
<Text size="body2" weight="bold">
|
||||
$
|
||||
{ad.weeks.length *
|
||||
SponsorAdFormatConfigs[ad.format].pricePerWeekUSD}
|
||||
</Text>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{promoCode?.percentOff && (
|
||||
<Text
|
||||
className="line-through"
|
||||
color="secondary"
|
||||
size="body2">
|
||||
$
|
||||
{ad.weeks.length *
|
||||
SponsorAdFormatConfigs[ad.format].pricePerWeekUSD}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text size="body2" weight="bold">
|
||||
$
|
||||
{ad.weeks.length *
|
||||
(promoCode?.percentOff
|
||||
? getDiscountedPrice({
|
||||
percentOff: promoCode?.percentOff,
|
||||
price:
|
||||
SponsorAdFormatConfigs[ad.format]
|
||||
.pricePerWeekUSD,
|
||||
})
|
||||
: SponsorAdFormatConfigs[ad.format]
|
||||
.pricePerWeekUSD)}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
{isReadonly ? (
|
||||
<Button
|
||||
|
|
@ -238,7 +268,15 @@ export default function SponsorsAdvertiseRequestFormAdsSection({
|
|||
(acc, curr) =>
|
||||
acc +
|
||||
curr.weeks.length *
|
||||
SponsorAdFormatConfigs[curr.format].pricePerWeekUSD,
|
||||
(promoCode?.percentOff
|
||||
? getDiscountedPrice({
|
||||
percentOff: promoCode?.percentOff,
|
||||
price:
|
||||
SponsorAdFormatConfigs[curr.format]
|
||||
.pricePerWeekUSD,
|
||||
})
|
||||
: SponsorAdFormatConfigs[curr.format]
|
||||
.pricePerWeekUSD),
|
||||
0,
|
||||
),
|
||||
}}
|
||||
|
|
@ -553,20 +591,29 @@ export default function SponsorsAdvertiseRequestFormAdsSection({
|
|||
onPrevious();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={RiArrowRightLine}
|
||||
isDisabled={ads.length === 0}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: 'Company details',
|
||||
description: 'Label for company details button',
|
||||
id: 'OY0i/0',
|
||||
})}
|
||||
size="md"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
onSubmit();
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-start gap-3">
|
||||
<SponsorsAdvertiseRequestPromoCode
|
||||
appliedPromoCode={promoCode?.code}
|
||||
className="mt-0.5"
|
||||
onApplyPromoCode={(_promoCode) => {
|
||||
updatePromoCode(_promoCode);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={RiArrowRightLine}
|
||||
isDisabled={ads.length === 0}
|
||||
label={intl.formatMessage({
|
||||
defaultMessage: 'Company details',
|
||||
description: 'Label for company details button',
|
||||
id: 'OY0i/0',
|
||||
})}
|
||||
size="md"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
onSubmit();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { RiCheckLine } from 'react-icons/ri';
|
||||
|
||||
import { useIntl } from '~/components/intl';
|
||||
import { SponsorsPromoCodeConfig } from '~/components/sponsors/SponsorsPromoCodeConfig';
|
||||
import Button from '~/components/ui/Button';
|
||||
import Text from '~/components/ui/Text';
|
||||
import TextInput from '~/components/ui/TextInput';
|
||||
import { themeTextSuccessColor } from '~/components/ui/theme';
|
||||
|
||||
type Props = Readonly<{
|
||||
appliedPromoCode?: string;
|
||||
className?: string;
|
||||
onApplyPromoCode: ({
|
||||
code,
|
||||
percentOff,
|
||||
}: Readonly<{ code: string; percentOff: number }>) => void;
|
||||
}>;
|
||||
|
||||
export default function SponsorsAdvertiseRequestPromoCode({
|
||||
appliedPromoCode,
|
||||
className,
|
||||
onApplyPromoCode,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const label = intl.formatMessage({
|
||||
defaultMessage: 'Add promotion code',
|
||||
description: 'Label to add promotion code',
|
||||
id: 'YQhqx6',
|
||||
});
|
||||
const [isValidated, setIsValidated] = useState(!!appliedPromoCode);
|
||||
const [promoCode, setPromoCode] = useState<{
|
||||
error: boolean;
|
||||
percentOff: number | null;
|
||||
value: string;
|
||||
}>({
|
||||
error: false,
|
||||
percentOff:
|
||||
SponsorsPromoCodeConfig[appliedPromoCode ?? '']?.percentOff ?? null,
|
||||
value: appliedPromoCode ?? '',
|
||||
});
|
||||
|
||||
async function handleValidate(e: React.FormEvent<HTMLFormElement>) {
|
||||
e.preventDefault();
|
||||
|
||||
const data = SponsorsPromoCodeConfig[promoCode.value];
|
||||
|
||||
setIsValidated(data != null);
|
||||
if (data) {
|
||||
const discount = data.percentOff;
|
||||
|
||||
if (discount > 0) {
|
||||
onApplyPromoCode({
|
||||
code: data.code,
|
||||
percentOff: discount,
|
||||
});
|
||||
}
|
||||
|
||||
setPromoCode({
|
||||
...promoCode,
|
||||
error: false,
|
||||
percentOff: data.percentOff,
|
||||
value: data.code,
|
||||
});
|
||||
} else {
|
||||
setPromoCode({
|
||||
...promoCode,
|
||||
error: true,
|
||||
percentOff: null,
|
||||
value: promoCode.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx('space-y-2', 'w-60', className)}>
|
||||
<form className="relative" onSubmit={handleValidate}>
|
||||
<TextInput
|
||||
className="pr-8"
|
||||
errorMessage={
|
||||
promoCode.error
|
||||
? intl.formatMessage({
|
||||
defaultMessage: 'Invalid promo code',
|
||||
description: 'Error message for invalid promotion code',
|
||||
id: 'isHmCU',
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
isLabelHidden={true}
|
||||
label={label}
|
||||
placeholder={label}
|
||||
size="sm"
|
||||
value={promoCode.value}
|
||||
onChange={(value) => {
|
||||
setIsValidated(false);
|
||||
setPromoCode({ ...promoCode, value });
|
||||
}}
|
||||
/>
|
||||
{isValidated ? (
|
||||
<RiCheckLine
|
||||
aria-hidden={true}
|
||||
className={clsx(
|
||||
'absolute right-3 top-2',
|
||||
'size-4',
|
||||
themeTextSuccessColor,
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
className="absolute right-1.5 top-0.5"
|
||||
icon={RiCheckLine}
|
||||
isDisabled={promoCode.value.length === 0}
|
||||
isLabelHidden={true}
|
||||
label={label}
|
||||
size="xs"
|
||||
type="submit"
|
||||
variant="tertiary"
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
{promoCode.percentOff && (
|
||||
<Text color="secondary" size="body3">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: 'Code successfully applied! {percentOff}% off',
|
||||
description: 'Message when promo code is successfully applied',
|
||||
id: 'Lqq1vK',
|
||||
},
|
||||
{ percentOff: promoCode.percentOff },
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -250,8 +250,9 @@ export default function SponsorsAdvertiseRequestAgreement({
|
|||
.
|
||||
</li>
|
||||
<li>
|
||||
The exact amount for this booking is specified in the Booking Form and
|
||||
is <strong>payable upfront</strong>.
|
||||
The exact amount for this booking, including any applicable discounts,
|
||||
is specified in the Booking Form and is{' '}
|
||||
<strong>payable upfront</strong>.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { RiArrowLeftLine, RiArrowRightLine } from 'react-icons/ri';
|
|||
|
||||
import type { StepsTabItemStatus } from '~/components/common/StepsTabs';
|
||||
import { FormattedMessage, useIntl } from '~/components/intl';
|
||||
import { getDiscountedPrice } from '~/components/purchase/PurchasePricingUtils';
|
||||
import Anchor from '~/components/ui/Anchor';
|
||||
import Button from '~/components/ui/Button';
|
||||
import CheckboxInput from '~/components/ui/CheckboxInput';
|
||||
|
|
@ -22,6 +23,7 @@ import {
|
|||
import type {
|
||||
SponsorsAdFormatFormItem,
|
||||
SponsorsCompanyDetails,
|
||||
SponsorsPromoCode,
|
||||
} from '../types';
|
||||
import SponsorsAdvertiseRequestAgreement from './SponsorsAdvertiseRequestAgreement';
|
||||
|
||||
|
|
@ -30,6 +32,7 @@ type Props = Readonly<{
|
|||
ads: Array<SponsorsAdFormatFormItem>;
|
||||
company: SponsorsCompanyDetails;
|
||||
emails: Array<string>;
|
||||
promoCode: SponsorsPromoCode;
|
||||
};
|
||||
isSubmitting?: boolean;
|
||||
mode?: 'create' | 'edit' | 'readonly';
|
||||
|
|
@ -62,7 +65,7 @@ export default function SponsorsAdvertiseRequestFormReviewSection({
|
|||
mode === 'create' ? false : true,
|
||||
);
|
||||
|
||||
const { ads, company, emails } = data;
|
||||
const { ads, company, emails, promoCode } = data;
|
||||
const { address, legalName, signatoryName, signatoryTitle, taxNumber } =
|
||||
company;
|
||||
const addressString = [
|
||||
|
|
@ -81,6 +84,13 @@ export default function SponsorsAdvertiseRequestFormReviewSection({
|
|||
0,
|
||||
);
|
||||
|
||||
const discountedAmount = promoCode?.percentOff
|
||||
? getDiscountedPrice({
|
||||
percentOff: promoCode.percentOff,
|
||||
price: totalAmount,
|
||||
})
|
||||
: totalAmount;
|
||||
|
||||
useEffect(() => {
|
||||
if (isReadonly) {
|
||||
return;
|
||||
|
|
@ -105,7 +115,7 @@ export default function SponsorsAdvertiseRequestFormReviewSection({
|
|||
authorizedSignatoryName={signatoryName}
|
||||
authorizedSignatoryTitle={signatoryTitle}
|
||||
contactEmails={emails}
|
||||
totalAmount={totalAmount}
|
||||
totalAmount={discountedAmount}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
@ -184,10 +194,20 @@ export default function SponsorsAdvertiseRequestFormReviewSection({
|
|||
</div>
|
||||
);
|
||||
})}
|
||||
{promoCode?.code && (
|
||||
<div className="flex justify-between">
|
||||
<Text color="secondary" size="body2">
|
||||
{promoCode.code}
|
||||
</Text>
|
||||
<Text color="secondary" size="body2" weight="medium">
|
||||
-${totalAmount - discountedAmount}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<Divider className="my-2" />
|
||||
<div className="flex justify-end">
|
||||
<Text color="secondary" size="body2" weight="medium">
|
||||
${totalAmount}
|
||||
${discountedAmount}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -49,3 +49,8 @@ export type SponsorsCompanyDetails = Readonly<{
|
|||
signatoryTitle: string;
|
||||
taxNumber?: string;
|
||||
}>;
|
||||
|
||||
export type SponsorsPromoCode = Readonly<{
|
||||
code: string;
|
||||
percentOff: number;
|
||||
} | null>;
|
||||
|
|
|
|||
|
|
@ -4,12 +4,17 @@ import { v4 as uuidv4 } from 'uuid';
|
|||
|
||||
import { useGreatStorageLocal } from '~/hooks/useGreatStorageLocal';
|
||||
|
||||
import type { SponsorsAdFormatFormItem, SponsorsCompanyDetails } from './types';
|
||||
import type {
|
||||
SponsorsAdFormatFormItem,
|
||||
SponsorsCompanyDetails,
|
||||
SponsorsPromoCode,
|
||||
} from './types';
|
||||
|
||||
export type AdvertiseRequestFormValues = Readonly<{
|
||||
ads: Array<SponsorsAdFormatFormItem>;
|
||||
company: SponsorsCompanyDetails | null;
|
||||
emails: Array<string>;
|
||||
promoCode: SponsorsPromoCode;
|
||||
sessionId: string;
|
||||
}>;
|
||||
|
||||
|
|
@ -28,6 +33,7 @@ export default function useSponsorsAdvertiseRequestFormData(
|
|||
ads: [],
|
||||
company: null,
|
||||
emails: [],
|
||||
promoCode: null,
|
||||
sessionId: uuidv4(),
|
||||
}),
|
||||
{ ttl: 7 * 24 * 60 * 60 },
|
||||
|
|
@ -39,6 +45,7 @@ export default function useSponsorsAdvertiseRequestFormData(
|
|||
ads: [],
|
||||
company: null,
|
||||
emails: [],
|
||||
promoCode: null,
|
||||
sessionId: uuidv4(),
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ export const EmailsItemConfigSponsorsAdRequestReview: EmailItemConfig<
|
|||
signatoryTitle: 'CEO',
|
||||
},
|
||||
from: {
|
||||
email: 'hello@greatfrontend',
|
||||
email: 'hello@greatfrontend.com',
|
||||
name: 'GreatFrontEnd Sponsorships',
|
||||
},
|
||||
id: 'SPONSORS_AD_REQUEST_REVIEW',
|
||||
|
|
|
|||
|
|
@ -56,12 +56,14 @@ export async function sendSponsorsAdRequestReviewEmail({
|
|||
adRequestId,
|
||||
ads,
|
||||
legalName,
|
||||
percentOff,
|
||||
signatoryName,
|
||||
signatoryTitle,
|
||||
}: Readonly<{
|
||||
adRequestId: string;
|
||||
ads: Array<SponsorsAdFormatFormItem>;
|
||||
legalName: string;
|
||||
percentOff?: number | null;
|
||||
signatoryName: string;
|
||||
signatoryTitle: string;
|
||||
}>) {
|
||||
|
|
@ -75,6 +77,7 @@ export async function sendSponsorsAdRequestReviewEmail({
|
|||
props: {
|
||||
ads,
|
||||
legalName,
|
||||
percentOff,
|
||||
requestUrl: url.format({
|
||||
host: getSiteOrigin({ usePreviewForDev: true }),
|
||||
pathname: `/admin/sponsorships/request/${adRequestId}`,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
import { lowerCase, startCase } from 'lodash-es';
|
||||
import React from 'react';
|
||||
|
||||
import { getDiscountedPrice } from '~/components/purchase/PurchasePricingUtils';
|
||||
import type { SponsorsAdFormatFormItem } from '~/components/sponsors/request/types';
|
||||
import { SponsorAdFormatConfigs } from '~/components/sponsors/SponsorsAdFormatConfigs';
|
||||
import {
|
||||
|
|
@ -27,6 +28,7 @@ import { containerStyle, mainStyle } from '~/emails/components/EmailsStyles';
|
|||
type Props = Readonly<{
|
||||
ads: Array<SponsorsAdFormatFormItem>;
|
||||
legalName: string;
|
||||
percentOff?: number | null;
|
||||
requestUrl: string;
|
||||
signatoryName: string;
|
||||
signatoryTitle: string;
|
||||
|
|
@ -35,6 +37,7 @@ type Props = Readonly<{
|
|||
export default function EmailsTemplateSponsorsAdRequestReview({
|
||||
ads,
|
||||
legalName,
|
||||
percentOff,
|
||||
requestUrl,
|
||||
signatoryName,
|
||||
signatoryTitle,
|
||||
|
|
@ -42,7 +45,13 @@ export default function EmailsTemplateSponsorsAdRequestReview({
|
|||
const totalAmount = ads.reduce(
|
||||
(acc, curr) =>
|
||||
acc +
|
||||
curr.weeks.length * SponsorAdFormatConfigs[curr.format].pricePerWeekUSD,
|
||||
curr.weeks.length *
|
||||
(percentOff
|
||||
? getDiscountedPrice({
|
||||
percentOff,
|
||||
price: SponsorAdFormatConfigs[curr.format].pricePerWeekUSD,
|
||||
})
|
||||
: SponsorAdFormatConfigs[curr.format].pricePerWeekUSD),
|
||||
0,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -276,84 +276,94 @@ export const sponsorsRouter = router({
|
|||
agreement: z.string(),
|
||||
company: sponsorsCompanySchemaServer,
|
||||
emails: z.array(z.string().email()),
|
||||
promoCode: z
|
||||
.object({
|
||||
code: z.string(),
|
||||
percentOff: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ input: { ads, agreement, company, emails } }) => {
|
||||
const { address, legalName, signatoryName, signatoryTitle, taxNumber } =
|
||||
company;
|
||||
.mutation(
|
||||
async ({ input: { ads, agreement, company, emails, promoCode } }) => {
|
||||
const { address, legalName, signatoryName, signatoryTitle, taxNumber } =
|
||||
company;
|
||||
|
||||
const adRequest = await prisma.sponsorsAdRequest.create({
|
||||
data: {
|
||||
address,
|
||||
ads: {
|
||||
create: ads.map((ad) => {
|
||||
const adData = (() => {
|
||||
switch (ad.format) {
|
||||
case 'GLOBAL_BANNER':
|
||||
return {};
|
||||
case 'IN_CONTENT':
|
||||
return {
|
||||
body: ad.body,
|
||||
imageUrl: ad.imageUrl,
|
||||
};
|
||||
case 'SPOTLIGHT':
|
||||
return {
|
||||
imageUrl: ad.imageUrl,
|
||||
};
|
||||
default:
|
||||
throw new Error('Invalid ad format');
|
||||
}
|
||||
})();
|
||||
const adRequest = await prisma.sponsorsAdRequest.create({
|
||||
data: {
|
||||
address,
|
||||
ads: {
|
||||
create: ads.map((ad) => {
|
||||
const adData = (() => {
|
||||
switch (ad.format) {
|
||||
case 'GLOBAL_BANNER':
|
||||
return {};
|
||||
case 'IN_CONTENT':
|
||||
return {
|
||||
body: ad.body,
|
||||
imageUrl: ad.imageUrl,
|
||||
};
|
||||
case 'SPOTLIGHT':
|
||||
return {
|
||||
imageUrl: ad.imageUrl,
|
||||
};
|
||||
default:
|
||||
throw new Error('Invalid ad format');
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
format: ad.format,
|
||||
sponsorName: ad.sponsorName,
|
||||
title: ad.text,
|
||||
url: ad.url,
|
||||
...adData,
|
||||
slots: {
|
||||
create: Array.from(ad.weeks).map((slot) => {
|
||||
const [year, week] = slot
|
||||
.split('/')
|
||||
.map((part) => Number(part));
|
||||
return {
|
||||
format: ad.format,
|
||||
sponsorName: ad.sponsorName,
|
||||
title: ad.text,
|
||||
url: ad.url,
|
||||
...adData,
|
||||
slots: {
|
||||
create: Array.from(ad.weeks).map((slot) => {
|
||||
const [year, week] = slot
|
||||
.split('/')
|
||||
.map((part) => Number(part));
|
||||
|
||||
return {
|
||||
week,
|
||||
year,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}),
|
||||
return {
|
||||
week,
|
||||
year,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
agreement,
|
||||
emails,
|
||||
legalName,
|
||||
promoCode: promoCode?.code,
|
||||
signatoryName,
|
||||
signatoryTitle,
|
||||
taxNumber,
|
||||
},
|
||||
agreement,
|
||||
emails,
|
||||
legalName,
|
||||
signatoryName,
|
||||
signatoryTitle,
|
||||
taxNumber,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Send email to advertiser and sponsor manager
|
||||
await Promise.all([
|
||||
sendSponsorsAdRequestConfirmationEmail({
|
||||
adRequestId: adRequest.id,
|
||||
email: emails[0],
|
||||
signatoryName,
|
||||
}),
|
||||
sendSponsorsAdRequestReviewEmail({
|
||||
adRequestId: adRequest.id,
|
||||
ads: ads.map((ad) => ({
|
||||
...ad,
|
||||
id: new Date().toISOString(),
|
||||
})),
|
||||
legalName,
|
||||
signatoryName,
|
||||
signatoryTitle,
|
||||
}),
|
||||
]);
|
||||
}),
|
||||
// Send email to advertiser and sponsor manager
|
||||
await Promise.all([
|
||||
sendSponsorsAdRequestConfirmationEmail({
|
||||
adRequestId: adRequest.id,
|
||||
email: emails[0],
|
||||
signatoryName,
|
||||
}),
|
||||
sendSponsorsAdRequestReviewEmail({
|
||||
adRequestId: adRequest.id,
|
||||
ads: ads.map((ad) => ({
|
||||
...ad,
|
||||
id: new Date().toISOString(),
|
||||
})),
|
||||
legalName,
|
||||
percentOff: promoCode?.percentOff ?? 0,
|
||||
signatoryName,
|
||||
signatoryTitle,
|
||||
}),
|
||||
]);
|
||||
},
|
||||
),
|
||||
adRequestInquiries: adminProcedure.query(async () => {
|
||||
const aplQuery = `
|
||||
['events']
|
||||
|
|
@ -518,12 +528,26 @@ export const sponsorsRouter = router({
|
|||
company: sponsorsCompanySchemaServer,
|
||||
emails: z.array(z.string()),
|
||||
id: z.string(),
|
||||
promoCode: z
|
||||
.object({
|
||||
code: z.string(),
|
||||
percentOff: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(
|
||||
async ({
|
||||
ctx: { viewer },
|
||||
input: { ads, advertiserEmail, agreement, company, emails, id },
|
||||
input: {
|
||||
ads,
|
||||
advertiserEmail,
|
||||
agreement,
|
||||
company,
|
||||
emails,
|
||||
id,
|
||||
promoCode,
|
||||
},
|
||||
}) => {
|
||||
// If not admin, check if the user is authorized to update the request
|
||||
if (!(viewer && ADMIN_EMAILS.includes(viewer.email))) {
|
||||
|
|
@ -553,6 +577,7 @@ export const sponsorsRouter = router({
|
|||
agreement,
|
||||
emails,
|
||||
legalName,
|
||||
promoCode: promoCode?.code,
|
||||
signatoryName,
|
||||
signatoryTitle,
|
||||
taxNumber,
|
||||
|
|
|
|||
Loading…
Reference in New Issue