Skip to content

Commit c6ce103

Browse files
committed
feat: share modal
Signed-off-by: Innei <[email protected]>
1 parent 571c049 commit c6ce103

File tree

9 files changed

+172
-34
lines changed

9 files changed

+172
-34
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"mermaid": "10.2.3",
6868
"next": "13.4.7",
6969
"next-themes": "0.2.1",
70+
"qrcode.react": "3.1.0",
7071
"react": "18.2.0",
7172
"react-dom": "18.2.0",
7273
"react-intersection-observer": "9.5.1",

pnpm-lock.yaml

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/atoms/url.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { atom, useAtomValue } from 'jotai'
22

33
import { jotaiStore } from '~/lib/store'
4+
import { useAggregationSelector } from '~/providers/root/aggregation-data-provider'
45
import { apiClient } from '~/utils/request'
56

67
export interface UrlConfig {
@@ -9,15 +10,22 @@ export interface UrlConfig {
910
webUrl: string
1011
}
1112

12-
const appUrlAtom = atom<UrlConfig | null>(null)
13+
const adminUrlAtom = atom<string | null>(null)
1314

1415
export const fetchAppUrl = async () => {
1516
const { data } = await apiClient.proxy.options.url.get<{
1617
data: UrlConfig
1718
}>()
1819

19-
jotaiStore.set(appUrlAtom, data)
20+
jotaiStore.set(adminUrlAtom, data.adminUrl)
2021
}
2122

22-
export const getAppUrl = () => jotaiStore.get(appUrlAtom)
23-
export const useAppUrl = () => useAtomValue(appUrlAtom)
23+
export const getAppUrl = () => jotaiStore.get(adminUrlAtom)
24+
export const useAppUrl = () => {
25+
const url = useAggregationSelector((a) => a.url)
26+
const adminUrl = useAtomValue(adminUrlAtom)
27+
return {
28+
adminUrl,
29+
...url,
30+
}
31+
}

src/components/widgets/note/NoteActionAside.tsx

+20-5
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ import {
1313
useCurrentNoteDataSelector,
1414
} from '~/providers/note/CurrentNoteDataProvider'
1515
import { useCurrentNoteId } from '~/providers/note/CurrentNoteIdProvider'
16+
import { useModalStack } from '~/providers/root/modal-stack-provider'
1617
import { isLikedBefore, setLikeId } from '~/utils/cookie'
1718
import { clsxm } from '~/utils/helper'
1819
import { apiClient } from '~/utils/request'
1920

2021
import { DonateButton } from '../shared/DonateButton'
22+
import { ShareModal } from '../shared/ShareModal'
2123

2224
export const NoteActionAside: Component = ({ className }) => {
2325
return (
@@ -107,6 +109,7 @@ const LikeButton = () => {
107109

108110
const ShareButton = () => {
109111
const isClient = useIsClient()
112+
const { present } = useModalStack()
110113

111114
if (!isClient) return null
112115

@@ -120,16 +123,28 @@ const ShareButton = () => {
120123
if (!note) return
121124

122125
const hasShare = 'share' in navigator
126+
127+
const title = '分享一片宝藏文章'
128+
const url = urlBuilder(
129+
routeBuilder(Routes.Note, {
130+
id: note.nid.toString(),
131+
}),
132+
).href
133+
134+
const text = `嘿,我发现了一片宝藏文章「${note.title}」哩,快来看看吧!${url}`
135+
123136
if (hasShare)
124137
navigator.share({
125138
title: note.title,
126139
text: note.text,
127-
url: urlBuilder(
128-
routeBuilder(Routes.Note, {
129-
id: note.nid.toString(),
130-
}),
131-
).href,
140+
url,
141+
})
142+
else {
143+
present({
144+
title: '分享此内容',
145+
content: () => <ShareModal text={text} title={title} url={url} />,
132146
})
147+
}
133148
}}
134149
>
135150
<i className="icon-[mingcute--share-forward-fill] text-[24px] opacity-80 duration-200 hover:text-uk-cyan-light hover:opacity-100" />

src/components/widgets/post/PostActionAside.tsx

+36-23
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,18 @@ import { useIsClient } from '~/hooks/common/use-is-client'
88
import { routeBuilder, Routes } from '~/lib/route-builder'
99
import { toast } from '~/lib/toast'
1010
import { urlBuilder } from '~/lib/url-builder'
11-
import { getCurrentNoteData } from '~/providers/note/CurrentNoteDataProvider'
12-
import { useCurrentNoteId } from '~/providers/note/CurrentNoteIdProvider'
1311
import {
12+
getCurrentPostData,
1413
setCurrentPostData,
1514
useCurrentPostDataSelector,
1615
} from '~/providers/post/CurrentPostDataProvider'
16+
import { useModalStack } from '~/providers/root/modal-stack-provider'
1717
import { isLikedBefore, setLikeId } from '~/utils/cookie'
1818
import { clsxm } from '~/utils/helper'
1919
import { apiClient } from '~/utils/request'
2020

2121
import { DonateButton } from '../shared/DonateButton'
22+
import { ShareModal } from '../shared/ShareModal'
2223

2324
export const PostActionAside: Component = ({ className }) => {
2425
return (
@@ -40,12 +41,12 @@ const LikeButton = () => {
4041
const [update] = useForceUpdate()
4142

4243
const id = useCurrentPostDataSelector((data) => data?.id)
43-
const nid = useCurrentNoteId()
44+
4445
if (!id) return null
4546
const handleLike = () => {
4647
if (isLikedBefore(id)) return
47-
if (!nid) return
48-
apiClient.note.likeIt(id).then(() => {
48+
49+
apiClient.post.thumbsUp(id).then(() => {
4950
setLikeId(id)
5051
setCurrentPostData((draft) => {
5152
draft.count.like += 1
@@ -105,32 +106,44 @@ const LikeButton = () => {
105106
}
106107

107108
const ShareButton = () => {
108-
const hasShare = 'share' in navigator
109109
const isClient = useIsClient()
110-
void useCurrentNoteId()
111-
const note = getCurrentNoteData()?.data
110+
const { present } = useModalStack()
112111

113112
if (!isClient) return null
114-
if (!note) return null
115-
116-
if (!hasShare) {
117-
return null
118-
}
119113

120114
return (
121115
<MotionButtonBase
122-
aria-label="Share this post"
116+
aria-label="Share This Post Button"
123117
className="flex flex-col space-y-2"
124118
onClick={() => {
125-
navigator.share({
126-
title: note.title,
127-
text: note.text,
128-
url: urlBuilder(
129-
routeBuilder(Routes.Note, {
130-
id: note.nid.toString(),
131-
}),
132-
).href,
133-
})
119+
const post = getCurrentPostData()
120+
121+
if (!post) return
122+
123+
const hasShare = 'share' in navigator
124+
125+
const title = '分享一片宝藏文章'
126+
const url = urlBuilder(
127+
routeBuilder(Routes.Post, {
128+
slug: post.slug,
129+
category: post.category.slug,
130+
}),
131+
).href
132+
133+
const text = `嘿,我发现了一片宝藏文章「${post.title}」哩,快来看看吧!${url}`
134+
135+
if (hasShare)
136+
navigator.share({
137+
title: post.title,
138+
text: post.text,
139+
url,
140+
})
141+
else {
142+
present({
143+
title: '分享此内容',
144+
content: () => <ShareModal text={text} title={title} url={url} />,
145+
})
146+
}
134147
}}
135148
>
136149
<i className="icon-[mingcute--share-forward-fill] text-[24px] opacity-80 duration-200 hover:text-uk-cyan-light hover:opacity-100" />

src/components/widgets/post/PostMetaBar.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
'use client'
2+
13
import type { PostModel } from '@mx-space/api-client'
24

35
import { MdiClockOutline } from '~/components/icons/clock'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import dynamic from 'next/dynamic'
2+
import type { FC } from 'react'
3+
4+
import {
5+
IcBaselineTelegram,
6+
MdiTwitter,
7+
} from '~/components/icons/menu-collection'
8+
import { toast } from '~/lib/toast'
9+
import { getAggregationData } from '~/providers/root/aggregation-data-provider'
10+
11+
const QRCodeSVG = dynamic(
12+
() => import('qrcode.react').then((module) => module.QRCodeSVG),
13+
{ ssr: false },
14+
)
15+
16+
const shareList = [
17+
{
18+
name: 'Twitter',
19+
icon: <MdiTwitter className="text-[#1DA1F2]" />,
20+
onClick: (data: ShareData) => {
21+
window.open(
22+
`https://twitter.com/intent/tweet?url=${data.url}&text=${
23+
data.text
24+
}&via=${getAggregationData()?.seo.title}`,
25+
)
26+
},
27+
},
28+
{
29+
name: 'Telegram',
30+
icon: <IcBaselineTelegram className="text-[#2AABEE]" />,
31+
onClick: (data: ShareData) => {
32+
window.open(
33+
`https://telegram.me/share/url?url=${data.url}&text=${data.text}`,
34+
)
35+
},
36+
},
37+
38+
{
39+
name: 'Copy',
40+
icon: <i className="icon-[mingcute--copy-fill]" />,
41+
onClick: (data: ShareData) => {
42+
navigator.clipboard.writeText(data.url)
43+
toast('Copied to clipboard')
44+
},
45+
},
46+
]
47+
48+
interface ShareData {
49+
url: string
50+
title: string
51+
text: string
52+
}
53+
54+
export const ShareModal: FC<ShareData> = ({ url, text, title }) => {
55+
return (
56+
<div className="relative grid grid-cols-[200px_auto] gap-5">
57+
<div className="qrcode inline-block">
58+
<QRCodeSVG
59+
value={url}
60+
className="aspect-square w-[200px]"
61+
height={200}
62+
width={200}
63+
/>
64+
</div>
65+
<div className="share-options flex flex-col gap-2">
66+
分享到...
67+
<ul className="w-[200px] flex-col gap-2 [&>li]:flex [&>li]:items-center [&>li]:space-x-2">
68+
{shareList.map(({ name, icon, onClick }) => (
69+
<li
70+
key={name}
71+
className="flex cursor-pointer items-center space-x-2 rounded-md px-3 py-2 text-lg transition-colors hover:bg-gray-100 dark:hover:bg-gray-800"
72+
aria-label={`Share to ${name}`}
73+
role="button"
74+
onClick={() => onClick({ url, text, title })}
75+
>
76+
{icon}
77+
<span>{name}</span>
78+
</li>
79+
))}
80+
</ul>
81+
</div>
82+
</div>
83+
)
84+
}

src/lib/url-builder.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
import { appConfig } from '~/app.config'
1+
import { aggregationDataAtom } from '~/providers/root/aggregation-data-provider'
2+
3+
import { jotaiStore } from './store'
24

35
export function urlBuilder(path = '') {
4-
return new URL(path, appConfig.site.url)
6+
return new URL(path, jotaiStore.get(aggregationDataAtom)?.url.webUrl)
57
}

src/providers/root/aggregation-data-provider.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,5 @@ export const useAggregationSelector = <T,>(
6161
),
6262
),
6363
)
64+
65+
export const getAggregationData = () => jotaiStore.get(aggregationDataAtom)

0 commit comments

Comments
 (0)