Skip to content

Commit 16bf795

Browse files
suemor233Innei
andauthored
feat: subscribe support (#1582)
* feat: subscribe button * feat: subscribe modal * refactor: subscribe * feat: api Signed-off-by: Innei <tukon479@gmail.com> * fix: button animation Signed-off-by: Innei <tukon479@gmail.com> --------- Signed-off-by: Innei <tukon479@gmail.com> Co-authored-by: Innei <tukon479@gmail.com>
1 parent db2fefd commit 16bf795

File tree

12 files changed

+227
-16
lines changed

12 files changed

+227
-16
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"kami-design": "workspace:@mx-space/kami-design@*",
6464
"@floating-ui/react-dom": "1.3.0",
6565
"@formkit/auto-animate": "1.0.0-beta.6",
66-
"@mx-space/api-client": "1.2.0",
66+
"@mx-space/api-client": "1.3.2",
6767
"axios": "0.27.2",
6868
"clsx": "1.2.1",
6969
"css-spring": "4.1.0",

packages/kami-design/components/Icons/for-footer.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,19 @@ export function FaSolidHeadphonesAlt(props: SVGProps<SVGSVGElement>) {
1818
</svg>
1919
)
2020
}
21+
export function SubscribeOutlined(props: SVGProps<SVGSVGElement>) {
22+
return (
23+
<svg
24+
xmlns="http://www.w3.org/2000/svg"
25+
width="1em"
26+
height="1em"
27+
viewBox="0 0 24 24"
28+
{...props}
29+
>
30+
<path
31+
d="M20.99 14.04V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h10.05c.28 1.92 2.1 3.35 4.18 2.93c1.34-.27 2.43-1.37 2.7-2.71c.25-1.24-.16-2.39-.94-3.18zm-2-9.04L12 8.5L5 5h13.99zm-3.64 10H5V7l7 3.5L19 7v6.05c-.16-.02-.33-.05-.5-.05c-1.39 0-2.59.82-3.15 2zm5.15 2h-4v-1h4v1z"
32+
fill="currentColor"
33+
/>
34+
</svg>
35+
)
36+
}

packages/kami-design/components/Modal/Modal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const Modal = forwardRef<
4949
resolve(null as any)
5050
props.disposer()
5151
}, 300)
52+
props.onClose && props.onClose()
5253
})
5354
}, [props.disposer])
5455

packages/kami-design/components/Modal/stack-context.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { clsx } from 'clsx'
22
import uniqueId from 'lodash-es/uniqueId'
3-
import {
3+
import type {
44
FC,
55
FunctionComponentElement,
66
ReactChildren,
77
ReactElement,
88
ReactNode,
9-
useEffect,
109
} from 'react'
1110
import React, {
1211
createContext,
1312
createElement,
1413
memo,
1514
useContext,
15+
useEffect,
1616
useRef,
1717
useState,
1818
} from 'react'
@@ -198,6 +198,7 @@ export const ModalStackProvider: FC<{
198198
component: $modalElement,
199199
id,
200200
disposer,
201+
useBottomDrawerInMobile,
201202
...rest,
202203
},
203204
]
@@ -260,6 +261,7 @@ export const ModalStackProvider: FC<{
260261
overlayProps,
261262
useBottomDrawerInMobile = true,
262263
} = comp
264+
263265
const extraProps = extraModalPropsMap.get(id)!
264266

265267
const onClose = () => {
@@ -288,7 +290,7 @@ export const ModalStackProvider: FC<{
288290
dismissFnMapRef.current.set(Component, onClose)
289291
return (
290292
<Overlay
291-
center={!isMobileViewport && useBottomDrawerInMobile}
293+
center={!(isMobileViewport && useBottomDrawerInMobile)}
292294
standaloneWrapperClassName={clsx(
293295
isMobileViewport &&
294296
useBottomDrawerInMobile &&

pnpm-lock.yaml

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/layouts/AppLayout.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { FC } from 'react'
44
import { useEffect, useInsertionEffect } from 'react'
55

66
import type { AggregateRoot } from '@mx-space/api-client'
7-
import { ModalStackProvider } from '@mx-space/kami-design/components/Modal/stack-context'
87

98
import { MetaFooter } from '~/components/biz/Meta/footer'
109
import { DynamicHeadMeta } from '~/components/biz/Meta/head'
@@ -22,7 +21,7 @@ import { loadStyleSheet } from '~/utils/load-script'
2221
import { useStore } from '../../store'
2322

2423
export const Content: FC = observer((props) => {
25-
const { userStore: master, appUIStore } = useStore()
24+
const { userStore: master } = useStore()
2625

2726
useScreenMedia()
2827
const { check: checkBrowser } = useCheckOldBrowser()
@@ -56,7 +55,7 @@ export const Content: FC = observer((props) => {
5655
}, [])
5756

5857
return (
59-
<ModalStackProvider isMobileViewport={appUIStore.viewport.mobile}>
58+
<>
6059
<DynamicHeadMeta />
6160
<NextSeo
6261
title={`${initialData.seo.title} · ${initialData.seo.description}`}
@@ -66,6 +65,6 @@ export const Content: FC = observer((props) => {
6665
<div id="next">{props.children}</div>
6766
<Loader />
6867
<MetaFooter />
69-
</ModalStackProvider>
68+
</>
7069
)
7170
})

src/components/layouts/BasicLayout/Footer/actions.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { RootPortal } from '@mx-space/kami-design/components/Portal'
1414
import { ScaleTransitionView } from '@mx-space/kami-design/components/Transition/scale'
1515

16+
import { SubscribeEmail } from '~/components/widgets/Subscribe'
1617
import { TrackerAction } from '~/constants/tracker'
1718
import { useAnalyze } from '~/hooks/use-analyze'
1819
import { useStore } from '~/store'
@@ -101,7 +102,7 @@ export const FooterActions: FC = observer(() => {
101102
)
102103
})}
103104
</TransitionGroup>
104-
105+
<SubscribeEmail />
105106
<button aria-label="open player" onClick={handlePlayMusic}>
106107
<FaSolidHeadphonesAlt />
107108
</button>

src/components/layouts/BasicLayout/index.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import React, {
1010
import type { ShortcutOptions } from 'react-shortcut-guide'
1111
import { ShortcutProvider } from 'react-shortcut-guide'
1212

13+
import { ModalStackProvider } from '@mx-space/kami-design'
1314
import {
1415
BiMoonStarsFill,
1516
PhSunBold,
@@ -113,7 +114,7 @@ export const BasicLayout: FC = observer(({ children }) => {
113114
}, [])
114115
const { event } = useAnalyze()
115116
return (
116-
<>
117+
<ModalStackProvider isMobileViewport={appStore.viewport.mobile}>
117118
<div className="inset-0 fixed bg-fixed pointer-events-none transition-opacity duration-500 ease transform-gpu">
118119
<div className="bg absolute inset-0 transform-gpu" />
119120
</div>
@@ -152,6 +153,6 @@ export const BasicLayout: FC = observer(({ children }) => {
152153

153154
<SearchHotKey />
154155
</Suspense>
155-
</>
156+
</ModalStackProvider>
156157
)
157158
})
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useEffect, useState } from 'react'
2+
3+
import { SubscribeOutlined } from '@mx-space/kami-design/components/Icons'
4+
import { useModalStack } from '@mx-space/kami-design/components/Modal'
5+
6+
import { TrackerAction } from '~/constants/tracker'
7+
import { useAnalyze } from '~/hooks/use-analyze'
8+
9+
import { SubscribeModal } from './modal'
10+
import { useSubscribeStatus } from './query'
11+
12+
export const SubscribeEmail = () => {
13+
const { event } = useAnalyze()
14+
const { present } = useModalStack()
15+
16+
const handleSubscribe = () => {
17+
event({
18+
action: TrackerAction.Click,
19+
label: `底部订阅点击`,
20+
})
21+
const dispose = present({
22+
modalProps: {
23+
title: '邮件订阅',
24+
closeable: true,
25+
useRootPortal: true,
26+
},
27+
overlayProps: {
28+
stopPropagation: true,
29+
darkness: 0.5,
30+
},
31+
component: () => <SubscribeModal onConfirm={dispose} />,
32+
useBottomDrawerInMobile: false,
33+
})
34+
}
35+
36+
const [canSubscribe, setCanSubscribe] = useState(false)
37+
38+
const query = useSubscribeStatus()
39+
40+
useEffect(() => {
41+
const status = query.data?.enable
42+
if (typeof status !== 'boolean') return
43+
setCanSubscribe(status)
44+
}, [query.data])
45+
46+
if (!canSubscribe) return null
47+
48+
return (
49+
<button
50+
aria-label="subscribe"
51+
onClick={handleSubscribe}
52+
className="animate-bubble animate-duration-600 animate-repeat-3"
53+
>
54+
<SubscribeOutlined />
55+
</button>
56+
)
57+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { observer } from 'mobx-react-lite'
2+
import { useReducer } from 'react'
3+
import { message } from 'react-message-popup'
4+
5+
import { Input } from '@mx-space/kami-design'
6+
import { MdiEmailFastOutline } from '@mx-space/kami-design/components/Icons'
7+
8+
import { apiClient } from '~/utils/client'
9+
10+
import { useSubscribeStatus } from './query'
11+
12+
interface SubscribeModalProps {
13+
onConfirm: () => void
14+
}
15+
16+
const subscibeTextMap: Record<string, string> = {
17+
post_c: '文章',
18+
note_c: '笔记',
19+
say_c: '说说',
20+
recently_c: '速记',
21+
}
22+
23+
const initialState = {
24+
email: '',
25+
types: {
26+
post_c: false,
27+
note_c: false,
28+
say_c: false,
29+
recently_c: false,
30+
},
31+
}
32+
33+
type Action =
34+
| { type: 'set'; data: Partial<typeof initialState> }
35+
| { type: 'reset' }
36+
37+
const useFormData = () => {
38+
const [state, dispatch] = useReducer(
39+
(state: typeof initialState, payload: Action) => {
40+
switch (payload.type) {
41+
case 'set':
42+
return { ...state, ...payload.data }
43+
case 'reset':
44+
return initialState
45+
}
46+
},
47+
{ ...initialState },
48+
)
49+
return [state, dispatch] as const
50+
}
51+
52+
export const SubscribeModal = observer<SubscribeModalProps>(({ onConfirm }) => {
53+
const [state, dispatch] = useFormData()
54+
55+
const query = useSubscribeStatus()
56+
57+
const handleSubList = async () => {
58+
if (!state.email) {
59+
message.error('请输入邮箱')
60+
return
61+
}
62+
if (Object.values(state.types).every((type) => !type)) {
63+
message.error('请选择订阅类型')
64+
return
65+
}
66+
const { email, types } = state
67+
await apiClient.subscribe.subscribe(
68+
email,
69+
Object.keys(types).filter((name) => state.types[name]) as any[],
70+
)
71+
72+
message.success('订阅成功')
73+
dispatch({ type: 'reset' })
74+
onConfirm()
75+
}
76+
77+
return (
78+
<form action="#" onSubmit={handleSubList} className="flex flex-col gap-5">
79+
<Input
80+
type="text"
81+
placeholder="留下你的邮箱哦 *"
82+
required
83+
prefix={<MdiEmailFastOutline />}
84+
value={state.email}
85+
onChange={(e) => {
86+
dispatch({ type: 'set', data: { email: e.target.value } })
87+
}}
88+
/>
89+
<div className="flex gap-10">
90+
{Object.keys(state.types)
91+
.filter((type) => query.data?.allowTypes.includes(type as any))
92+
.map((name) => (
93+
<fieldset
94+
className="inline-flex items-center children:cursor-pointer text-lg"
95+
key={name}
96+
>
97+
<input
98+
type="checkbox"
99+
onChange={(e) => {
100+
dispatch({
101+
type: 'set',
102+
data: {
103+
types: {
104+
...state.types,
105+
[name]: e.target.checked,
106+
},
107+
},
108+
})
109+
}}
110+
checked={state.types[name]}
111+
id={name}
112+
/>
113+
<label htmlFor={name} className="text-shizuku">
114+
{subscibeTextMap[name]}
115+
</label>
116+
</fieldset>
117+
))}
118+
</div>
119+
<button className="btn yellow">订阅</button>
120+
</form>
121+
)
122+
})

0 commit comments

Comments
 (0)