diff --git a/src/app/Landing/ArticlesIntroTabs/ChangelogTab/ChangelogDemo/EmotionBar.tsx b/src/app/Landing/ArticlesIntroTabs/ChangelogTab/ChangelogDemo/EmotionBar.tsx index f932ca1db..10dbba821 100644 --- a/src/app/Landing/ArticlesIntroTabs/ChangelogTab/ChangelogDemo/EmotionBar.tsx +++ b/src/app/Landing/ArticlesIntroTabs/ChangelogTab/ChangelogDemo/EmotionBar.tsx @@ -7,7 +7,7 @@ import { Count, } from '../../../styles/articles_intro_tabs/changelog_tab/changelog_demo/emotion_bar' -const EMOTION_STATIC = '/icons/static/emotion' +const EMOTION_STATIC = 'icons/emotion' const EditorPreview: FC = () => { return ( diff --git a/src/app/Landing/index.tsx b/src/app/Landing/index.tsx index cf8bbb346..fcae303d0 100644 --- a/src/app/Landing/index.tsx +++ b/src/app/Landing/index.tsx @@ -1,3 +1,5 @@ +'use client' + /* * * LandingPage * @@ -13,7 +15,7 @@ import useMetric from '@/hooks/useMetric' import { ROUTE } from '@/constant/route' import LavaLampLoading from '@/widgets/Loading/LavaLampLoading' -import { DesktopOnly, MobileOnly, LinkAble } from '@/widgets/Common' +import { DesktopOnly, LinkAble } from '@/widgets/Common' import Tooltip from '@/widgets/Tooltip' import FaqList from '@/widgets/FaqList' import HomeHeader from '@/widgets/HomeHeader' diff --git a/src/app/page.tsx b/src/app/page.tsx index f50f5e844..ff6322a85 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,3 @@ -'use client' - import { memo } from 'react' import Landing from './Landing' diff --git a/src/app/providers/RootStoreProvider.tsx b/src/app/providers/RootStoreProvider.tsx index 47c2dd128..25bae67d4 100644 --- a/src/app/providers/RootStoreProvider.tsx +++ b/src/app/providers/RootStoreProvider.tsx @@ -7,6 +7,7 @@ import { enableStaticRendering } from 'mobx-react-lite' import { useStore } from '@/stores/init' import { + useThemeFromURL, useMetric, useCommunity, useTags, @@ -43,10 +44,14 @@ const RootStoreWrapper: FC = ({ children }) => { const { groupedKanbanPosts } = useGroupedKanbanPosts(userHasLogin) const { tags } = useTags() - const wallpaper = useWallpaper(community) const dashboard = useDashboard(community) + const wallpaper = useWallpaper(community) const filterSearchParams = useFilterSearchParams() + // NOTE: 目前在没有启动后端的情况下,如果这行代码出现在 useCommunity 之前,会导致 build 后的代码疯狂 + // post 到 /GraphiQL, 奇怪的行为。。,很怀疑是 URQL 客户端的 Bug .. + const theme = useThemeFromURL() + const store = useStore({ metric, articles: { @@ -65,8 +70,11 @@ const RootStoreWrapper: FC = ({ children }) => { changelog, activeThread, }, - dashboardThread: dashboard, wallpaperEditor: wallpaper, + dashboardThread: dashboard, + theme: { + curTheme: theme, + }, }) console.log('## root store provider') diff --git a/src/app/queries/helper.ts b/src/app/queries/helper.ts index 0e6f95162..371b00e88 100644 --- a/src/app/queries/helper.ts +++ b/src/app/queries/helper.ts @@ -46,7 +46,7 @@ export const commonRes = (result): TGQSSRResult => { export const useIsStaticQuery = (): boolean => { const pathname = usePathname() - return startsWith('/_next/static', pathname) + return startsWith('/_next', pathname) || startsWith('/_vercel', pathname) } export const useCommunityParam = (): string => { diff --git a/src/app/queries/index.ts b/src/app/queries/index.ts index 4532676e6..ebcfdf8f5 100644 --- a/src/app/queries/index.ts +++ b/src/app/queries/index.ts @@ -7,10 +7,11 @@ import { values, includes } from 'ramda' import { useQuery } from '@urql/next' import { usePathname, useSearchParams } from 'next/navigation' -import type { TCommunity, TMetric } from '@/spec' +import type { TCommunity, TMetric, TThemeName } from '@/spec' import { P } from '@/schemas' import { DEFAULT_THEME } from '@/config' import { THREAD, ARTICLE_THREAD } from '@/constant/thread' +import THEME from '@/constant/theme' import METRIC from '@/constant/metric' import URL_PARAM from '@/constant/url_param' import { ARTICLE_CAT, ARTICLE_STATE, ARTICLE_ORDER } from '@/constant/gtd' @@ -41,9 +42,21 @@ import { parseWallpaper, parseDashboard, } from './helper' +import { useMemo } from 'react' export { parseCommunity, useThreadParam } from './helper' +export const useThemeFromURL = (): TThemeName => { + const searchParams = useSearchParams() + const theme = searchParams.get('theme') + + if (theme === THEME.NIGHT) { + return THEME.NIGHT + } + + return THEME.DAY +} + export const useMetric = (): TMetric => { const thread = useThreadParam() const articleParams = useArticleParams() @@ -228,9 +241,13 @@ export const useDashboard = (community: TCommunity): TParseDashboard => { const pathname = usePathname() // @ts-ignore - if (isStaticQuery) return {} + if (isStaticQuery || !community) return {} + + return useMemo(() => { + return parseDashboard(community, pathname) + }, [community.slug]) - return parseDashboard(community, pathname) + // return parseDashboard(community, pathname) } /** diff --git a/src/containers/thread/PostThread/index.tsx b/src/containers/thread/PostThread/index.tsx index 16080cb32..c42b08002 100644 --- a/src/containers/thread/PostThread/index.tsx +++ b/src/containers/thread/PostThread/index.tsx @@ -34,8 +34,6 @@ const PostThread: FC = () => { const postLayout = usePostLayout() const trackerRef = useRef(null) - // if (store.curThread !== THREAD.POST) return - const isMobile = false const showFilters = true diff --git a/src/containers/unit/TagsBar/DesktopView/index.tsx b/src/containers/unit/TagsBar/DesktopView/index.tsx index 94025b435..f724c6ff2 100755 --- a/src/containers/unit/TagsBar/DesktopView/index.tsx +++ b/src/containers/unit/TagsBar/DesktopView/index.tsx @@ -4,12 +4,13 @@ * */ -import { FC } from 'react' +import { FC, useMemo } from 'react' import { keys, reverse } from 'ramda' import { observer } from 'mobx-react-lite' import { buildLog } from '@/logger' +import useViewingCommunity from '@/hooks/useViewingCommunity' import type { TProps as TTagProps } from '..' import { useStore } from '../store' @@ -26,8 +27,14 @@ type TProps = Omit const TagsBar: FC = ({ onSelect }) => { const store = useStore() useInit(store) + const community = useViewingCommunity() + + const { activeTagData, maxDisplayCount, totalCountThrold } = store + + // TODO: thread is also need for deps + const tagsData = useMemo(() => store.tagsData, [community.slug]) + const groupedTags = useMemo(() => store.groupedTags, [community.slug]) - const { groupedTags, tagsData, activeTagData, maxDisplayCount, totalCountThrold } = store const groupsKeys = reverse(keys(groupedTags)) as string[] return ( diff --git a/src/containers/unit/TagsBar/store.ts b/src/containers/unit/TagsBar/store.ts index 3ad242f45..014a8dd58 100755 --- a/src/containers/unit/TagsBar/store.ts +++ b/src/containers/unit/TagsBar/store.ts @@ -4,7 +4,6 @@ */ import { findIndex, propEq } from 'ramda' -import { THREAD } from '@/constant/thread' import type { TRootStore, TCommunity, TTag, TGroupedTags, TThread } from '@/spec' diff --git a/src/hooks/useFooterLinks.ts b/src/hooks/useFooterLinks.ts index b8845cd79..e15bb3513 100644 --- a/src/hooks/useFooterLinks.ts +++ b/src/hooks/useFooterLinks.ts @@ -14,7 +14,8 @@ type TFooterLinks = { const useFooterLinks = (): TFooterLinks => { const { store } = useContext(MobXProviderContext) - const footerlinks = useMemo(() => store.dashboardThread.footerLinksData, []) + const viewingCommunity = store.viewing.community.slug + const footerlinks = useMemo(() => store.dashboardThread.footerLinksData, [viewingCommunity]) return { layout: store.dashboardThread.footerLayout, diff --git a/src/hooks/useHeaderLinks.ts b/src/hooks/useHeaderLinks.ts index 83c8181d4..4f1636697 100644 --- a/src/hooks/useHeaderLinks.ts +++ b/src/hooks/useHeaderLinks.ts @@ -24,10 +24,8 @@ const useHeaderLinks = (): THeaderLinks => { } const { isModerator } = store.accountInfo - const { community } = store.viewing - - const { headerLinks } = store.dashboardThread - const headerLinksRow = toJS(headerLinks) + const viewingCommunity = store.viewing.community.slug + const headerLinksRow = useMemo(() => store.dashboardThread.headerLinksData, [viewingCommunity]) const hasExtraAbout = find((link: TLinkItem) => link.title === '关于', headerLinksRow) @@ -36,7 +34,7 @@ const useHeaderLinks = (): THeaderLinks => { index: 999, title: '关于', group: MORE_GROUP, - link: `/${community.slug}/about`, + link: `/${viewingCommunity}/about`, } : { title: '', index: 0 } @@ -44,11 +42,14 @@ const useHeaderLinks = (): THeaderLinks => { index: 1000, title: '控制台', group: MORE_GROUP, - link: `/${community.slug}/dashboard`, + link: `/${viewingCommunity}/dashboard`, } - const customLinks = isModerator ? [...headerLinksRow, aboutLink, dashboardLink] : headerLinksRow - const headerlinks = useMemo(() => store.dashboardThread.headerLinksData, []) + const customLinks = useMemo(() => { + return isModerator ? [...headerLinksRow, aboutLink, dashboardLink] : headerLinksRow + }, [viewingCommunity]) + + const headerlinks = useMemo(() => store.dashboardThread.headerLinksData, [viewingCommunity]) return { layout: store.dashboardThread.headerLayout, diff --git a/src/hooks/useMetric.ts b/src/hooks/useMetric.ts index 24b4683ac..c242cf123 100644 --- a/src/hooks/useMetric.ts +++ b/src/hooks/useMetric.ts @@ -2,10 +2,12 @@ import { useContext } from 'react' import { MobXProviderContext } from 'mobx-react' import { usePathname } from 'next/navigation' +import { includes } from 'ramda' import type { TMetric } from '@/spec' import METRIC from '@/constant/metric' import { BANNER_LAYOUT } from '@/constant/layout' +import { ROUTE } from '@/constant/route' /** * NOTE: should use observer to wrap the component who use this hook @@ -19,7 +21,7 @@ const useMetric = (): TMetric => { const pathname = usePathname() - if (pathname === '/') { + if (includes(pathname, ['/', `/${ROUTE.PRICE}`, `/${ROUTE.BOOK_DEMO}`])) { return METRIC.HOME } diff --git a/src/hooks/useNameAlias.ts b/src/hooks/useNameAlias.ts index 7c7c51e3c..220f012e0 100644 --- a/src/hooks/useNameAlias.ts +++ b/src/hooks/useNameAlias.ts @@ -16,8 +16,10 @@ const useNameAlias = (group = 'kanban'): Record => { const alias = {} let aliasList = [] + const viewingCommunity = store.viewing.community.slug + // NOTE: 如果这里不用 useMemo,会导致首页切换页面时一直 re-render, 相当变态 - const curAlias = useMemo(() => store.dashboardThread.nameAliasData, []) + const curAlias = useMemo(() => store.dashboardThread.nameAliasData, [viewingCommunity]) if (!group) { aliasList = curAlias @@ -25,9 +27,9 @@ const useNameAlias = (group = 'kanban'): Record => { aliasList = filter((item: TNameAlias) => item.group === group, curAlias) } - aliasList.forEach((item) => { + for (const item of aliasList) { alias[item.slug] = item - }) + } return alias } diff --git a/src/hooks/usePagedChangelogs.ts b/src/hooks/usePagedChangelogs.ts index d3038ac47..aa7c3737d 100644 --- a/src/hooks/usePagedChangelogs.ts +++ b/src/hooks/usePagedChangelogs.ts @@ -1,8 +1,10 @@ -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import { MobXProviderContext } from 'mobx-react' import type { TPagedChangelogs, TResState } from '@/spec' +import { toJS } from '@/mobx' + type TRes = { resState: TResState pagedChangelogs: TPagedChangelogs @@ -18,9 +20,18 @@ const usePagedChangelogs = (): TRes => { throw new Error('Store cannot be null, please add a context provider') } + const viewingCommunity = store.viewing.community.slug + const curPageNumber = store.articles.pagedChangelogs.pageNumber + + const resState = useMemo(() => toJS(store.articles.resState), [viewingCommunity, curPageNumber]) + const pagedChangelogs = useMemo( + () => toJS(store.articles.pagedChangelogs), + [viewingCommunity, curPageNumber], + ) + return { - resState: store.articles.resState, - pagedChangelogs: store.articles.pagedChangelogs, + resState, + pagedChangelogs, } } diff --git a/src/hooks/usePagedPosts.ts b/src/hooks/usePagedPosts.ts index 0b6b86a17..aa13590e3 100644 --- a/src/hooks/usePagedPosts.ts +++ b/src/hooks/usePagedPosts.ts @@ -1,8 +1,10 @@ -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import { MobXProviderContext } from 'mobx-react' import type { TPagedPosts, TResState } from '@/spec' +import { toJS } from '@/mobx' + type TRes = { resState: TResState pagedPosts: TPagedPosts @@ -18,9 +20,18 @@ const usePagedPosts = (): TRes => { throw new Error('Store cannot be null, please add a context provider') } + const viewingCommunity = store.viewing.community.slug + const curPageNumber = store.articles.pagedPosts.pageNumber + + const resState = useMemo(() => toJS(store.articles.resState), [viewingCommunity, curPageNumber]) + const pagedPosts = useMemo( + () => toJS(store.articles.pagedPosts), + [viewingCommunity, curPageNumber], + ) + return { - resState: store.articles.resState, - pagedPosts: store.articles.pagedPosts, + resState, + pagedPosts, } } diff --git a/src/hooks/usePublicThreads.ts b/src/hooks/usePublicThreads.ts index ee3b9f431..8c8612a1f 100644 --- a/src/hooks/usePublicThreads.ts +++ b/src/hooks/usePublicThreads.ts @@ -1,4 +1,4 @@ -import { useContext } from 'react' +import { useContext, useMemo } from 'react' import { MobXProviderContext } from 'mobx-react' import { find, propEq, reject } from 'ramda' @@ -18,15 +18,18 @@ const usePublicThreads = (): TCommunityThread[] => { throw new Error('Store cannot be null, please add a context provider') } - const { community } = store.viewing - const { enable, nameAlias } = store.dashboardThread + const viewingCommunity = store.viewing.community.slug + const community = useMemo(() => toJS(store.viewing.community), [viewingCommunity]) + const nameAliasData = useMemo(() => store.dashboardThread.nameAliasData, [viewingCommunity]) + const enable = useMemo(() => store.dashboardThread.enableSettings, [viewingCommunity]) - const { threads } = community + // const { enable, nameAliasData } = store.dashboardThread + const { threads } = community const enabledThreads = sortByIndex(threads.filter((thread) => enable[thread.slug])) const mappedThreads = enabledThreads.map((pThread) => { - const aliasItem = find(propEq(pThread.slug, 'slug'))(nameAlias) as TNameAlias + const aliasItem = find(propEq(pThread.slug, 'slug'))(nameAliasData) as TNameAlias return { ...pThread, @@ -34,10 +37,9 @@ const usePublicThreads = (): TCommunityThread[] => { } }) - const { headerLinks } = store.dashboardThread - const headerLinksRow = toJS(headerLinks) + const headerLinks = useMemo(() => store.dashboardThread.headerLinksData, [viewingCommunity]) - const hasExtraAbout = find((link: TLinkItem) => link.title === '关于', headerLinksRow) + const hasExtraAbout = find((link: TLinkItem) => link.title === '关于', headerLinks) if (hasExtraAbout) { return reject( @@ -45,6 +47,7 @@ const usePublicThreads = (): TCommunityThread[] => { mappedThreads as TCommunityThread[], ) as TCommunityThread[] } + return mappedThreads as TCommunityThread[] } diff --git a/src/middleware.ts b/src/middleware.ts index 3c73d5357..c008810c5 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,24 +1,18 @@ -// middleware.ts import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' -export function middleware(request: NextRequest) { - const url = request.nextUrl.clone() +// import { themeMiddleware } from './middlewares/theme' +import { queryWhitelistMiddleware } from './middlewares/query-whitelist' - // 根据需求定制缓存时间。这里的604800秒相当于一周。 - const CACHE_DURATION = 60 * 60 * 24 * 7 // seconds +export function middleware(request: NextRequest) { + // const response = themeMiddleware(request) + // if (response instanceof NextResponse) return response - if (url.pathname === '/book-demo') { - const response = NextResponse.next() + // whitelist the query parameters to improve CDN caching + const response = queryWhitelistMiddleware(request) + if (response instanceof NextResponse) return response - response.headers.set( - 'Cache-Control', - `public, max-age=${CACHE_DURATION}, s-maxage=${CACHE_DURATION}, stale-while-revalidate=${CACHE_DURATION}`, - ) - return response - } -} + // return response instanceof NextResponse ? response : NextResponse.next() -export const config = { - matcher: '/book-demo', + return NextResponse.next() } diff --git a/src/middlewares/query-whitelist.ts b/src/middlewares/query-whitelist.ts new file mode 100644 index 000000000..6cb55bfd7 --- /dev/null +++ b/src/middlewares/query-whitelist.ts @@ -0,0 +1,38 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +// 白名单参数数组 +const ALLOWED_PARAMS = ['theme'] + +export function queryWhitelistMiddleware(req: NextRequest) { + const url = new URL(req.url) + let hasUnallowedParams = false + + // 检查URL中的查询参数是否都在白名单中 + url.searchParams.forEach((value, key) => { + if (!ALLOWED_PARAMS.includes(key)) { + hasUnallowedParams = true + } + }) + + // 如果发现不在白名单中的查询参数,重设URL查询参数,只包括白名单内的 + if (hasUnallowedParams) { + const searchParams = new URLSearchParams() + + for (const param in ALLOWED_PARAMS) { + const value = url.searchParams.get(ALLOWED_PARAMS[param]) + if (value !== null) { + searchParams.set(ALLOWED_PARAMS[param], value) + } + } + + // 重构URL的查询部分 + const newUrl = `${url.origin + url.pathname}?${searchParams.toString()}` + + // 重写URL + return NextResponse.rewrite(newUrl) + } + + // 如果所有查询参数都在白名单内或者没有查询参数,保持原样 + return NextResponse.next() +} diff --git a/src/middlewares/theme.ts b/src/middlewares/theme.ts new file mode 100644 index 000000000..fd47bc58a --- /dev/null +++ b/src/middlewares/theme.ts @@ -0,0 +1,34 @@ +import { NextResponse } from 'next/server' +import type { NextRequest } from 'next/server' + +import THEME from '@/constant/theme' + +// 使用正则表达式来匹配需要排除的路径 +// 使用正则表达式来匹配需要排除的路径,包括所有.js文件和图片文件类型 +const excludedPaths = /\.(js|png|jpg|jpeg|gif|svg|webp|ico)$/ + +export function themeMiddleware(request: NextRequest) { + // 如果请求的路径匹配排除路径,则直接放行 + + if (excludedPaths.test(request.nextUrl.pathname)) { + return NextResponse.next() + } + + // 读取cookie中的theme值 + const themeCookie = request.cookies.get('theme') + + // 如果theme值为'night',则重写URL + if (themeCookie && themeCookie.value === THEME.NIGHT) { + // 获取请求的URL + const url = request.nextUrl.clone() + + // 为URL添加查询参数?theme=night + url.searchParams.set('theme', THEME.NIGHT) + + // 重写请求的URL + return NextResponse.rewrite(url) + } + + // 如果theme不是'night'或路径被排除,中间件不做任何操作 + return NextResponse.next() +} diff --git a/src/stores/AccountStore/index.ts b/src/stores/AccountStore/index.ts index 04888221f..540871c46 100755 --- a/src/stores/AccountStore/index.ts +++ b/src/stores/AccountStore/index.ts @@ -3,7 +3,7 @@ * */ -import { mergeRight, clone, remove, insert, findIndex, propEq, includes } from 'ramda' +import { mergeRight, remove, insert, findIndex, propEq, includes } from 'ramda' import type { TRootStore, TAccount, TCommunity, TPagedCommunities, TModerator } from '@/spec' import { T, getParent, markStates, Instance, toJS } from '@/mobx' @@ -21,17 +21,24 @@ const AccountStore = T.model('AccountStore', { const root = getParent(self) as TRootStore return toJS(root.viewing.community) }, - get accountInfo(): TAccount { + get isModerator(): boolean { const slf = self as TStore + const { user, curCommunity, isLogin } = slf + + if (!isLogin) return false - const { user, curCommunity } = slf const moderatorLogins = curCommunity.moderators.map((item: TModerator) => item.user.login) + return includes(user.login, moderatorLogins) + }, + get accountInfo(): TAccount { + const slf = self as TStore + const { user, isModerator } = slf return { ...toJS(user), isLogin: self.isValidSession, isValidSession: self.isValidSession, - isModerator: includes(slf.user.login, moderatorLogins), + isModerator, } }, get subscribedCommunities(): TPagedCommunities { diff --git a/src/widgets/CommunityDigest/HeaderLayout/index.tsx b/src/widgets/CommunityDigest/HeaderLayout/index.tsx index d126f0505..7aff99b40 100644 --- a/src/widgets/CommunityDigest/HeaderLayout/index.tsx +++ b/src/widgets/CommunityDigest/HeaderLayout/index.tsx @@ -30,6 +30,12 @@ const HeaderLayout: FC = () => { const { layout: headerLayout } = useHeaderLinks() const { enterView, leaveView } = useCommunityDigestViewport() + // return ( + // <> + //

this is h3

+ // + // ) + return ( { + diff --git a/src/widgets/CommunityDigest/index.tsx b/src/widgets/CommunityDigest/index.tsx index 09431ce9d..ab938e00e 100644 --- a/src/widgets/CommunityDigest/index.tsx +++ b/src/widgets/CommunityDigest/index.tsx @@ -4,6 +4,7 @@ * */ import { FC, Fragment } from 'react' +import Link from 'next/link' // import { useRouter } from 'next/navigation' import { usePathname } from 'next/navigation' @@ -25,6 +26,16 @@ const CommunityDigest: FC = () => { const bannerLayout = useBannerLayout() const pathname = usePathname() + // return + + // return ( + // <> + // 讨论区 + // 看板 + // + // + // ) + if (pathname.split('/')[2] === 'dashboard') { return } diff --git a/src/widgets/EmotionSelector/SelectedEmotions/EmotionIcon.tsx b/src/widgets/EmotionSelector/SelectedEmotions/EmotionIcon.tsx index b73f47741..22d804208 100755 --- a/src/widgets/EmotionSelector/SelectedEmotions/EmotionIcon.tsx +++ b/src/widgets/EmotionSelector/SelectedEmotions/EmotionIcon.tsx @@ -1,5 +1,4 @@ import { FC, memo } from 'react' -import { ICON } from '@/config' import type { TEmotionType } from '@/spec' import { EIcon } from '../styles/selected_emotions/emotion_icon' @@ -9,7 +8,7 @@ type TProps = { } const EmotionIcon: FC = ({ name }) => { - return + return } export default memo(EmotionIcon)