@anker-in/campaign-ui 0.5.0-beta.4 → 0.5.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/cjs/components/credits/context/hooks/useCountries.js +1 -1
  2. package/dist/cjs/components/credits/context/hooks/useCountries.js.map +3 -3
  3. package/dist/cjs/components/memberPopup/MemberPopup.js +1 -1
  4. package/dist/cjs/components/memberPopup/MemberPopup.js.map +3 -3
  5. package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.d.ts +10 -0
  6. package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.js +2 -0
  7. package/dist/cjs/components/memberPopup/panels/ActivitiesSubscribePanel.js.map +7 -0
  8. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.d.ts +2 -1
  9. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js +1 -1
  10. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js.map +3 -3
  11. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/index.js +1 -1
  12. package/dist/cjs/components/memberPopup/panels/ConsumeCredits/index.js.map +3 -3
  13. package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.d.ts +11 -0
  14. package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js +2 -0
  15. package/dist/cjs/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js.map +7 -0
  16. package/dist/cjs/components/memberPopup/panels/EarnCredits/SubscribeModal.js +1 -1
  17. package/dist/cjs/components/memberPopup/panels/EarnCredits/SubscribeModal.js.map +2 -2
  18. package/dist/cjs/components/memberPopup/panels/EarnCredits/index.js +1 -1
  19. package/dist/cjs/components/memberPopup/panels/EarnCredits/index.js.map +2 -2
  20. package/dist/cjs/components/memberPopup/panels/Referral/index.js +1 -1
  21. package/dist/cjs/components/memberPopup/panels/Referral/index.js.map +2 -2
  22. package/dist/cjs/components/memberPopup/type.d.ts +14 -2
  23. package/dist/cjs/components/memberPopup/type.js +1 -1
  24. package/dist/cjs/components/memberPopup/type.js.map +2 -2
  25. package/dist/esm/components/credits/context/hooks/useCountries.js +1 -1
  26. package/dist/esm/components/credits/context/hooks/useCountries.js.map +3 -3
  27. package/dist/esm/components/memberPopup/MemberPopup.js +1 -1
  28. package/dist/esm/components/memberPopup/MemberPopup.js.map +3 -3
  29. package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.d.ts +10 -0
  30. package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.js +2 -0
  31. package/dist/esm/components/memberPopup/panels/ActivitiesSubscribePanel.js.map +7 -0
  32. package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.d.ts +2 -1
  33. package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js +1 -1
  34. package/dist/esm/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.js.map +3 -3
  35. package/dist/esm/components/memberPopup/panels/ConsumeCredits/index.js +1 -1
  36. package/dist/esm/components/memberPopup/panels/ConsumeCredits/index.js.map +3 -3
  37. package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.d.ts +11 -0
  38. package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js +2 -0
  39. package/dist/esm/components/memberPopup/panels/EarnCredits/ActivitiesSubscribePanel.js.map +7 -0
  40. package/dist/esm/components/memberPopup/panels/EarnCredits/SubscribeModal.js +1 -1
  41. package/dist/esm/components/memberPopup/panels/EarnCredits/SubscribeModal.js.map +2 -2
  42. package/dist/esm/components/memberPopup/panels/EarnCredits/index.js +1 -1
  43. package/dist/esm/components/memberPopup/panels/EarnCredits/index.js.map +2 -2
  44. package/dist/esm/components/memberPopup/panels/Referral/index.js +1 -1
  45. package/dist/esm/components/memberPopup/panels/Referral/index.js.map +2 -2
  46. package/dist/esm/components/memberPopup/type.d.ts +14 -2
  47. package/dist/esm/components/memberPopup/type.js +1 -1
  48. package/dist/esm/components/memberPopup/type.js.map +2 -2
  49. package/package.json +1 -1
  50. package/src/components/credits/context/hooks/useCountries.ts +1 -2
  51. package/src/components/memberPopup/MemberPopup.tsx +18 -13
  52. package/src/components/memberPopup/panels/ActivitiesSubscribePanel.tsx +194 -0
  53. package/src/components/memberPopup/panels/ConsumeCredits/RedeemCouponPanel.tsx +61 -28
  54. package/src/components/memberPopup/panels/ConsumeCredits/index.tsx +40 -14
  55. package/src/components/memberPopup/panels/EarnCredits/index.tsx +94 -64
  56. package/src/components/memberPopup/panels/Referral/index.tsx +1 -1
  57. package/src/components/memberPopup/type.ts +10 -1
  58. package/src/components/memberPopup/panels/ConsumeCredits/RedeemCouponModal.tsx +0 -150
  59. package/src/components/memberPopup/panels/EarnCredits/SubscribeModal.tsx +0 -144
  60. package/src/components/memberPopup/panels/EarnCredits/SubscribePanel.tsx +0 -147
@@ -84,7 +84,7 @@ function Referral({ copy, onClose }: Props) {
84
84
  <Picture
85
85
  source={copy.image.url}
86
86
  alt={copy.image.alt ?? ''}
87
- className={cn('h-[100px] w-full object-cover', rounded ? 'rounded-[8px]' : 'rounded-none')}
87
+ className={cn('mt-[8px] h-[100px] w-full object-cover', rounded ? 'rounded-[8px]' : 'rounded-none')}
88
88
  />
89
89
  )}
90
90
 
@@ -37,6 +37,8 @@ export enum EarnCreditsTaskType {
37
37
  export enum ConsumeCreditsItemType {
38
38
  Coupon = 'COUPON',
39
39
  RedeemAndBuy = 'REDEEM_AND_BUY',
40
+ GiftCard = 'GIFT_CARD',
41
+ Product = 'PRODUCT',
40
42
  }
41
43
 
42
44
  // ─── 通用 link(统一一种类型) ─────────────────────────
@@ -81,7 +83,7 @@ export type MemberPopupCopy = {
81
83
  shopLabel: string
82
84
  shopUrl?: string
83
85
  }
84
- subscribe?: SubscribeModalCopy
86
+ activitiesSubscribe?: SubscribeModalCopy
85
87
  earnCredits?: EarnCreditsCopy
86
88
  consumeCredits?: ConsumeCreditsopy
87
89
  referral?: ReferralCopy
@@ -141,6 +143,13 @@ export type SubscribeModalCopy = {
141
143
  single_brand_subscribe?: boolean
142
144
  genre?: string
143
145
  sub_brand_type?: string
146
+ promo?: {
147
+ title?: string
148
+ description?: string
149
+ learnMoreLabel?: string
150
+ learnMoreUrl?: string
151
+ image?: { url: string; alt?: string }
152
+ }
144
153
  }
145
154
 
146
155
  // ─── ConsumeCredits Copy ────────────────────────────
@@ -1,150 +0,0 @@
1
- import { useCallback, useEffect, useState } from 'react'
2
- import { Button, Text } from '@anker-in/headless-ui'
3
- import { classNames as cn } from '@anker-in/lib'
4
- import { CreditsModalContainer } from '../../../credits/modal/ModalContainer'
5
- import { AlpcErrorCode } from '../../../credits/context/const'
6
- import { useRedeemCoupon } from './hooks/useRedeemCoupon'
7
- import useCopy from '../../../credits/context/hooks/useCopy'
8
- import type { RedeemCouponModalCopy, AlpcConfig } from '../../type'
9
-
10
- type Props = {
11
- isOpen: boolean
12
- onClose: () => void
13
- itemTitle: string
14
- itemCredits: number
15
- itemImage?: { url: string; alt?: string }
16
- redeemId: number
17
- userId?: string
18
- copy: RedeemCouponModalCopy
19
- pointUnit?: string
20
- alpcConfig: AlpcConfig
21
- onError?: (code: number) => void
22
- }
23
-
24
- export function RedeemCouponModal({
25
- isOpen,
26
- onClose,
27
- itemTitle,
28
- itemCredits,
29
- itemImage,
30
- redeemId,
31
- userId,
32
- copy,
33
- pointUnit = 'pts',
34
- alpcConfig,
35
- onError,
36
- }: Props) {
37
- const [couponCode, setCouponCode] = useState<string>()
38
- const [errorInfo, setErrorInfo] = useState('')
39
- const [disabled, setDisabled] = useState(false)
40
- const { copy: copyText, copied } = useCopy()
41
-
42
- const { isMutating: loading, trigger } = useRedeemCoupon(alpcConfig)
43
-
44
- const handleConfirm = useCallback(async () => {
45
- setErrorInfo('')
46
- const { data } = await trigger({ user_id: userId, rule_id: redeemId })
47
- if (!data) {
48
- setErrorInfo(copy.commonError)
49
- return
50
- }
51
- if (data.data?.success) {
52
- setCouponCode(data.data.coupon_code)
53
- } else {
54
- let msg = copy.commonError
55
- if (data.code === AlpcErrorCode.CodeLpcShopifyCouponRuleRedeemLimit) {
56
- msg = copy.redeemLimitError
57
- setDisabled(true)
58
- } else if (data.code === AlpcErrorCode.CodeLpcRuleInventoryNotEnough) {
59
- msg = copy.inventoryNotEnough
60
- setDisabled(true)
61
- } else if (data.code === AlpcErrorCode.CodeLpcNotEnoughCredits) {
62
- msg = copy.creditsNotEnough
63
- setDisabled(true)
64
- } else if (data.code === AlpcErrorCode.CodeCrossSiteError) {
65
- msg = copy.crossSiteError
66
- setDisabled(true)
67
- }
68
- setErrorInfo(msg)
69
- onError?.(data.code)
70
- }
71
- }, [trigger, userId, redeemId, copy, onError])
72
-
73
- useEffect(() => {
74
- if (!isOpen) {
75
- setCouponCode(undefined)
76
- setErrorInfo('')
77
- setDisabled(false)
78
- }
79
- }, [isOpen])
80
-
81
- return (
82
- <CreditsModalContainer
83
- isOpen={isOpen}
84
- onClose={onClose}
85
- className="w-[540px] md:h-auto"
86
- scrollClassName="mb-[40px] px-0 min-l:px-0 md:mb-[24px]"
87
- titleClassName="h-[56px] bg-[#f5f5f7]"
88
- >
89
- <div className="bg-[#f5f5f7] px-[48px] pb-[24px] md:px-[16px]">
90
- {itemImage?.url && (
91
- <div className="relative mx-auto h-[164px] w-[324px]">
92
- <img src={itemImage.url} alt={itemImage.alt ?? ''} className="h-full w-full object-cover" />
93
- </div>
94
- )}
95
- <p className={cn('mt-[30px] text-center text-[24px] font-bold md:mt-[18px] md:text-[20px]')}>
96
- {itemCredits} {pointUnit}
97
- </p>
98
- </div>
99
-
100
- <div className="bg-white px-[48px] pt-[24px] text-center md:px-[16px]">
101
- {!couponCode ? (
102
- <>
103
- <Text className="block text-[24px] font-bold md:text-[20px]" html={copy.confirmTitle} />
104
- <Button
105
- loading={loading}
106
- variant="primary"
107
- size="lg"
108
- className="mt-[16px] w-full"
109
- onClick={handleConfirm}
110
- disabled={disabled}
111
- >
112
- {copy.confirmButton}
113
- </Button>
114
- </>
115
- ) : (
116
- <>
117
- <div className="flex items-center justify-center gap-[10px]">
118
- <span className="text-[24px] font-bold">{couponCode}</span>
119
- <button
120
- className={cn('text-[16px] font-bold underline', copied && 'pointer-events-none')}
121
- onClick={() => copyText(couponCode)}
122
- >
123
- {copied ? 'Copied' : 'Copy'}
124
- </button>
125
- </div>
126
- <Text className="mb-[4px] mt-[16px] text-[24px] font-bold md:text-[20px]" html={copy.successTitle} />
127
- <Text className="text-[16px] font-bold text-[#6d6d6f] md:text-[14px]" html={copy.successDesc} />
128
- <Button
129
- variant="primary"
130
- size="lg"
131
- className="mt-[16px] w-full"
132
- onClick={() => {
133
- if (copy.successButtonUrl) window.location.href = copy.successButtonUrl
134
- onClose()
135
- }}
136
- >
137
- {copy.successButton}
138
- </Button>
139
- </>
140
- )}
141
- </div>
142
-
143
- {errorInfo && (
144
- <div className="bg-white px-[48px] py-[12px] text-center md:px-[16px]">
145
- <Text className="text-[14px] font-semibold text-[#ff0000]" html={errorInfo} />
146
- </div>
147
- )}
148
- </CreditsModalContainer>
149
- )
150
- }
@@ -1,144 +0,0 @@
1
- import { useCallback, useEffect, useState } from 'react'
2
- import { Button, Checkbox, Text } from '@anker-in/headless-ui'
3
- import { classNames as cn, useHeadlessContext } from '@anker-in/lib'
4
- import Cookies from 'js-cookie'
5
- import { parse } from 'query-string'
6
- import { CreditsModalContainer } from '../../../credits/modal/ModalContainer'
7
- import { emailValidate } from '../../../credits/context/utils'
8
- import { useMemberPopupContext } from '../../context/provider'
9
- import type { SubscribeModalCopy } from '../../type'
10
- import { ROUNDED_BRANDS } from '../../../../constants'
11
-
12
- type Props = {
13
- isOpen: boolean
14
- onClose: () => void
15
- onSuccess?: () => void
16
- copy: SubscribeModalCopy
17
- email?: string
18
- }
19
-
20
- function getAdCookie() {
21
- if (typeof window === 'undefined') return { register_source: '' }
22
- const { search, href } = window.location
23
- const query = parse(search)
24
- return {
25
- fbuy_ref_code: Cookies.get('fbuy_ref_code'),
26
- ref: Cookies.get('ref_ads'),
27
- inviter_code: query.ic ?? query.inviter_code ?? Cookies.get('inviter_code'),
28
- register_source: query.redirect ?? Cookies.get('reg_source') ?? href,
29
- }
30
- }
31
-
32
- async function getRecaptchaToken(sitekey: string): Promise<string> {
33
- if (typeof window === 'undefined' || !window?.grecaptcha?.execute) return ''
34
- if (!sitekey) return ''
35
- try {
36
- return await window.grecaptcha.execute(sitekey, { action: 'activity' })
37
- } catch {
38
- return ''
39
- }
40
- }
41
-
42
- export function EarnCreditsSubscribeModal({ isOpen, onClose, onSuccess, copy, email: initialEmail }: Props) {
43
- const { alpcConfig } = useMemberPopupContext()
44
- const { locale } = alpcConfig
45
- const { brand: headlessBrand, storeDomain, recaptchaSitekey } = useHeadlessContext()
46
- const rounded = ROUNDED_BRANDS.includes(headlessBrand)
47
- const [policy, setPolicy] = useState(false)
48
- const [email, setEmail] = useState(initialEmail ?? '')
49
- const [errorMessage, setErrorMessage] = useState('')
50
- const [successMessage, setSuccessMessage] = useState('')
51
- const [loading, setLoading] = useState(false)
52
-
53
- useEffect(() => {
54
- if (initialEmail) setEmail(initialEmail)
55
- }, [initialEmail])
56
-
57
- const handleSubmit = useCallback(async () => {
58
- setErrorMessage('')
59
- if (!email) return setErrorMessage('Please fill in your email')
60
- if (!emailValidate(email)) return setErrorMessage('Invalid email address')
61
- if (!policy) return setErrorMessage('Please agree to the policy')
62
-
63
- setLoading(true)
64
- const { register_source } = getAdCookie()
65
- const recaptchaToken = await getRecaptchaToken(recaptchaSitekey || '')
66
- try {
67
- const response = await fetch('/api/multipass/rainbowbridge/activities', {
68
- method: 'POST',
69
- headers: {
70
- 'Content-Type': 'application/json',
71
- 'current-language': locale,
72
- 'X-Recaptcha-Token': recaptchaToken,
73
- },
74
- body: JSON.stringify({
75
- email,
76
- register_source,
77
- shopify_domain: storeDomain,
78
- single_brand_subscribe: copy.single_brand_subscribe ?? true,
79
- genre: copy.genre ?? copy.dealsType ?? '',
80
- sub_brand_type: copy.sub_brand_type ?? '',
81
- }),
82
- })
83
- const result = await response.json()
84
- if (!result.errors) {
85
- setSuccessMessage(copy.successTips ?? 'Subscribed successfully!')
86
- onSuccess?.()
87
- setTimeout(() => {
88
- setSuccessMessage('')
89
- onClose()
90
- }, 3000)
91
- } else {
92
- setErrorMessage(result.errors ?? 'Error occurred')
93
- }
94
- } catch {
95
- setErrorMessage('Network error')
96
- } finally {
97
- setLoading(false)
98
- }
99
- }, [email, policy, locale, storeDomain, copy, onSuccess, onClose])
100
-
101
- return (
102
- <CreditsModalContainer
103
- isOpen={isOpen}
104
- onClose={onClose}
105
- className="w-[540px] md:h-auto"
106
- scrollClassName="mb-[40px] md:mb-[24px]"
107
- useAnimation
108
- animationClassName="md:translate-y-[100vh]"
109
- >
110
- <div className="flex flex-col gap-[16px] text-center">
111
- <Text className="text-[22px] font-bold" html={copy.title} />
112
- {copy.desc && (
113
- <Text className="text-left text-[16px] font-semibold text-[#1f2021]" html={copy.desc} />
114
- )}
115
- <div className={cn('flex h-[52px] w-full overflow-hidden md:flex-col md:h-auto md:gap-[16px]', rounded ? 'rounded-[2px]' : 'rounded-none')}>
116
- <input
117
- className={cn('h-full flex-1 border border-[#d8d8d8] px-[14px] text-[16px] outline-none placeholder:text-[#999] md:h-[52px]', rounded ? 'rounded-l-[2px] md:rounded-[2px]' : 'rounded-none')}
118
- placeholder={copy.placeholder}
119
- value={email}
120
- onChange={e => { setErrorMessage(''); setEmail(e.target.value.trim()) }}
121
- />
122
- <Button
123
- variant="primary"
124
- loading={loading}
125
- className={cn('!h-full !min-w-0 !px-[24px] md:!h-[46px]', rounded ? '!rounded-l-none !rounded-r-[2px] md:!rounded-[2px]' : '!rounded-none')}
126
- onClick={handleSubmit}
127
- >
128
- {copy.buttonLabel ?? 'Subscribe'}
129
- </Button>
130
- </div>
131
- <div className="flex w-full items-start gap-[8px]">
132
- <Checkbox checked={policy} onCheckedChange={() => setPolicy(v => !v)} className="border-[#1d1d1f]" />
133
- <label
134
- className="text-left text-[14px] font-semibold text-[#777] [&_a]:underline"
135
- dangerouslySetInnerHTML={{ __html: copy.policy ?? '' }}
136
- />
137
- </div>
138
- <div className={cn('text-left text-[16px] font-semibold', errorMessage && 'text-[#f84d4f]', successMessage && 'text-[#52c41a]')}>
139
- <Text html={successMessage || errorMessage || ''} />
140
- </div>
141
- </div>
142
- </CreditsModalContainer>
143
- )
144
- }
@@ -1,147 +0,0 @@
1
- import { useCallback, useEffect, useState } from 'react'
2
- import { Checkbox, Text } from '@anker-in/headless-ui'
3
- import { classNames as cn, useHeadlessContext } from '@anker-in/lib'
4
- import Cookies from 'js-cookie'
5
- import { parse } from 'query-string'
6
- import { emailValidate } from '../../../credits/context/utils'
7
- import { useMemberPopupContext } from '../../context/provider'
8
- import type { SubscribeModalCopy } from '../../type'
9
- import { ROUNDED_BRANDS } from '../../../../constants'
10
-
11
- type Props = {
12
- copy: SubscribeModalCopy
13
- email?: string
14
- onClose: () => void
15
- onSuccess?: () => void
16
- }
17
-
18
- function getAdCookie() {
19
- if (typeof window === 'undefined') return { register_source: '' }
20
- const { search, href } = window.location
21
- const query = parse(search)
22
- return {
23
- fbuy_ref_code: Cookies.get('fbuy_ref_code'),
24
- ref: Cookies.get('ref_ads'),
25
- inviter_code: query.ic ?? query.inviter_code ?? Cookies.get('inviter_code'),
26
- register_source: query.redirect ?? Cookies.get('reg_source') ?? href,
27
- }
28
- }
29
-
30
- async function getRecaptchaToken(sitekey: string): Promise<string> {
31
- if (typeof window === 'undefined' || !window?.grecaptcha?.execute) return ''
32
- if (!sitekey) return ''
33
- try {
34
- return await window.grecaptcha.execute(sitekey, { action: 'activity' })
35
- } catch {
36
- return ''
37
- }
38
- }
39
-
40
- export default function SubscribePanel({ copy, email: initialEmail, onClose, onSuccess }: Props) {
41
- const { alpcConfig } = useMemberPopupContext()
42
- const { locale } = alpcConfig
43
- const { brand: headlessBrand, storeDomain, recaptchaSitekey } = useHeadlessContext()
44
- const rounded = ROUNDED_BRANDS.includes(headlessBrand)
45
- const [policy, setPolicy] = useState(false)
46
- const [email, setEmail] = useState(initialEmail ?? '')
47
- const [errorMessage, setErrorMessage] = useState('')
48
- const [successMessage, setSuccessMessage] = useState('')
49
- const [loading, setLoading] = useState(false)
50
-
51
- useEffect(() => {
52
- if (initialEmail) setEmail(initialEmail)
53
- }, [initialEmail])
54
-
55
- const handleSubmit = useCallback(async () => {
56
- setErrorMessage('')
57
- if (!email) return setErrorMessage('Please fill in your email')
58
- if (!emailValidate(email)) return setErrorMessage('Invalid email address')
59
- if (!policy) return setErrorMessage('Please agree to the policy')
60
-
61
- setLoading(true)
62
- const { register_source } = getAdCookie()
63
- const recaptchaToken = await getRecaptchaToken(recaptchaSitekey || '')
64
- try {
65
- const response = await fetch('/api/multipass/rainbowbridge/activities', {
66
- method: 'POST',
67
- headers: {
68
- 'Content-Type': 'application/json',
69
- 'current-language': locale,
70
- 'X-Recaptcha-Token': recaptchaToken,
71
- },
72
- body: JSON.stringify({
73
- email,
74
- register_source,
75
- shopify_domain: storeDomain,
76
- single_brand_subscribe: copy.single_brand_subscribe ?? true,
77
- genre: copy.genre ?? copy.dealsType ?? '',
78
- sub_brand_type: copy.sub_brand_type ?? '',
79
- }),
80
- })
81
- const result = await response.json()
82
- if (!result.errors) {
83
- setSuccessMessage(copy.successTips ?? 'Subscribed successfully!')
84
- onSuccess?.()
85
- setTimeout(() => {
86
- setSuccessMessage('')
87
- onClose()
88
- }, 3000)
89
- } else {
90
- setErrorMessage(result.errors ?? 'Error occurred')
91
- }
92
- } catch {
93
- setErrorMessage('Network error')
94
- } finally {
95
- setLoading(false)
96
- }
97
- }, [email, policy, locale, storeDomain, copy, onClose, onSuccess])
98
-
99
- return (
100
- <div className="flex flex-col gap-[12px]">
101
- <div className="flex flex-col gap-[8px]">
102
- {copy.title && (
103
- <h2 className="text-[24px] font-[700] leading-[1.2] text-[#080a0f]">{copy.title}</h2>
104
- )}
105
- {copy.desc && (
106
- <Text className="text-[16px] text-[#1e2024]" html={copy.desc} />
107
- )}
108
- </div>
109
-
110
- <div className="flex flex-col gap-[16px]">
111
- <input
112
- className={cn(
113
- 'h-[46px] w-full border px-[14px] text-[16px] outline-none placeholder:text-[#999]',
114
- errorMessage ? 'border-[#f84d4f]' : 'border-[#d8d8d8]',
115
- rounded ? 'rounded-[4px]' : 'rounded-none'
116
- )}
117
- placeholder={copy.placeholder}
118
- value={email}
119
- onChange={e => { setErrorMessage(''); setEmail(e.target.value.trim()) }}
120
- />
121
-
122
- <div className="flex w-full items-start gap-[8px]">
123
- <Checkbox checked={policy} onCheckedChange={() => setPolicy(v => !v)} className="mt-[2px] border-[#1d1d1f]" />
124
- <label
125
- className="cursor-pointer text-left text-[14px] text-[#1e2024] [&_a]:underline"
126
- onClick={() => setPolicy(v => !v)}
127
- dangerouslySetInnerHTML={{ __html: copy.policy ?? '' }}
128
- />
129
- </div>
130
-
131
- {(errorMessage || successMessage) && (
132
- <div className={cn('text-[14px] font-[700]', errorMessage && 'text-[#f84d4f]', successMessage && 'text-[#52c41a]')}>
133
- <Text html={successMessage || errorMessage} />
134
- </div>
135
- )}
136
-
137
- <button
138
- className={cn('w-full cursor-pointer bg-[#080a0f] py-[14px] text-[14px] font-[700] text-white disabled:opacity-60', rounded ? 'rounded-[4px]' : 'rounded-none')}
139
- disabled={loading}
140
- onClick={handleSubmit}
141
- >
142
- {copy.buttonLabel ?? 'Subscribe'}
143
- </button>
144
- </div>
145
- </div>
146
- )
147
- }