Skip to content

Commit 72d9363

Browse files
authored
Merge pull request #3478 from qixing-jk/feat/mail-encypt
feat(email encrypt): implement base64 encoding for contact email config
2 parents 3b92d29 + 57ac5e4 commit 72d9363

File tree

17 files changed

+413
-164
lines changed

17 files changed

+413
-164
lines changed

components/CanvasEmail.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useEffect, useRef, useState } from 'react'
2+
3+
const CanvasEmail = ({ email, className = '' }) => {
4+
const canvasRef = useRef(null)
5+
const textRef = useRef(null)
6+
const [isCopied, setIsCopied] = useState(false)
7+
8+
useEffect(() => {
9+
if (!textRef.current || !canvasRef.current) return
10+
11+
const canvas = canvasRef.current
12+
const ctx = canvas.getContext('2d')
13+
const textElement = textRef.current
14+
15+
// Get computed styles from the hidden text element
16+
const style = window.getComputedStyle(textElement)
17+
const font = style.font
18+
const color = style.color
19+
20+
// Set canvas font and measure text
21+
ctx.font = font
22+
const metrics = ctx.measureText(email)
23+
const fontSize = parseInt(style.fontSize)
24+
const lineHeight = fontSize * 1.2
25+
26+
// Set canvas dimensions
27+
const scale = window.devicePixelRatio || 1
28+
canvas.width = metrics.width * scale
29+
canvas.height = lineHeight * scale
30+
canvas.style.width = `${metrics.width}px`
31+
canvas.style.height = `${lineHeight}px`
32+
33+
// Redraw with high DPI support
34+
ctx.scale(scale, scale)
35+
ctx.font = font
36+
ctx.fillStyle = color
37+
ctx.textBaseline = 'top' // Changed to 'top' for better vertical alignment
38+
ctx.fillText(email, 0, 0)
39+
40+
// Handle copy to clipboard
41+
const handleCopy = e => {
42+
e.preventDefault()
43+
navigator.clipboard.writeText(email).then(() => {
44+
setIsCopied(true)
45+
setTimeout(() => setIsCopied(false), 2000)
46+
})
47+
}
48+
49+
canvas.style.cursor = 'pointer'
50+
canvas.addEventListener('click', handleCopy)
51+
return () => canvas.removeEventListener('click', handleCopy)
52+
}, [email])
53+
54+
return (
55+
<span
56+
className={`relative inline-block align-middle ${className}`}
57+
style={{ lineHeight: 'normal' }}>
58+
{/* Hidden span for measuring text metrics */}
59+
<span
60+
ref={textRef}
61+
style={{
62+
position: 'absolute',
63+
visibility: 'hidden',
64+
whiteSpace: 'nowrap',
65+
font: 'inherit',
66+
pointerEvents: 'none',
67+
userSelect: 'none',
68+
lineHeight: 'normal'
69+
}}></span>
70+
71+
{/* Canvas that displays the text */}
72+
<canvas
73+
ref={canvasRef}
74+
className='inline-block align-middle'
75+
style={{
76+
verticalAlign: 'middle',
77+
backgroundColor: 'transparent',
78+
pointerEvents: 'auto',
79+
font: 'inherit',
80+
lineHeight: 'normal',
81+
display: 'inline-block',
82+
userSelect: 'none',
83+
WebkitUserSelect: 'none',
84+
msUserSelect: 'none',
85+
MozUserSelect: 'none',
86+
KhtmlUserSelect: 'none'
87+
}}
88+
title={isCopied ? 'Copied!' : 'Click to copy'}
89+
aria-label={`Email: ${email}`}
90+
/>
91+
</span>
92+
)
93+
}
94+
95+
export default CanvasEmail

conf/contact.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@
33
*/
44
module.exports = {
55
// 社交链接,不需要可留空白,例如 CONTACT_WEIBO:''
6-
CONTACT_EMAIL: process.env.NEXT_PUBLIC_CONTACT_EMAIL || '', // 邮箱地址 例如[email protected]
6+
CONTACT_EMAIL:
7+
(process.env.NEXT_PUBLIC_CONTACT_EMAIL &&
8+
btoa(
9+
unescape(encodeURIComponent(process.env.NEXT_PUBLIC_CONTACT_EMAIL))
10+
)) ||
11+
'', // 邮箱地址 例如[email protected]
712
CONTACT_WEIBO: process.env.NEXT_PUBLIC_CONTACT_WEIBO || '', // 你的微博个人主页
813
CONTACT_TWITTER: process.env.NEXT_PUBLIC_CONTACT_TWITTER || '', // 你的twitter个人主页
914
CONTACT_GITHUB: process.env.NEXT_PUBLIC_CONTACT_GITHUB || '', // 你的github个人主页 例如 https://github.com/tangly1024

lib/notion/getNotionConfig.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getDateValue, getTextContent } from 'notion-utils'
1010
import { deepClone } from '../utils'
1111
import getAllPageIds from './getAllPageIds'
1212
import { getPage } from './getPostBlocks'
13+
import { encryptEmail } from '@/lib/plugins/mailEncrypt'
1314

1415
/**
1516
* 从Notion中读取Config配置表
@@ -157,9 +158,14 @@ export async function getConfigMapFromConfigPage(allPages) {
157158
// 只导入生效的配置
158159
if (config.enable) {
159160
// console.log('[Notion配置]', config.key, config.value)
160-
notionConfig[config.key] =
161-
parseTextToJson(config.value) || config.value || null
162-
// 配置不能是undefined,至少是null
161+
if (config.key === 'CONTACT_EMAIL') {
162+
notionConfig[config.key] =
163+
(config.value && encryptEmail(config.value)) || null
164+
} else {
165+
notionConfig[config.key] =
166+
parseTextToJson(config.value) || config.value || null
167+
// 配置不能是undefined,至少是null
168+
}
163169
}
164170
}
165171
}

lib/plugins/mailEncrypt.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export const handleEmailClick = (e, emailIcon, CONTACT_EMAIL) => {
2+
if (CONTACT_EMAIL && emailIcon && !emailIcon.current.href) {
3+
e.preventDefault()
4+
const email = decryptEmail(CONTACT_EMAIL)
5+
emailIcon.current.href = `mailto:${email}`
6+
emailIcon.current.click()
7+
}
8+
}
9+
10+
export const encryptEmail = email => {
11+
return btoa(unescape(encodeURIComponent(email)))
12+
}
13+
14+
export const decryptEmail = encryptedEmail => {
15+
try {
16+
return decodeURIComponent(escape(atob(encryptedEmail)))
17+
} catch (error) {
18+
console.error('解密邮箱失败:', error)
19+
return encryptedEmail
20+
}
21+
}

lib/rss.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getPostBlocks } from '@/lib/db/getSiteData'
44
import { Feed } from 'feed'
55
import fs from 'fs'
66
import ReactDOMServer from 'react-dom/server'
7+
import { decryptEmail } from '@/lib/plugins/mailEncrypt'
78

89
/**
910
* 生成RSS内容
@@ -37,7 +38,9 @@ export async function generateRss(props) {
3738
const AUTHOR = NOTION_CONFIG?.AUTHOR || BLOG.AUTHOR
3839
const LANG = NOTION_CONFIG?.LANG || BLOG.LANG
3940
const SUB_PATH = NOTION_CONFIG?.SUB_PATH || BLOG.SUB_PATH
40-
const CONTACT_EMAIL = NOTION_CONFIG?.CONTACT_EMAIL || BLOG.CONTACT_EMAIL
41+
const CONTACT_EMAIL = decryptEmail(
42+
NOTION_CONFIG?.CONTACT_EMAIL || BLOG.CONTACT_EMAIL
43+
)
4144

4245
// 检查 feed 文件是否在10分钟内更新过
4346
if (isFeedRecentlyUpdated('./public/rss/feed.xml', 10)) {

themes/commerce/components/Footer.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import CopyRightDate from '@/components/CopyRightDate'
44
import { siteConfig } from '@/lib/config'
55
import SmartLink from '@/components/SmartLink'
66
import CONFIG from '../config'
7+
import { decryptEmail, handleEmailClick } from '@/lib/plugins/mailEncrypt'
8+
import { useRef } from 'react'
9+
import CanvasEmail from '@/components/CanvasEmail'
710

811
/**
912
* 页脚
@@ -18,6 +21,10 @@ const Footer = props => {
1821
parseInt(since) < currentYear ? since + '-' + currentYear : currentYear
1922
const { categoryOptions, customMenu } = props
2023

24+
const CONTACT_EMAIL = siteConfig('CONTACT_EMAIL')
25+
26+
const emailIcon = useRef(null)
27+
2128
return (
2229
<footer
2330
id='footer-wrapper'
@@ -128,14 +135,16 @@ const Footer = props => {
128135
</div>
129136
<div className='text-lg'>
130137
{' '}
131-
{siteConfig('CONTACT_EMAIL') && (
138+
{CONTACT_EMAIL && (
132139
<a
133-
target='_blank'
134-
rel='noreferrer'
135-
title={'email'}
136-
href={`mailto:${siteConfig('CONTACT_EMAIL')}`}>
140+
onClick={e =>
141+
handleEmailClick(e, emailIcon, CONTACT_EMAIL)
142+
}
143+
title='email'
144+
className='cursor-pointer'
145+
ref={emailIcon}>
137146
<i className='transform hover:scale-125 duration-150 fas fa-envelope dark:hover:text-red-400 hover:text-red-600' />{' '}
138-
{siteConfig('CONTACT_EMAIL')}
147+
<CanvasEmail email={decryptEmail(CONTACT_EMAIL)} />
139148
</a>
140149
)}
141150
</div>

themes/commerce/components/SocialButton.js

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,93 @@
11
import { siteConfig } from '@/lib/config'
2+
import { useRef } from 'react'
3+
import { handleEmailClick } from '@/lib/plugins/mailEncrypt'
4+
25
/**
36
* 社交联系方式按钮组
47
* @returns {JSX.Element}
58
* @constructor
69
*/
710
const SocialButton = () => {
11+
const CONTACT_GITHUB = siteConfig('CONTACT_GITHUB')
12+
const CONTACT_TWITTER = siteConfig('CONTACT_TWITTER')
13+
const CONTACT_TELEGRAM = siteConfig('CONTACT_TELEGRAM')
14+
const CONTACT_LINKEDIN = siteConfig('CONTACT_LINKEDIN')
15+
const CONTACT_WEIBO = siteConfig('CONTACT_WEIBO')
16+
const CONTACT_INSTAGRAM = siteConfig('CONTACT_INSTAGRAM')
17+
const CONTACT_EMAIL = siteConfig('CONTACT_EMAIL')
18+
const ENABLE_RSS = siteConfig('ENABLE_RSS')
19+
const CONTACT_BILIBILI = siteConfig('CONTACT_BILIBILI')
20+
const CONTACT_YOUTUBE = siteConfig('CONTACT_YOUTUBE')
21+
22+
const emailIcon = useRef(null)
23+
824
return (
925
<div className='w-full justify-center flex-wrap flex'>
1026
<div className='space-x-3 text-xl text-gray-600 dark:text-gray-300 '>
11-
{siteConfig('CONTACT_GITHUB') && (
27+
{CONTACT_GITHUB && (
1228
<a
1329
target='_blank'
1430
rel='noreferrer'
1531
title={'github'}
16-
href={siteConfig('CONTACT_GITHUB')}>
32+
href={CONTACT_GITHUB}>
1733
<i className='transform hover:scale-125 duration-150 fab fa-github dark:hover:text-red-400 hover:text-red-600' />
1834
</a>
1935
)}
20-
{siteConfig('CONTACT_TWITTER') && (
36+
{CONTACT_TWITTER && (
2137
<a
2238
target='_blank'
2339
rel='noreferrer'
2440
title={'twitter'}
25-
href={siteConfig('CONTACT_TWITTER')}>
41+
href={CONTACT_TWITTER}>
2642
<i className='transform hover:scale-125 duration-150 fab fa-twitter dark:hover:text-red-400 hover:text-red-600' />
2743
</a>
2844
)}
29-
{siteConfig('CONTACT_TELEGRAM') && (
45+
{CONTACT_TELEGRAM && (
3046
<a
3147
target='_blank'
3248
rel='noreferrer'
33-
href={siteConfig('CONTACT_TELEGRAM')}
49+
href={CONTACT_TELEGRAM}
3450
title={'telegram'}>
3551
<i className='transform hover:scale-125 duration-150 fab fa-telegram dark:hover:text-red-400 hover:text-red-600' />
3652
</a>
3753
)}
38-
{siteConfig('CONTACT_LINKEDIN') && (
54+
{CONTACT_LINKEDIN && (
3955
<a
4056
target='_blank'
4157
rel='noreferrer'
42-
href={siteConfig('CONTACT_LINKEDIN')}
58+
href={CONTACT_LINKEDIN}
4359
title={'linkIn'}>
4460
<i className='transform hover:scale-125 duration-150 fab fa-linkedin dark:hover:text-red-400 hover:text-red-600' />
4561
</a>
4662
)}
47-
{siteConfig('CONTACT_WEIBO') && (
63+
{CONTACT_WEIBO && (
4864
<a
4965
target='_blank'
5066
rel='noreferrer'
5167
title={'weibo'}
52-
href={siteConfig('CONTACT_WEIBO')}>
68+
href={CONTACT_WEIBO}>
5369
<i className='transform hover:scale-125 duration-150 fab fa-weibo dark:hover:text-red-400 hover:text-red-600' />
5470
</a>
5571
)}
56-
{siteConfig('CONTACT_INSTAGRAM') && (
72+
{CONTACT_INSTAGRAM && (
5773
<a
5874
target='_blank'
5975
rel='noreferrer'
6076
title={'instagram'}
61-
href={siteConfig('CONTACT_INSTAGRAM')}>
77+
href={CONTACT_INSTAGRAM}>
6278
<i className='transform hover:scale-125 duration-150 fab fa-instagram dark:hover:text-red-400 hover:text-red-600' />
6379
</a>
6480
)}
65-
{siteConfig('CONTACT_EMAIL') && (
81+
{CONTACT_EMAIL && (
6682
<a
67-
target='_blank'
68-
rel='noreferrer'
69-
title={'email'}
70-
href={`mailto:${siteConfig('CONTACT_EMAIL')}`}>
83+
onClick={e => handleEmailClick(e, emailIcon, CONTACT_EMAIL)}
84+
title='email'
85+
className='cursor-pointer'
86+
ref={emailIcon}>
7187
<i className='transform hover:scale-125 duration-150 fas fa-envelope dark:hover:text-red-400 hover:text-red-600' />
7288
</a>
7389
)}
74-
{JSON.parse(siteConfig('ENABLE_RSS')) && (
90+
{ENABLE_RSS && (
7591
<a
7692
target='_blank'
7793
rel='noreferrer'
@@ -80,21 +96,21 @@ const SocialButton = () => {
8096
<i className='transform hover:scale-125 duration-150 fas fa-rss dark:hover:text-red-400 hover:text-red-600' />
8197
</a>
8298
)}
83-
{siteConfig('CONTACT_BILIBILI') && (
99+
{CONTACT_BILIBILI && (
84100
<a
85101
target='_blank'
86102
rel='noreferrer'
87103
title={'bilibili'}
88-
href={siteConfig('CONTACT_BILIBILI')}>
104+
href={CONTACT_BILIBILI}>
89105
<i className='transform hover:scale-125 duration-150 fab fa-bilibili dark:hover:text-red-400 hover:text-red-600' />
90106
</a>
91107
)}
92-
{siteConfig('CONTACT_YOUTUBE') && (
108+
{CONTACT_YOUTUBE && (
93109
<a
94110
target='_blank'
95111
rel='noreferrer'
96112
title={'youtube'}
97-
href={siteConfig('CONTACT_YOUTUBE')}>
113+
href={CONTACT_YOUTUBE}>
98114
<i className='transform hover:scale-125 duration-150 fab fa-youtube dark:hover:text-red-400 hover:text-red-600' />
99115
</a>
100116
)}

themes/fukasawa/components/SocialButton.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import QrCode from '@/components/QrCode'
22
import { siteConfig } from '@/lib/config'
3-
import { useState } from 'react'
3+
import { useRef, useState } from 'react'
4+
import { handleEmailClick } from '@/lib/plugins/mailEncrypt'
45

56
/**
67
* 社交联系方式按钮组
@@ -32,6 +33,9 @@ const SocialButton = () => {
3233
setQrCodeShow(false)
3334
}
3435

36+
const emailIcon = useRef(null)
37+
38+
3539
return (
3640
<div className='w-full justify-center flex-wrap flex'>
3741
<div className='space-x-3 text-xl flex items-center text-gray-600 dark:text-gray-300 '>
@@ -91,10 +95,10 @@ const SocialButton = () => {
9195
)}
9296
{CONTACT_EMAIL && (
9397
<a
94-
target='_blank'
95-
rel='noreferrer'
96-
title={'email'}
97-
href={`mailto:${CONTACT_EMAIL}`}>
98+
onClick={e => handleEmailClick(e, emailIcon, CONTACT_EMAIL)}
99+
title='email'
100+
className='cursor-pointer'
101+
ref={emailIcon}>
98102
<i className='transform hover:scale-125 duration-150 fas fa-envelope dark:hover:text-green-400 hover:text-green-600' />
99103
</a>
100104
)}

0 commit comments

Comments
 (0)