appfunnel 0.7.0 → 0.8.0
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.
- package/dist/index.js +65 -18
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/default/appfunnel.config.ts +2 -0
- package/templates/default/src/components/ConsentDrawer.tsx +70 -0
- package/templates/default/src/components/Header.tsx +37 -0
- package/templates/default/src/components/paywall/PaymentCheckoutDialog.tsx +76 -0
- package/templates/default/src/pages/birthday.tsx +66 -0
- package/templates/default/src/pages/download.tsx +67 -0
- package/templates/default/src/pages/email.tsx +95 -0
- package/templates/default/src/pages/intro.tsx +109 -0
- package/templates/default/src/pages/multi-select.tsx +79 -0
- package/templates/default/src/pages/name.tsx +48 -0
- package/templates/default/src/pages/paywall.tsx +191 -0
- package/templates/default/src/pages/single-select.tsx +61 -0
- package/templates/default/src/pages/upsell.tsx +158 -0
- package/templates/default/template.json +1 -1
- package/templates/default/src/pages/index.tsx +0 -37
- package/templates/default/src/pages/loading.tsx +0 -76
- package/templates/default/src/pages/result.tsx +0 -38
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Drawer, type DrawerHandle, motion } from '@appfunnel-dev/sdk/elements'
|
|
2
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
export interface ConsentDrawerHandle {
|
|
5
|
+
open: () => void
|
|
6
|
+
close: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface ConsentDrawerProps {
|
|
10
|
+
onAccept: () => void
|
|
11
|
+
onDecline: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ConsentDrawer = forwardRef<ConsentDrawerHandle, ConsentDrawerProps>(
|
|
15
|
+
function ConsentDrawer({ onAccept, onDecline }, ref) {
|
|
16
|
+
const drawerRef = useRef<DrawerHandle>(null)
|
|
17
|
+
|
|
18
|
+
useImperativeHandle(
|
|
19
|
+
ref,
|
|
20
|
+
() => ({
|
|
21
|
+
open: () => drawerRef.current?.open(),
|
|
22
|
+
close: () => drawerRef.current?.close(),
|
|
23
|
+
}),
|
|
24
|
+
[]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Drawer
|
|
29
|
+
ref={drawerRef}
|
|
30
|
+
height="95dvh"
|
|
31
|
+
borderRadius={24}
|
|
32
|
+
closeOnOverlayClick={false}
|
|
33
|
+
showHandle={false}
|
|
34
|
+
>
|
|
35
|
+
<div className="p-6 h-full flex flex-col justify-between max-w-[672px] mx-auto w-full">
|
|
36
|
+
<div>
|
|
37
|
+
<h2 className="text-[28px] font-semibold mt-4">
|
|
38
|
+
May we send product updates to your email?
|
|
39
|
+
</h2>
|
|
40
|
+
<p className="text-lg font-semibold mt-4">
|
|
41
|
+
Tips, promotions, and special offers
|
|
42
|
+
</p>
|
|
43
|
+
<p className="text-[15px] text-[#727272] mt-2">
|
|
44
|
+
You can change your mind at any time by clicking the unsubscribe
|
|
45
|
+
link in the footer of any email you receive from us.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
<div className="flex flex-col gap-2 mt-auto pt-4">
|
|
49
|
+
<motion.button
|
|
50
|
+
whileTap={{ scale: 0.95 }}
|
|
51
|
+
transition={{ duration: 0.15 }}
|
|
52
|
+
onClick={onAccept}
|
|
53
|
+
className="w-full py-4 bg-blue-600 text-white font-bold rounded-2xl text-xl"
|
|
54
|
+
>
|
|
55
|
+
Yes, keep me updated
|
|
56
|
+
</motion.button>
|
|
57
|
+
<motion.button
|
|
58
|
+
whileTap={{ scale: 0.95 }}
|
|
59
|
+
transition={{ duration: 0.15 }}
|
|
60
|
+
onClick={onDecline}
|
|
61
|
+
className="w-full py-3 text-lg font-bold text-black/50"
|
|
62
|
+
>
|
|
63
|
+
No thanks
|
|
64
|
+
</motion.button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</Drawer>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useNavigation } from '@appfunnel-dev/sdk'
|
|
2
|
+
|
|
3
|
+
export function Header({ showBack = true }: { showBack?: boolean }) {
|
|
4
|
+
const { progress, goBack } = useNavigation()
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<div className="flex flex-col gap-2 px-4 pt-4 pb-2">
|
|
8
|
+
<div className="flex items-center justify-between">
|
|
9
|
+
<div className="w-10 flex items-center">
|
|
10
|
+
{showBack && (
|
|
11
|
+
<button onClick={goBack} className="p-2 -ml-2">
|
|
12
|
+
<svg
|
|
13
|
+
width="24"
|
|
14
|
+
height="24"
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="#171717"
|
|
18
|
+
strokeWidth="2"
|
|
19
|
+
>
|
|
20
|
+
<path d="M15 18l-6-6 6-6" />
|
|
21
|
+
</svg>
|
|
22
|
+
</button>
|
|
23
|
+
)}
|
|
24
|
+
</div>
|
|
25
|
+
<span className="text-xs font-semibold text-blue-600 whitespace-nowrap">
|
|
26
|
+
{progress.current} / {progress.total}
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
<div className="h-1 bg-gray-100 rounded-full overflow-hidden">
|
|
30
|
+
<div
|
|
31
|
+
className="h-full bg-blue-600 rounded-full transition-all duration-300"
|
|
32
|
+
style={{ width: `${progress.percentage}%` }}
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StripePaymentForm,
|
|
3
|
+
type StripePaymentHandle,
|
|
4
|
+
usePayment,
|
|
5
|
+
} from '@appfunnel-dev/sdk'
|
|
6
|
+
import { Dialog, type DialogHandle, Loading, motion } from '@appfunnel-dev/sdk/elements'
|
|
7
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react'
|
|
8
|
+
|
|
9
|
+
export interface PaymentCheckoutDialogHandle {
|
|
10
|
+
open: () => void
|
|
11
|
+
close: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PaymentCheckoutDialogProps {
|
|
15
|
+
onSuccess: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const PaymentCheckoutDialog = forwardRef<
|
|
19
|
+
PaymentCheckoutDialogHandle,
|
|
20
|
+
PaymentCheckoutDialogProps
|
|
21
|
+
>(function PaymentCheckoutDialog({ onSuccess }, ref) {
|
|
22
|
+
const dialogRef = useRef<DialogHandle>(null)
|
|
23
|
+
const paymentRef = useRef<StripePaymentHandle>(null)
|
|
24
|
+
const { loading, error } = usePayment()
|
|
25
|
+
|
|
26
|
+
useImperativeHandle(
|
|
27
|
+
ref,
|
|
28
|
+
() => ({
|
|
29
|
+
open: () => dialogRef.current?.open(),
|
|
30
|
+
close: () => dialogRef.current?.close(),
|
|
31
|
+
}),
|
|
32
|
+
[]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Dialog
|
|
37
|
+
ref={dialogRef}
|
|
38
|
+
title="Complete payment"
|
|
39
|
+
maxWidth={400}
|
|
40
|
+
borderRadius={16}
|
|
41
|
+
>
|
|
42
|
+
<div className="flex flex-col gap-4 mt-4">
|
|
43
|
+
{error && <p className="text-sm text-red-500">{error}</p>}
|
|
44
|
+
|
|
45
|
+
<StripePaymentForm
|
|
46
|
+
ref={paymentRef}
|
|
47
|
+
onSuccess={() => {
|
|
48
|
+
dialogRef.current?.close()
|
|
49
|
+
onSuccess()
|
|
50
|
+
}}
|
|
51
|
+
appearance={{
|
|
52
|
+
theme: 'flat',
|
|
53
|
+
variables: {
|
|
54
|
+
colorPrimary: '#2563EB',
|
|
55
|
+
borderRadius: '12px',
|
|
56
|
+
},
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<motion.button
|
|
61
|
+
whileTap={!loading ? { scale: 0.95 } : {}}
|
|
62
|
+
transition={{ duration: 0.15 }}
|
|
63
|
+
onClick={() => paymentRef.current?.submit()}
|
|
64
|
+
disabled={loading}
|
|
65
|
+
className={`w-full py-4 rounded-2xl text-base font-bold ${
|
|
66
|
+
loading
|
|
67
|
+
? 'bg-gray-400 text-white cursor-not-allowed'
|
|
68
|
+
: 'bg-blue-600 text-white cursor-pointer'
|
|
69
|
+
}`}
|
|
70
|
+
>
|
|
71
|
+
{loading ? <Loading size="xs" color="#fff" /> : 'Pay now'}
|
|
72
|
+
</motion.button>
|
|
73
|
+
</div>
|
|
74
|
+
</Dialog>
|
|
75
|
+
)
|
|
76
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { definePage, useDateOfBirth, useNavigation } from '@appfunnel-dev/sdk'
|
|
2
|
+
import { motion } from '@appfunnel-dev/sdk/elements'
|
|
3
|
+
import { Header } from '../components/Header'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
|
|
6
|
+
export const page = definePage({
|
|
7
|
+
name: 'Birthday',
|
|
8
|
+
routes: [{ to: 'email' }],
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export default function Birthday() {
|
|
12
|
+
const [dob, setDob] = useDateOfBirth()
|
|
13
|
+
const [display, setDisplay] = useState('')
|
|
14
|
+
const { goToNextPage } = useNavigation()
|
|
15
|
+
|
|
16
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
17
|
+
let val = e.target.value.replace(/[^\d]/g, '')
|
|
18
|
+
if (val.length > 8) val = val.slice(0, 8)
|
|
19
|
+
|
|
20
|
+
let formatted = ''
|
|
21
|
+
if (val.length > 0) formatted = val.slice(0, 2)
|
|
22
|
+
if (val.length > 2) formatted += ' / ' + val.slice(2, 4)
|
|
23
|
+
if (val.length > 4) formatted += ' / ' + val.slice(4, 8)
|
|
24
|
+
|
|
25
|
+
setDisplay(formatted)
|
|
26
|
+
|
|
27
|
+
if (val.length === 8) {
|
|
28
|
+
setDob(val)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const enabled = !!dob?.trim()
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="min-h-screen bg-white flex flex-col max-w-[672px] mx-auto w-full">
|
|
36
|
+
<Header />
|
|
37
|
+
<div className="flex-1 flex flex-col justify-between px-4 pt-6 pb-6">
|
|
38
|
+
<div>
|
|
39
|
+
<h1 className="text-[30px] leading-tight font-bold text-[#171717] mb-6">
|
|
40
|
+
What's your date of birth?
|
|
41
|
+
</h1>
|
|
42
|
+
<input
|
|
43
|
+
type="text"
|
|
44
|
+
value={display}
|
|
45
|
+
onChange={handleChange}
|
|
46
|
+
placeholder="MM / DD / YYYY"
|
|
47
|
+
className="w-full px-4 py-4 rounded-2xl border border-gray-200 text-base text-[#171717] placeholder-gray-400 outline-none focus:border-blue-600 transition-colors"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<motion.button
|
|
51
|
+
whileTap={enabled ? { scale: 0.95 } : {}}
|
|
52
|
+
transition={{ duration: 0.15 }}
|
|
53
|
+
onClick={goToNextPage}
|
|
54
|
+
disabled={!enabled}
|
|
55
|
+
className={`w-full py-4 rounded-2xl text-base font-bold transition-colors ${
|
|
56
|
+
enabled
|
|
57
|
+
? 'bg-blue-600 text-white'
|
|
58
|
+
: 'bg-gray-100 text-gray-400'
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
Continue
|
|
62
|
+
</motion.button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { definePage } from '@appfunnel-dev/sdk'
|
|
2
|
+
|
|
3
|
+
export const page = definePage({
|
|
4
|
+
name: 'Download',
|
|
5
|
+
type: 'finish',
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
export default function Download() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="min-h-screen bg-white flex flex-col items-center justify-center p-6">
|
|
11
|
+
<div className="w-full max-w-[672px] flex flex-col items-center gap-8">
|
|
12
|
+
{/* Success icon */}
|
|
13
|
+
<div className="w-20 h-20 rounded-full bg-blue-600 flex items-center justify-center">
|
|
14
|
+
<svg
|
|
15
|
+
width="40"
|
|
16
|
+
height="40"
|
|
17
|
+
viewBox="0 0 24 24"
|
|
18
|
+
fill="none"
|
|
19
|
+
stroke="white"
|
|
20
|
+
strokeWidth="2.5"
|
|
21
|
+
strokeLinecap="round"
|
|
22
|
+
strokeLinejoin="round"
|
|
23
|
+
>
|
|
24
|
+
<polyline points="20 6 9 17 4 12" />
|
|
25
|
+
</svg>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
{/* Title */}
|
|
29
|
+
<div className="text-center">
|
|
30
|
+
<h1 className="text-[28px] font-bold text-[#171717]">
|
|
31
|
+
You're all set!
|
|
32
|
+
</h1>
|
|
33
|
+
<p className="text-base text-[#727272] mt-2">
|
|
34
|
+
Download the app to get started with your personalized plan.
|
|
35
|
+
</p>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Store buttons */}
|
|
39
|
+
<div className="flex flex-col gap-3 w-full">
|
|
40
|
+
<a
|
|
41
|
+
href="#"
|
|
42
|
+
className="w-full flex items-center justify-center gap-3 py-4 rounded-2xl bg-[#171717] text-white font-bold text-base"
|
|
43
|
+
>
|
|
44
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
|
|
45
|
+
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z" />
|
|
46
|
+
</svg>
|
|
47
|
+
Download on the App Store
|
|
48
|
+
</a>
|
|
49
|
+
<a
|
|
50
|
+
href="#"
|
|
51
|
+
className="w-full flex items-center justify-center gap-3 py-4 rounded-2xl border-2 border-[#171717] text-[#171717] font-bold text-base"
|
|
52
|
+
>
|
|
53
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="#171717">
|
|
54
|
+
<path d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.61 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z" />
|
|
55
|
+
</svg>
|
|
56
|
+
Get it on Google Play
|
|
57
|
+
</a>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Footer */}
|
|
61
|
+
<p className="text-sm text-[#9CA3AF] text-center">
|
|
62
|
+
Check your email for your account details and download links.
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {
|
|
2
|
+
definePage,
|
|
3
|
+
useUser,
|
|
4
|
+
useNavigation,
|
|
5
|
+
} from '@appfunnel-dev/sdk'
|
|
6
|
+
import { motion } from '@appfunnel-dev/sdk/elements'
|
|
7
|
+
import { Header } from '../components/Header'
|
|
8
|
+
import {
|
|
9
|
+
ConsentDrawer,
|
|
10
|
+
type ConsentDrawerHandle,
|
|
11
|
+
} from '../components/ConsentDrawer'
|
|
12
|
+
import { useRef } from 'react'
|
|
13
|
+
|
|
14
|
+
export const page = definePage({
|
|
15
|
+
name: 'Email',
|
|
16
|
+
routes: [{ to: 'paywall' }],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
export default function Email() {
|
|
20
|
+
const { email, setEmail, identify, setMarketingConsent } = useUser()
|
|
21
|
+
const { goToNextPage } = useNavigation()
|
|
22
|
+
const consentRef = useRef<ConsentDrawerHandle>(null)
|
|
23
|
+
|
|
24
|
+
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email || '')
|
|
25
|
+
|
|
26
|
+
const handleSubmit = () => {
|
|
27
|
+
if (!isValid) return
|
|
28
|
+
identify(email!)
|
|
29
|
+
consentRef.current?.open()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="min-h-screen bg-white flex flex-col max-w-[672px] mx-auto w-full">
|
|
34
|
+
<Header />
|
|
35
|
+
<div className="flex-1 flex flex-col justify-between px-4 pt-6 pb-6">
|
|
36
|
+
<div>
|
|
37
|
+
<h1 className="text-[30px] leading-tight font-bold text-[#171717] mb-6">
|
|
38
|
+
What's your email?
|
|
39
|
+
</h1>
|
|
40
|
+
<input
|
|
41
|
+
type="email"
|
|
42
|
+
value={email || ''}
|
|
43
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
44
|
+
placeholder="name@example.com"
|
|
45
|
+
className="w-full px-4 py-4 rounded-2xl border border-gray-200 text-base text-[#171717] placeholder-gray-400 outline-none focus:border-blue-600 transition-colors"
|
|
46
|
+
/>
|
|
47
|
+
|
|
48
|
+
<div className="flex items-start gap-3 bg-gray-100 rounded-xl px-4 py-3 mt-4">
|
|
49
|
+
<svg
|
|
50
|
+
width="16"
|
|
51
|
+
height="16"
|
|
52
|
+
viewBox="0 0 24 24"
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="#171717"
|
|
55
|
+
strokeWidth="2"
|
|
56
|
+
className="mt-0.5 shrink-0"
|
|
57
|
+
>
|
|
58
|
+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
59
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
60
|
+
</svg>
|
|
61
|
+
<span className="text-sm text-[#171717]">
|
|
62
|
+
We respect your privacy and take it seriously. No spam, ever.
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<motion.button
|
|
68
|
+
whileTap={isValid ? { scale: 0.95 } : {}}
|
|
69
|
+
transition={{ duration: 0.15 }}
|
|
70
|
+
onClick={handleSubmit}
|
|
71
|
+
disabled={!isValid}
|
|
72
|
+
className={`w-full py-4 rounded-2xl text-base font-bold transition-colors ${
|
|
73
|
+
isValid
|
|
74
|
+
? 'bg-blue-600 text-white'
|
|
75
|
+
: 'bg-gray-100 text-gray-400'
|
|
76
|
+
}`}
|
|
77
|
+
>
|
|
78
|
+
Continue
|
|
79
|
+
</motion.button>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<ConsentDrawer
|
|
83
|
+
ref={consentRef}
|
|
84
|
+
onAccept={() => {
|
|
85
|
+
setMarketingConsent(true)
|
|
86
|
+
goToNextPage()
|
|
87
|
+
}}
|
|
88
|
+
onDecline={() => {
|
|
89
|
+
setMarketingConsent(false)
|
|
90
|
+
goToNextPage()
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { definePage, useNavigation } from '@appfunnel-dev/sdk'
|
|
2
|
+
import { motion } from '@appfunnel-dev/sdk/elements'
|
|
3
|
+
|
|
4
|
+
export const page = definePage({
|
|
5
|
+
name: 'Intro',
|
|
6
|
+
type: 'default',
|
|
7
|
+
routes: [{ to: 'single-select' }],
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export default function Intro() {
|
|
11
|
+
const { goToNextPage } = useNavigation()
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="min-h-screen bg-white flex flex-col items-center justify-center p-6">
|
|
15
|
+
<div className="w-full max-w-[672px] flex flex-col items-center gap-8">
|
|
16
|
+
{/* Icon */}
|
|
17
|
+
<div className="w-20 h-20 rounded-2xl bg-blue-600 flex items-center justify-center">
|
|
18
|
+
<svg
|
|
19
|
+
width="40"
|
|
20
|
+
height="40"
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
fill="none"
|
|
23
|
+
stroke="white"
|
|
24
|
+
strokeWidth="2"
|
|
25
|
+
strokeLinecap="round"
|
|
26
|
+
strokeLinejoin="round"
|
|
27
|
+
>
|
|
28
|
+
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{/* Text */}
|
|
33
|
+
<div className="flex flex-col items-center gap-3 text-center">
|
|
34
|
+
<h1 className="text-3xl font-bold text-[#171717]">
|
|
35
|
+
Get your personalized plan
|
|
36
|
+
</h1>
|
|
37
|
+
<p className="text-lg text-[#727272] leading-relaxed">
|
|
38
|
+
Answer a few quick questions so we can create a plan tailored just
|
|
39
|
+
for you.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Features */}
|
|
44
|
+
<div className="flex flex-col gap-4 w-full">
|
|
45
|
+
{[
|
|
46
|
+
{ icon: 'clock', text: 'Takes less than 2 minutes' },
|
|
47
|
+
{ icon: 'target', text: 'Personalized to your goals' },
|
|
48
|
+
{ icon: 'shield', text: 'Your data stays private' },
|
|
49
|
+
].map((item) => (
|
|
50
|
+
<div key={item.icon} className="flex items-center gap-3">
|
|
51
|
+
<div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center shrink-0">
|
|
52
|
+
{item.icon === 'clock' && (
|
|
53
|
+
<svg
|
|
54
|
+
width="20"
|
|
55
|
+
height="20"
|
|
56
|
+
viewBox="0 0 24 24"
|
|
57
|
+
fill="none"
|
|
58
|
+
stroke="#2563EB"
|
|
59
|
+
strokeWidth="2"
|
|
60
|
+
>
|
|
61
|
+
<circle cx="12" cy="12" r="10" />
|
|
62
|
+
<polyline points="12 6 12 12 16 14" />
|
|
63
|
+
</svg>
|
|
64
|
+
)}
|
|
65
|
+
{item.icon === 'target' && (
|
|
66
|
+
<svg
|
|
67
|
+
width="20"
|
|
68
|
+
height="20"
|
|
69
|
+
viewBox="0 0 24 24"
|
|
70
|
+
fill="none"
|
|
71
|
+
stroke="#2563EB"
|
|
72
|
+
strokeWidth="2"
|
|
73
|
+
>
|
|
74
|
+
<circle cx="12" cy="12" r="10" />
|
|
75
|
+
<circle cx="12" cy="12" r="6" />
|
|
76
|
+
<circle cx="12" cy="12" r="2" />
|
|
77
|
+
</svg>
|
|
78
|
+
)}
|
|
79
|
+
{item.icon === 'shield' && (
|
|
80
|
+
<svg
|
|
81
|
+
width="20"
|
|
82
|
+
height="20"
|
|
83
|
+
viewBox="0 0 24 24"
|
|
84
|
+
fill="none"
|
|
85
|
+
stroke="#2563EB"
|
|
86
|
+
strokeWidth="2"
|
|
87
|
+
>
|
|
88
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
89
|
+
</svg>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
<span className="text-base text-[#171717]">{item.text}</span>
|
|
93
|
+
</div>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* CTA */}
|
|
98
|
+
<motion.button
|
|
99
|
+
whileTap={{ scale: 0.95 }}
|
|
100
|
+
transition={{ duration: 0.15 }}
|
|
101
|
+
onClick={goToNextPage}
|
|
102
|
+
className="w-full py-4 rounded-2xl bg-blue-600 text-white text-lg font-bold"
|
|
103
|
+
>
|
|
104
|
+
Get Started
|
|
105
|
+
</motion.button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { definePage, useResponse, useNavigation } from '@appfunnel-dev/sdk'
|
|
2
|
+
import { MultiSelect, motion } from '@appfunnel-dev/sdk/elements'
|
|
3
|
+
import { Header } from '../components/Header'
|
|
4
|
+
|
|
5
|
+
export const page = definePage({
|
|
6
|
+
name: 'Interests',
|
|
7
|
+
routes: [{ to: 'name' }],
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export default function Interests() {
|
|
11
|
+
const [interests] = useResponse<string[]>('interests')
|
|
12
|
+
const { goToNextPage } = useNavigation()
|
|
13
|
+
|
|
14
|
+
const hasSelection = interests && interests.length > 0
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen bg-white flex flex-col max-w-[672px] mx-auto w-full">
|
|
18
|
+
<Header />
|
|
19
|
+
<div className="flex-1 flex flex-col px-4 pt-8 pb-6">
|
|
20
|
+
<h1 className="text-[30px] leading-tight font-bold text-[#171717] mb-2">
|
|
21
|
+
What are you most interested in?
|
|
22
|
+
</h1>
|
|
23
|
+
<p className="text-lg text-[#727272] mb-6">Select all that apply</p>
|
|
24
|
+
|
|
25
|
+
<MultiSelect
|
|
26
|
+
responseKey="interests"
|
|
27
|
+
className="flex flex-col gap-3"
|
|
28
|
+
min={1}
|
|
29
|
+
options={[
|
|
30
|
+
{ label: 'Fitness & Health', value: 'fitness' },
|
|
31
|
+
{ label: 'Nutrition & Diet', value: 'nutrition' },
|
|
32
|
+
{ label: 'Mindfulness', value: 'mindfulness' },
|
|
33
|
+
{ label: 'Productivity', value: 'productivity' },
|
|
34
|
+
{ label: 'Finance', value: 'finance' },
|
|
35
|
+
{ label: 'Learning', value: 'learning' },
|
|
36
|
+
]}
|
|
37
|
+
renderItem={({ item, active }) => (
|
|
38
|
+
<div
|
|
39
|
+
className={`w-full px-4 py-4 rounded-2xl text-left transition-all border-2 ${
|
|
40
|
+
active
|
|
41
|
+
? 'border-blue-600 bg-blue-50'
|
|
42
|
+
: 'border-transparent bg-[#F5F5F5]'
|
|
43
|
+
}`}
|
|
44
|
+
>
|
|
45
|
+
<div className="flex items-center gap-3">
|
|
46
|
+
<div
|
|
47
|
+
className={`w-5 h-5 rounded-lg border-2 flex items-center justify-center shrink-0 ${
|
|
48
|
+
active ? 'border-blue-600 bg-blue-600' : 'border-gray-300'
|
|
49
|
+
}`}
|
|
50
|
+
>
|
|
51
|
+
{active && (
|
|
52
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3">
|
|
53
|
+
<polyline points="20 6 9 17 4 12" />
|
|
54
|
+
</svg>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
<span className="text-base font-semibold text-[#171717]">{item.label}</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
<motion.button
|
|
64
|
+
whileTap={hasSelection ? { scale: 0.95 } : {}}
|
|
65
|
+
transition={{ duration: 0.15 }}
|
|
66
|
+
onClick={goToNextPage}
|
|
67
|
+
disabled={!hasSelection}
|
|
68
|
+
className={`w-full py-4 rounded-2xl text-base font-bold transition-colors mt-6 ${
|
|
69
|
+
hasSelection
|
|
70
|
+
? 'bg-blue-600 text-white'
|
|
71
|
+
: 'bg-gray-100 text-gray-400'
|
|
72
|
+
}`}
|
|
73
|
+
>
|
|
74
|
+
Continue
|
|
75
|
+
</motion.button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { definePage, useUser, useNavigation } from '@appfunnel-dev/sdk'
|
|
2
|
+
import { motion } from '@appfunnel-dev/sdk/elements'
|
|
3
|
+
import { Header } from '../components/Header'
|
|
4
|
+
|
|
5
|
+
export const page = definePage({
|
|
6
|
+
name: 'Name',
|
|
7
|
+
routes: [{ to: 'birthday' }],
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
export default function Name() {
|
|
11
|
+
const { name, setName } = useUser()
|
|
12
|
+
const { goToNextPage } = useNavigation()
|
|
13
|
+
|
|
14
|
+
const enabled = !!name?.trim()
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen bg-white flex flex-col max-w-[672px] mx-auto w-full">
|
|
18
|
+
<Header />
|
|
19
|
+
<div className="flex-1 flex flex-col justify-between px-4 pt-6 pb-6">
|
|
20
|
+
<div>
|
|
21
|
+
<h1 className="text-[30px] leading-tight font-bold text-[#171717] mb-6">
|
|
22
|
+
What's your name?
|
|
23
|
+
</h1>
|
|
24
|
+
<input
|
|
25
|
+
type="text"
|
|
26
|
+
value={name || ''}
|
|
27
|
+
onChange={(e) => setName(e.target.value)}
|
|
28
|
+
placeholder="Your name"
|
|
29
|
+
className="w-full px-4 py-4 rounded-2xl border border-gray-200 text-base text-[#171717] placeholder-gray-400 outline-none focus:border-blue-600 transition-colors"
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
<motion.button
|
|
33
|
+
whileTap={enabled ? { scale: 0.95 } : {}}
|
|
34
|
+
transition={{ duration: 0.15 }}
|
|
35
|
+
onClick={goToNextPage}
|
|
36
|
+
disabled={!enabled}
|
|
37
|
+
className={`w-full py-4 rounded-2xl text-base font-bold transition-colors ${
|
|
38
|
+
enabled
|
|
39
|
+
? 'bg-blue-600 text-white'
|
|
40
|
+
: 'bg-gray-100 text-gray-400'
|
|
41
|
+
}`}
|
|
42
|
+
>
|
|
43
|
+
Continue
|
|
44
|
+
</motion.button>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|