├── .env ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── README.md ├── components ├── AnimationList │ └── index.tsx ├── BlogHead │ ├── blogHeadLeft.tsx │ ├── blogHeadRight.tsx │ └── index.tsx ├── BlogItem │ ├── blogItemLeft.tsx │ ├── blogItemLeftUl.tsx │ ├── blogItemRight.tsx │ ├── index.module.scss │ └── index.tsx ├── BlogMessage │ ├── childMessage.tsx │ ├── index.module.scss │ ├── index.tsx │ └── primaryMessage.tsx ├── BlogUtils │ ├── blogCanvas.tsx │ ├── blogMenu.tsx │ ├── index.module.scss │ └── index.tsx ├── Button │ └── index.tsx ├── CardHead │ └── index.tsx ├── CheckBox │ ├── index.module.scss │ └── index.tsx ├── Drop │ ├── dropContainer.tsx │ ├── dropItem.tsx │ ├── dropSelectItem.tsx │ ├── index.module.scss │ └── index.tsx ├── Footer │ ├── footContainerCardMe.tsx │ ├── footContainerContactMe.tsx │ ├── footContainerContentItem.tsx │ ├── footContainerRecommend.tsx │ ├── footContainerYiYan.tsx │ ├── index.module.scss │ └── index.tsx ├── Head │ └── index.tsx ├── Header │ ├── headContainerButton.tsx │ ├── headContainerItem.tsx │ ├── headContainerList.tsx │ ├── headContainerUser.tsx │ ├── index.module.scss │ └── index.tsx ├── Hover │ ├── animate.tsx │ ├── index.module.scss │ └── index.tsx ├── Image │ └── index.tsx ├── Input │ ├── index.module.scss │ └── index.tsx ├── Layout │ └── index.tsx ├── LoadRender │ ├── index.ts │ └── loadingError.tsx ├── Loading │ ├── index.module.scss │ └── index.tsx ├── LoadingBar │ ├── index.tsx │ └── loadingBar.tsx ├── ModuleManager │ ├── index.module.scss │ └── index.tsx ├── Overlay │ ├── desktop.tsx │ ├── index.module.scss │ ├── index.tsx │ └── mobile.tsx ├── PageFoot │ └── index.tsx ├── Publish │ ├── index.module.scss │ ├── publishEditor.tsx │ ├── publishHead.tsx │ ├── publishState.tsx │ ├── publishTag.tsx │ ├── publishType.tsx │ └── publishTypeAndTag.tsx ├── Tag │ ├── index.module.scss │ └── index.tsx ├── Toast │ ├── index.module.scss │ └── index.tsx ├── Type │ └── index.tsx └── UserHover │ ├── hoverItem.tsx │ ├── index.module.scss │ └── index.tsx ├── config ├── action.ts ├── api.ts ├── archive.ts ├── blogItem.ts ├── footer.ts ├── header.ts ├── highLight.ts ├── home.ts ├── hover.ts ├── manage.ts ├── message.ts ├── publish.ts ├── ssr.ts ├── toast.ts ├── type&tag.ts └── user.ts ├── containers ├── About │ ├── aboutLeft.tsx │ ├── aboutRight.tsx │ ├── aboutRightAbout.tsx │ ├── aboutRightLink.tsx │ └── index.tsx ├── Archive │ ├── archiveContent.tsx │ ├── archiveHead.tsx │ ├── index.module.scss │ └── index.tsx ├── Blog │ ├── blogContentBody.tsx │ ├── blogContentCheckCodeModule.tsx │ ├── blogContentDeleteModule.tsx │ ├── blogContentImg.tsx │ ├── blogContentLike.tsx │ ├── blogContentLikeToPayModule.tsx │ ├── blogContentMessage.tsx │ ├── blogContentMessageChild.tsx │ ├── blogContentMessageMarkdown.tsx │ ├── blogContentMessagePrimary.tsx │ ├── blogContentMessagePut.tsx │ ├── blogContentReplayModule.tsx │ ├── blogContentTypeAndTag.tsx │ ├── blogContentUpdateModule.tsx │ ├── index.module.scss │ └── index.tsx ├── Editor │ ├── editorSubmit.tsx │ └── index.tsx ├── Login │ ├── index.module.scss │ ├── index.tsx │ ├── loginCheckCode.tsx │ ├── loginPassword.tsx │ ├── loginSubmit.tsx │ └── loginUsername.tsx ├── Main │ ├── index.tsx │ ├── mainLeft.tsx │ ├── mainLeftContent.tsx │ ├── mainLeftFoot.tsx │ ├── mainLeftHead.tsx │ ├── mainRight.tsx │ ├── mainRightCommend.tsx │ ├── mainRightCommendItem.tsx │ ├── mainRightTag.tsx │ ├── mainRightTagItem.tsx │ ├── mainRightType.tsx │ └── mainRightTypeItem.tsx ├── Manage │ ├── index.module.scss │ ├── index.tsx │ ├── manageAddModule.tsx │ ├── manageAddTagButton.tsx │ ├── manageAddTypeButton.tsx │ ├── manageDeleteBlogItem.tsx │ ├── manageDeleteModule.tsx │ ├── manageDeleteTagItem.tsx │ ├── manageDeleteTypeItem.tsx │ ├── manageLeft.tsx │ ├── manageResult.tsx │ ├── manageResultAll.tsx │ ├── manageResultSearch.tsx │ ├── manageRight.tsx │ ├── manageSearch.tsx │ ├── manageTag.tsx │ └── manageType.tsx ├── NotLogin │ └── index.tsx ├── Publish │ ├── index.module.scss │ ├── index.tsx │ ├── publishImage.tsx │ ├── publishImageModule.tsx │ └── publishSubmit.tsx ├── ReLogin │ └── index.tsx ├── Tag │ ├── index.module.scss │ ├── index.tsx │ ├── tagContent.tsx │ ├── tagFoot.tsx │ └── tagHead.tsx └── Type │ ├── index.module.scss │ ├── index.tsx │ ├── typeContent.tsx │ ├── typeFoot.tsx │ └── typeHead.tsx ├── global.d.ts ├── hook ├── useAnimate.ts ├── useArchive.ts ├── useAuto.ts ├── useBase.ts ├── useBlog.ts ├── useData.ts ├── useHeader.ts ├── useHome.ts ├── useInView.ts ├── useIsMounted.ts ├── useLoadingBar.ts ├── useManage.ts ├── useMediaQuery.ts ├── useMessage.ts ├── useOverlay.ts ├── usePinch.ts ├── useTag.ts ├── useToast.ts ├── useType.ts └── useUser.ts ├── md ├── 1.博客需求总结.md ├── 2.数据库设计.md ├── 3.ssr的redux.md └── 4.ts书写格式.md ├── next-env.d.ts ├── next.config.js ├── nodemon.json ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── _document.tsx ├── about.tsx ├── archive.tsx ├── blog │ └── [id].tsx ├── editor │ └── [id].tsx ├── index.tsx ├── login.tsx ├── manage.tsx ├── publish.tsx ├── tag.tsx └── type.tsx ├── public ├── avatar │ ├── about.jpg │ ├── admin.png │ ├── aliPay.jpg │ ├── man.jpg │ ├── weichatPay.png │ └── woman.jpeg ├── favicon.ico ├── font │ ├── blog.ttf │ └── 测试1.ttf └── vercel.svg ├── server ├── api │ ├── 3rd │ │ ├── 3rd.ts │ │ └── index.ts │ ├── blog │ │ ├── blog.ts │ │ └── index.ts │ ├── home │ │ ├── home.ts │ │ └── index.ts │ ├── image │ │ ├── captcha.ts │ │ ├── image.ts │ │ └── index.ts │ ├── index.ts │ ├── message │ │ ├── childMessage.ts │ │ ├── index.ts │ │ └── primaryMessage.ts │ ├── tag │ │ ├── index.ts │ │ └── tag.ts │ ├── test │ │ ├── index.ts │ │ └── test.ts │ ├── type │ │ ├── index.ts │ │ └── type.ts │ └── user │ │ ├── index.ts │ │ └── user.ts ├── check │ └── index.ts ├── database │ ├── delete.ts │ ├── get.ts │ ├── insert.ts │ ├── server │ └── update.ts ├── index.ts ├── init │ ├── index.ts │ └── init.ts ├── middleware │ └── apiHandler.ts ├── setup │ └── index.ts ├── type.ts └── utils │ ├── error.ts │ └── merge.ts ├── store ├── index.ts ├── reducer │ ├── client │ │ ├── action │ │ │ ├── currentArchive.ts │ │ │ ├── currentBlogId.ts │ │ │ ├── currentHeader.ts │ │ │ ├── currentHomePage.ts │ │ │ ├── currentIp.ts │ │ │ ├── currentResult.ts │ │ │ ├── currentTag.ts │ │ │ ├── currentTagPage.ts │ │ │ ├── currentToken.ts │ │ │ ├── currentType.ts │ │ │ ├── currentTypePage.ts │ │ │ ├── currentUser.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── share │ │ │ ├── action.ts │ │ │ └── type.ts │ │ └── type.ts │ ├── index.ts │ ├── server │ │ ├── action │ │ │ ├── blog.ts │ │ │ ├── home.ts │ │ │ ├── index.ts │ │ │ ├── tag.ts │ │ │ ├── type.ts │ │ │ └── userHome.ts │ │ ├── index.ts │ │ ├── share │ │ │ ├── action.ts │ │ │ └── type.ts │ │ └── type.ts │ └── type.ts ├── saga │ ├── action │ │ ├── blog.ts │ │ ├── home.ts │ │ ├── index.ts │ │ ├── tag.ts │ │ └── type.ts │ ├── index.ts │ └── util.ts └── type.ts ├── styles ├── _basic.module.scss ├── _variable.module.scss └── globals.css ├── tsconfig.json ├── tsconfig.server.json ├── types ├── components.ts ├── config.ts ├── index.ts └── utils.ts ├── utils ├── action.ts ├── cache.ts ├── data.ts ├── delay.ts ├── dom.ts ├── env.ts ├── fetcher.ts ├── headers.ts ├── image.ts ├── log.ts ├── markdown.ts ├── node.ts ├── path.ts ├── pointer.ts ├── request.ts └── time.ts └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | PORT=10010 2 | DATABASE=./server/database/server 3 | COOKIE_PARSER=mr_wang_just_todo 4 | BING_URL=https://cn.bing.com 5 | BING_API=https://cn.bing.com/HPImageArchive.aspx?format=js&idx=--idx--&n=--n--&mkt=zh-CN 6 | NEXT_PUBLIC_STRING=RFchPT6heK4bZtI3MaJw 7 | NEXT_PUBLIC_HTTPS=false 8 | NEXT_PUBLIC_API_HOST=localhost:10010 9 | NEXT_PUBLIC_API_TOKEN=mr_wang_say 10 | NEXT_PUBLIC_ONE_SAY=https://v1.hitokoto.cn/ 11 | NEXT_PUBLIC_IP_ADDRESS=http://ip-api.com/json/ 12 | NEXT_PUBLIC_ADMIN=/avatar/admin.png 13 | NEXT_PUBLIC_MAN=/avatar/man.jpg 14 | NEXT_PUBLIC_WOMEN=/avatar/woman.jpeg 15 | NEXT_PUBLIC_ABOUT=/avatar/about.jpg -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | /dist/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | 37 | # doc 38 | /md/ 39 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "printWidth": 160 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 前后端分离的完整博客项目,基于 Create Next APP 2 | 3 | ## 特性 4 | 5 | ### 1.在 Next.js 的基础上拥有完整的后端逻辑,高度可配置性 6 | 7 | ### 2.前后端完全使用 TypeScript 实现,在官方用例的基础上进行了后端 Ts 支持的完善 8 | 9 | ### 3.使用 Hook Api 编写,完全分离视图与逻辑 10 | 11 | ### 4.基于 Redux-Saga 的状态管理 12 | 13 | ### 5.组件库实现与抽离 14 | 15 | ### 6.功能完整,markdown,回复评论,编辑修改... 持续添加维护 16 | 17 | ### 7.移动端友好,单独的modal弹窗效果 18 | 19 | ### 8.高效动画组件 20 | 21 | ## ps 登录后点击用户名可以有额外操作 admin账号: mrwang 099647 22 | 23 | ## 运行 24 | 25 | ### 安装 26 | 27 | ```shell 28 | yarn install 29 | ``` 30 | 31 | ### 开发 32 | 33 | ```shell 34 | yarn run dev 35 | ``` 36 | 37 | ### 生产 38 | 39 | ```shell 40 | yarn run build 41 | and 42 | yarn run start 43 | ``` 44 | 45 | ## 欢迎下载尝试部署以及 PR 46 | -------------------------------------------------------------------------------- /components/BlogHead/blogHeadLeft.tsx: -------------------------------------------------------------------------------- 1 | import { calendar } from "utils/time"; 2 | import { Image } from "components/Image"; 3 | import { getCurrentAvatar } from "utils/data"; 4 | import { flexBetween, flexCenter, getClass } from "utils/dom"; 5 | import { BlogHeadProps } from "."; 6 | 7 | export const BlogHeadLeft = ({ gender, avatar, username, blogModifyState, blogModifyDate, blogReadCount }: BlogHeadProps) => { 8 | return ( 9 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/BlogHead/blogHeadRight.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { SimpleElement } from "types/components"; 3 | 4 | export const BlogHeadRight: SimpleElement = () => { 5 | return ( 6 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /components/BlogHead/index.tsx: -------------------------------------------------------------------------------- 1 | import { BlogProps, UserProps } from "types"; 2 | import { flexBetween, getClass } from "utils/dom"; 3 | import { BlogHeadLeft } from "./blogHeadLeft"; 4 | import { BlogHeadRight } from "./blogHeadRight"; 5 | 6 | export type BlogHeadProps = Pick & Pick; 7 | 8 | export const BlogHead = (props: BlogHeadProps) => { 9 | return ( 10 |
11 | 12 | 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /components/BlogItem/blogItemLeft.tsx: -------------------------------------------------------------------------------- 1 | import { BlogItemLeftUl } from "./blogItemLeftUl"; 2 | import { momentTo } from "utils/time"; 3 | import { flexBetween, getClass } from "utils/dom"; 4 | import { HomeBlogProps, TypeProps, UserProps } from "types"; 5 | 6 | export const BlogItemLeft = (props: HomeBlogProps & TypeProps & UserProps) => { 7 | const { blogTitle, typeContent, blogPreview, blogCreateDate } = props; 8 | 9 | return ( 10 |
11 |
12 |
13 | {blogTitle} 14 | 15 | {momentTo(blogCreateDate)} 16 | 17 |
18 |
19 |

{typeContent}

20 |
21 |
22 |

{blogPreview}

23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /components/BlogItem/blogItemRight.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "components/Image"; 2 | import { getClass } from "utils/dom"; 3 | 4 | import style from "./index.module.scss"; 5 | 6 | export const BlogItemRight = ({ src }: { src: string }) => { 7 | return ( 8 |
9 |
10 | blog picture 11 |
12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /components/BlogItem/index.module.scss: -------------------------------------------------------------------------------- 1 | .imgHover { 2 | & img { 3 | position: relative; 4 | transition-property: transform; 5 | transition-duration: 0.3s; 6 | } 7 | &:hover { 8 | & img { 9 | transform: scale(1.1, 1.1); 10 | } 11 | } 12 | } 13 | 14 | .ulStyle { 15 | table-layout: fixed; 16 | width: 100%; 17 | } 18 | 19 | .iconTransform { 20 | display: flex; 21 | position: relative; 22 | transition-duration: 0.3s; 23 | transition-property: transform; 24 | &:hover { 25 | transform-origin: 50% 50%; 26 | transform: scale(1.5, 1.5); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /components/BlogItem/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { BlogItemLeft } from "./blogItemLeft"; 3 | import { BlogItemRight } from "./blogItemRight"; 4 | import { getClass, flexAround } from "utils/dom"; 5 | import type { HomeBlogProps, TypeProps, UserProps } from "types"; 6 | 7 | type PropsType = HomeBlogProps & UserProps & TypeProps & { className?: string; _style?: { [props: string]: string } }; 8 | 9 | const BlogItem = (props: PropsType) => { 10 | const { blogImgLink, className = "", _style = {} } = props; 11 | 12 | return ( 13 |
14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | const WithReadBlogItem = (props: PropsType) => { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | const WithWriteBlogItem = (props: PropsType) => { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export { BlogItem, WithReadBlogItem, WithWriteBlogItem }; 41 | -------------------------------------------------------------------------------- /components/BlogMessage/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .author { 4 | background-color: rgba(200, 200, 200, 0.2); 5 | display: inline-block; 6 | max-width: 100px; 7 | } 8 | 9 | .messageOverflow { 10 | position: relative; 11 | padding-right: 8px; 12 | &::after { 13 | content: "..."; 14 | position: absolute; 15 | background-color: #fff; 16 | padding: 0 5px; 17 | bottom: 2px; 18 | right: 0px; 19 | } 20 | } 21 | 22 | .btnContainer { 23 | & > button:not(:first-of-type) { 24 | @include desktop { 25 | margin-left: 16px; 26 | } 27 | @include mobile { 28 | margin-left: 8px; 29 | } 30 | } 31 | } 32 | 33 | .replay, 34 | .update, 35 | .delete { 36 | @include desktop { 37 | font-size: 14px; 38 | padding: 4px; 39 | } 40 | 41 | @include mobile { 42 | font-size: 12px; 43 | padding: 2px; 44 | } 45 | } 46 | 47 | .childMessage { 48 | @include mobile { 49 | margin-left: -20px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /components/BlogMessage/index.tsx: -------------------------------------------------------------------------------- 1 | import PrimaryMessage from "./primaryMessage"; 2 | import ChildMessage from "./childMessage"; 3 | 4 | export { PrimaryMessage, ChildMessage }; 5 | -------------------------------------------------------------------------------- /components/BlogUtils/blogCanvas.tsx: -------------------------------------------------------------------------------- 1 | import { getClass } from "utils/dom"; 2 | import { useBool } from "hook/useData"; 3 | import { useLinkToImg } from "hook/useBlog"; 4 | import { useShowAndHideAnimate } from "hook/useAnimate"; 5 | import { SimpleElement } from "types/components"; 6 | 7 | import style from "./index.module.scss"; 8 | 9 | export const BlogCanvas: SimpleElement = () => { 10 | const canvasRef = useLinkToImg(); 11 | 12 | const { bool, switchBoolDebounce } = useBool(); 13 | 14 | useShowAndHideAnimate({ 15 | state: bool, 16 | forWardRef: canvasRef, 17 | showClassName: "fadeIn", 18 | hideClassName: "fadeOut", 19 | }); 20 | 21 | return ( 22 | <> 23 | 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /components/BlogUtils/blogMenu.tsx: -------------------------------------------------------------------------------- 1 | import { getClass } from "utils/dom"; 2 | import { useBool } from "hook/useData"; 3 | import { useBlogMenu } from "hook/useBlog"; 4 | import { useShowAndHideAnimate } from "hook/useAnimate"; 5 | import { SimpleElement } from "types/components"; 6 | 7 | import style from "./index.module.scss"; 8 | 9 | export const BlogMenu: SimpleElement = () => { 10 | const isSet = useBlogMenu(".blog-content"); 11 | 12 | const { bool, switchBoolDebounce } = useBool(); 13 | 14 | const { animateRef: ref } = useShowAndHideAnimate({ 15 | state: bool && isSet, 16 | showClassName: "lightSpeedInRight", 17 | hideClassName: "lightSpeedOutRight", 18 | }); 19 | 20 | return ( 21 | <> 22 | 25 |
26 |
    27 |
    28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/BlogUtils/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .btnPosition { 4 | left: calc(100vw - 90px); 5 | bottom: 18px; 6 | z-index: 100; 7 | width: 60px; 8 | opacity: 0.8; 9 | transition-property: opacity; 10 | transition-duration: 0.3s; 11 | &:hover { 12 | opacity: 1; 13 | } 14 | @include mobile { 15 | left: auto; 16 | right: 8px; 17 | bottom: 12px; 18 | & > * { 19 | font-size: 0.85em; 20 | } 21 | } 22 | } 23 | 24 | .menuContent { 25 | font-size: 0.85em; 26 | min-width: 200%; 27 | bottom: 100%; 28 | right: 0%; 29 | background-color: #fff; 30 | } 31 | 32 | .canvasContent { 33 | width: 100px !important; 34 | height: 100px !important; 35 | right: 110%; 36 | top: 26%; 37 | } 38 | -------------------------------------------------------------------------------- /components/BlogUtils/index.tsx: -------------------------------------------------------------------------------- 1 | // 博客显示的一些工具 2 | import { getClass } from "utils/dom"; 3 | import { useAutoShowAndHide } from "hook/useAuto"; 4 | import { useAutoScrollTop, useAutoScrollBottom } from "hook/useBlog"; 5 | import { BlogMenu } from "./blogMenu"; 6 | import { BlogCanvas } from "./blogCanvas"; 7 | import { SimpleElement } from "types/components"; 8 | 9 | import style from "./index.module.scss"; 10 | 11 | export const BlogUtils: SimpleElement = () => { 12 | const divRef = useAutoShowAndHide(360); 13 | 14 | const topRef = useAutoScrollTop(); 15 | 16 | const bottomRef = useAutoScrollBottom(); 17 | 18 | return ( 19 |
    20 | 21 | 24 | 25 | 28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { Loading } from "components/Loading"; 3 | import { useBool } from "hook/useData"; 4 | import { flexCenter, getClass } from "utils/dom"; 5 | import { ButtonType } from "types/components"; 6 | 7 | export const Button: ButtonType = ({ 8 | request, 9 | type = "button", 10 | disable = false, 11 | value = "确定", 12 | initState = true, 13 | loading = false, 14 | className = "", 15 | _style, 16 | loadingColor, 17 | children, 18 | }) => { 19 | const { bool, show, hide } = useBool({ init: initState }); 20 | 21 | const requestCallback = useCallback(() => { 22 | if (request) { 23 | hide(); 24 | Promise.resolve().then(request).then(show).catch(show); 25 | } 26 | }, [hide, request, show]); 27 | 28 | return ( 29 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /components/CardHead/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { CardHeadType } from "types/components"; 3 | import { flexBetween, flexCenter, getClass } from "utils/dom"; 4 | 5 | export const CardHead: CardHeadType = ({ icon, content, hrefTo }) => { 6 | return ( 7 |
    8 |
    9 | 10 |
    {content}
    11 |
    12 | {hrefTo && ( 13 | 14 | 15 | more 16 | 17 | 18 | 19 | )} 20 |
    21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /components/CheckBox/index.module.scss: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | display: block; 3 | position: relative; 4 | width: 2em; 5 | height: 1em; 6 | border: 0.1em solid #aaa; 7 | padding: 0.1em; 8 | border-top-left-radius: 25% 50%; 9 | border-top-right-radius: 25% 50%; 10 | border-bottom-left-radius: 25% 50%; 11 | border-bottom-right-radius: 25% 50%; 12 | cursor: pointer; 13 | box-sizing: content-box; 14 | box-shadow: 0 0 0 0 #49e68c inset; 15 | 16 | transition-property: box-shadow, border; 17 | transition-duration: 0.3s; 18 | &::before { 19 | content: ""; 20 | position: absolute; 21 | width: 1em; 22 | height: 1em; 23 | border: 0.1em solid #666; 24 | top: 50%; 25 | transform: translateY(-50%); 26 | border-radius: 50%; 27 | box-sizing: border-box; 28 | transition-duration: 0.3s; 29 | transition-timing-function: cubic-bezier(0.61, -0.13, 0.34, 1.19); 30 | left: 0.1em; 31 | z-index: 1; 32 | background-color: white; 33 | box-shadow: 0 0 0.3em 0.2em rgba(0, 0, 0, 0.2); 34 | } 35 | &::after { 36 | content: ""; 37 | position: absolute; 38 | width: 1em; 39 | height: 1em; 40 | border-radius: 50%; 41 | background-color: #49e68c; 42 | box-sizing: border-box; 43 | transition-duration: 0.2s; 44 | transition-timing-function: cubic-bezier(0.61, -0.13, 0.34, 1.19); 45 | } 46 | &.checked { 47 | box-shadow: 0 0 0 1.1em #49e68c inset; 48 | border-color: #49e68c; 49 | } 50 | &.checked::before { 51 | left: 1em; 52 | box-shadow: 0 0 0.3em 0.2em rgba(100, 100, 100, 0.2); 53 | } 54 | &.checked::after { 55 | width: 2em; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /components/CheckBox/index.tsx: -------------------------------------------------------------------------------- 1 | import { useBool } from "hook/useData"; 2 | import { getClass } from "utils/dom"; 3 | import { CheckBoxType } from "types/components"; 4 | 5 | import style from "./index.module.scss"; 6 | 7 | export const CheckBox: CheckBoxType = ({ fieldName, _style, type = "radio", initState, className = "" }) => { 8 | const { bool, switchBoolDebounce } = useBool({ init: initState }); 9 | 10 | return ( 11 |
    12 |
    13 | 14 |
    15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /components/Drop/dropContainer.tsx: -------------------------------------------------------------------------------- 1 | import { getClass } from "utils/dom"; 2 | import { useAutoSetHeight } from "hook/useAuto"; 3 | import { DropContainerType } from "types/components"; 4 | 5 | import style from "./index.module.scss"; 6 | 7 | export const DropContainer: DropContainerType = ({ bool, children, length, maxHeight }) => { 8 | const [ref, height] = useAutoSetHeight({ deps: [length], maxHeight }); 9 | 10 | return ( 11 |
    16 | {children} 17 |
    18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /components/Drop/dropItem.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { getClass } from "utils/dom"; 3 | import { DropItemType } from "types/components"; 4 | 5 | import style from "./index.module.scss"; 6 | 7 | export const DropItem: DropItemType = ({ clickHandler, value, name, index, checkedIndex }) => { 8 | const clickCallback = useCallback(() => clickHandler && index !== undefined && clickHandler(index), [clickHandler, index]); 9 | 10 | return ( 11 |
    15 | {name ? name : value} 16 |
    17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /components/Drop/dropSelectItem.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent, useCallback } from "react"; 2 | import { animateFadeIn, getClass } from "utils/dom"; 3 | import { DropSelectItemType } from "types/components"; 4 | 5 | import style from "./index.module.scss"; 6 | 7 | export const DropSelectItem: DropSelectItemType = ({ idx, name, value, cancel, multiple }) => { 8 | const cancelCallback = useCallback<(e: MouseEvent) => void>( 9 | (e) => { 10 | e.stopPropagation(); 11 | cancel(idx); 12 | }, 13 | [cancel, idx] 14 | ); 15 | 16 | return ( 17 | 21 | {name ? name : value} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /components/Drop/index.module.scss: -------------------------------------------------------------------------------- 1 | .drop { 2 | cursor: pointer; 3 | z-index: 2; 4 | min-width: 120px; 5 | } 6 | 7 | .dropItem { 8 | transition-duration: 0.2s; 9 | transition-property: background; 10 | &:hover { 11 | background-color: rgba(200, 200, 200, 0.5); 12 | } 13 | &_checked { 14 | background-color: rgba(200, 200, 200, 0.5); 15 | } 16 | } 17 | 18 | .dropContainer { 19 | transition-property: height; 20 | transition-duration: 0.22s; 21 | background-color: #fff; 22 | box-shadow: 0 0 3px rgba(20, 20, 20, 0.2); 23 | z-index: 30; 24 | top: calc(100% + 1px); 25 | left: 0; 26 | } 27 | 28 | .drpSelectItem { 29 | position: relative; 30 | &_cancel:hover { 31 | text-decoration: line-through; 32 | color: #9a9a9a; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /components/Footer/footContainerCardMe.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "components/Image"; 2 | import { getClass, flexCenter } from "utils/dom"; 3 | import { SimpleElement } from "types/components"; 4 | 5 | import style from "./index.module.scss"; 6 | 7 | export const FootContainerCardMe: SimpleElement = () => { 8 | return ( 9 |
    10 |
    11 | 扫码添加好友 12 |
    13 |
    14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /components/Footer/footContainerContactMe.tsx: -------------------------------------------------------------------------------- 1 | import { getClass, flexCenter } from "utils/dom"; 2 | import { footContentLength, footContactMe } from "config/footer"; 3 | import { FootContainerContentItem } from "./footContainerContentItem"; 4 | import { FootContainerContactMeType } from "types/components"; 5 | 6 | import style from "./index.module.scss"; 7 | 8 | export const FootContainerContactMe: FootContainerContactMeType = ({ length = footContentLength }) => { 9 | return ( 10 |
    11 |
    12 | 13 |
    联系我
    14 |
    15 |
      16 | {footContactMe.slice(0, length).map(({ head, content, column }) => ( 17 | 18 | ))} 19 |
    20 |
    21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /components/Footer/footContainerContentItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { getClass, flexCenter } from "utils/dom"; 3 | import { FootContainerContentItemType } from "types/components"; 4 | 5 | import style from "./index.module.scss"; 6 | 7 | const ColumnItem: FootContainerContentItemType = ({ head, content, title }) => { 8 | return ( 9 |
    10 |
    {head}
    11 |
    {content}
    12 |
    13 | ); 14 | }; 15 | 16 | const IconItem: FootContainerContentItemType = ({ icon, content, hrefTo, title }) => { 17 | return ( 18 | 19 | 20 | 21 | {content} 22 | 23 | 24 | ); 25 | }; 26 | 27 | export const FootContainerContentItem: FootContainerContentItemType = ({ 28 | column = 1, 29 | head = "hello", 30 | content = "hello", 31 | icon = "ri-links-line", 32 | hrefTo = "/", 33 | title = content, 34 | }) => { 35 | return ( 36 |
  1. 37 | {column === 1 ? : } 38 |
  2. 39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /components/Footer/footContainerRecommend.tsx: -------------------------------------------------------------------------------- 1 | import { getClass, flexCenter } from "utils/dom"; 2 | import { footContentLength, footRecommendContent } from "config/footer"; 3 | import { FootContainerContentItem } from "./footContainerContentItem"; 4 | import { FootContainerRecommendType } from "types/components"; 5 | 6 | import style from "./index.module.scss"; 7 | 8 | export const FootContainerRecommend: FootContainerRecommendType = ({ length = footContentLength }) => { 9 | return ( 10 |
    11 |
    12 | 13 |
    推荐
    14 |
    15 |
      16 | {footRecommendContent.slice(0, length).map(({ content, hrefTo }) => ( 17 | 18 | ))} 19 |
    20 |
    21 | ); 22 | }; -------------------------------------------------------------------------------- /components/Footer/footContainerYiYan.tsx: -------------------------------------------------------------------------------- 1 | import { getClass, flexCenter } from "utils/dom"; 2 | import { LoadRender } from "components/LoadRender"; 3 | import { SimpleElement, YiYanComponent } from "types/components"; 4 | 5 | import style from "./index.module.scss"; 6 | 7 | const YiYan: YiYanComponent = ({ hitokoto, from_who, from }) => { 8 | return ( 9 |
    10 |

    {hitokoto}

    11 |
    12 | {from_who} 13 | {from} 14 |
    15 |
    16 | ); 17 | }; 18 | 19 | export const FootContainerYiYan: SimpleElement = () => { 20 | return ( 21 |
    22 |
    23 | 24 |
    一言
    25 |
    26 | 27 |
    28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /components/Footer/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .footFont { 4 | @include mobile { 5 | font-size: 0.95rem; 6 | } 7 | } 8 | 9 | .fontInherit { 10 | @include mobile { 11 | font-size: inherit; 12 | } 13 | } 14 | 15 | .hover { 16 | border-radius: 2px; 17 | transition-duration: 0.3s; 18 | transition-property: background-color, color; 19 | color: #6c757d; 20 | &:hover { 21 | color: rgba(255, 255, 255, 0.75); 22 | background-color: rgba(100, 100, 100, 0.5); 23 | } 24 | } 25 | 26 | .autoHide { 27 | @include mobile { 28 | margin-top: 0.2em; 29 | border-left: none !important; 30 | overflow: hidden; 31 | } 32 | } 33 | 34 | .img { 35 | @include mobile { 36 | height: 75px !important; 37 | width: 75px !important; 38 | } 39 | } 40 | 41 | .yiYanFont { 42 | font-size: 0.95rem; 43 | color: rgba(255, 255, 255, 0.75); 44 | @include mobile { 45 | font-size: inherit; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /components/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { getClass, flexCenter } from "utils/dom"; 2 | import { FootContainerYiYan } from "./footContainerYiYan"; 3 | import { FootContainerCardMe } from "./footContainerCardMe"; 4 | import { FootContainerRecommend } from "./footContainerRecommend"; 5 | import { FootContainerContactMe } from "./footContainerContactMe"; 6 | import { SimpleElement } from "types/components"; 7 | 8 | import style from "./index.module.scss"; 9 | 10 | export const Footer: SimpleElement = () => { 11 | return ( 12 |
    13 |
    14 |
    15 | 16 | 17 | 18 | 19 |
    20 |
    21 |
    22 |
    23 |
    Copyright
    24 | 25 |
    2020-2021 MrWang
    26 |
    27 |
    28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/Head/index.tsx: -------------------------------------------------------------------------------- 1 | // 全局的head组件 2 | import OriginalHead from "next/head"; 3 | 4 | // Head 组件 5 | export const Head = ({ title = "Blog" }: { title?: string }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | {title} | Blog 13 | 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /components/Header/headContainerButton.tsx: -------------------------------------------------------------------------------- 1 | import { getClass } from "utils/dom"; 2 | import { HeadContainerTagNavBtnType } from "types/components"; 3 | 4 | import style from "./index.module.scss"; 5 | 6 | export const HeadContainerTagNavBtn: HeadContainerTagNavBtnType = ({ handler }) => { 7 | return ( 8 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /components/Header/headContainerItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useCallback } from "react"; 3 | import { useHeaderItem } from "hook/useHeader"; 4 | import { getClass } from "utils/dom"; 5 | import { HeadContainerItemType } from "types/components"; 6 | 7 | import style from "./index.module.scss"; 8 | 9 | export const HeadContainerItem: HeadContainerItemType = ({ value = "head", hrefTo = "/", icon = "ri-home-heart-fill" }) => { 10 | const { currentHeader, changeCurrentHeader } = useHeaderItem(); 11 | 12 | const clickHandler = useCallback(() => { 13 | changeCurrentHeader(hrefTo); 14 | }, [hrefTo]); 15 | 16 | return ( 17 |
  3. 18 | 19 | 20 | 21 | {value} 22 | 23 | 24 |
  4. 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /components/Header/headContainerList.tsx: -------------------------------------------------------------------------------- 1 | import { HeaderContent } from "config/header"; 2 | import { getClass } from "utils/dom"; 3 | import { useHeaderItem } from "hook/useHeader"; 4 | import { useAutoSetHeaderHeight } from "hook/useAuto"; 5 | import { HeadContainerItem } from "./headContainerItem"; 6 | import { HeadContainerListType } from "types/components"; 7 | 8 | import style from "./index.module.scss"; 9 | 10 | export const HeadContainerList: HeadContainerListType = ({ show }) => { 11 | const { ref, height } = useAutoSetHeaderHeight(992); 12 | 13 | useHeaderItem({ needInitHead: true }); 14 | 15 | return ( 16 |
    17 |
      18 | {HeaderContent.map(({ value, icon, hrefTo }) => ( 19 | 20 | ))} 21 |
    22 |
    23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /components/Header/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useBool } from "hook/useData"; 3 | import { getClass } from "utils/dom"; 4 | import { HeadContainerList } from "./headContainerList"; 5 | import { HeadContainerUser } from "./headContainerUser"; 6 | import { HeadContainerTagNavBtn } from "./headContainerButton"; 7 | import { SimpleElement } from "types/components"; 8 | 9 | import style from "./index.module.scss"; 10 | 11 | export const Header: SimpleElement = () => { 12 | const { bool, switchBoolDebounce } = useBool(); 13 | 14 | return ( 15 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /components/Hover/animate.tsx: -------------------------------------------------------------------------------- 1 | import { AnimateType } from "types/components"; 2 | import { getClass } from "utils/dom"; 3 | 4 | import style from "./index.module.scss"; 5 | 6 | export const Animate: AnimateType = ({ children, forwardRef }) => { 7 | return ( 8 |
    9 | {children} 10 |
    11 |
    12 |
    13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /components/Hover/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .hoverTriangle { 4 | width: 20px; 5 | height: 15px; 6 | clip-path: polygon(0 0, 100% 0, 50% 100%); 7 | left: 14px; 8 | bottom: 2px; 9 | } 10 | 11 | .hoverHolder { 12 | height: 15px; 13 | } 14 | 15 | .animatePanel { 16 | display: block; 17 | z-index: 100; 18 | position: absolute; 19 | left: -10px; 20 | bottom: 100%; 21 | filter: drop-shadow(0px 0px 8px rgba(180, 180, 180, 0.5)); 22 | } 23 | -------------------------------------------------------------------------------- /components/Hover/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { getClass } from "utils/dom"; 3 | import { useBool } from "hook/useData"; 4 | import { useShowAndHideAnimate } from "hook/useAnimate"; 5 | import { Animate } from "./animate"; 6 | import { HoverType } from "types/components"; 7 | 8 | export const Hover: HoverType = ({ className = "", children, hoverItem }) => { 9 | const { bool, boolState, showThrottleState, hideDebounce } = useBool(); 10 | 11 | const { animateRef } = useShowAndHideAnimate({ 12 | state: bool, 13 | showDone: () => { 14 | boolState.current = true; 15 | }, 16 | hideDone: () => { 17 | boolState.current = true; 18 | }, 19 | }); 20 | 21 | return ( 22 |
    23 | {children} 24 | {hoverItem} 25 |
    26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /components/Image/index.tsx: -------------------------------------------------------------------------------- 1 | import OriginalImage, { ImageProps } from "next/image"; 2 | 3 | const Image = (props: ImageProps) => { 4 | return ; 5 | }; 6 | 7 | export { Image }; 8 | -------------------------------------------------------------------------------- /components/Input/index.module.scss: -------------------------------------------------------------------------------- 1 | .success { 2 | position: absolute; 3 | font-size: 12px; 4 | color: green; 5 | right: 2px; 6 | top: 50%; 7 | transform: translateY(-50%); 8 | } 9 | 10 | .fail { 11 | position: absolute; 12 | font-size: 12px; 13 | color: red; 14 | right: 2px; 15 | top: 50%; 16 | transform: translateY(-50%); 17 | } 18 | 19 | .loading { 20 | position: absolute; 21 | font-size: 12px; 22 | right: 2px; 23 | top: 0; 24 | bottom: 0; 25 | margin-top: auto; 26 | margin-bottom: auto; 27 | } 28 | -------------------------------------------------------------------------------- /components/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { Head } from "components/Head"; 3 | import { Header } from "components/Header"; 4 | import { LoadingBar } from "components/LoadingBar"; 5 | import { ModuleManager } from "components/ModuleManager"; 6 | import { useAutoLogin, useAutoGetIp } from "hook/useUser"; 7 | import { getClass } from "utils/dom"; 8 | import { ReactNode } from "react"; 9 | 10 | const Footer = dynamic(() => import("../Footer").then((r) => r.Footer)); 11 | 12 | export const Layout = ({ title, container = true, children }: { title?: string; container?: boolean; children?: ReactNode }): JSX.Element => { 13 | useAutoLogin(); 14 | useAutoGetIp(); 15 | 16 | return ( 17 | <> 18 | 19 | 20 | 21 |
    22 | {container &&
    } 23 |
    24 | {children} 25 |
    26 | {container &&
    } 27 |
    28 |
    29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /components/LoadRender/loadingError.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingErrorType } from "types/components"; 2 | 3 | export const LoadingError: LoadingErrorType = (error) => ( 4 |
    5 | loading failed: {error} 6 |
    7 | ); 8 | -------------------------------------------------------------------------------- /components/Loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { flexCenter, getClass } from "utils/dom"; 2 | import { LoadingType } from "types/components"; 3 | 4 | import style from "./index.module.scss"; 5 | 6 | export const Loading: LoadingType = ({ _style = {}, className = "", color = "info" }) => { 7 | return ( 8 |
    9 |
    10 |
    11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /components/LoadingBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useRouter } from "next/dist/client/router"; 3 | import { useLoadingBar } from "hook/useLoadingBar"; 4 | import { Bar } from "./loadingBar"; 5 | import { LoadingBarType } from "types/components"; 6 | 7 | export const LoadingBar: LoadingBarType = React.memo(function LoadingBar({ height = 1.5, present = 0 }) { 8 | const router = useRouter(); 9 | 10 | const { start, end, ref } = useLoadingBar({ height, present }); 11 | 12 | useEffect(() => { 13 | let id: NodeJS.Timeout | null = null; 14 | const delayStart = () => { 15 | id = setTimeout(start, 200); 16 | }; 17 | const endNow = () => { 18 | id && clearTimeout(id) && (id = null); 19 | end(); 20 | }; 21 | router.events.on("routeChangeStart", delayStart); 22 | router.events.on("routeChangeComplete", endNow); 23 | return () => { 24 | router.events.off("routeChangeStart", delayStart); 25 | router.events.off("routeChangeComplete", endNow); 26 | }; 27 | }, [end, start]); 28 | 29 | return ; 30 | }); 31 | -------------------------------------------------------------------------------- /components/LoadingBar/loadingBar.tsx: -------------------------------------------------------------------------------- 1 | import { BarType } from "types/components"; 2 | 3 | export const Bar: BarType = ({ forwardRef }) => ( 4 |
    5 |
    6 |
    7 | ); 8 | -------------------------------------------------------------------------------- /components/ModuleManager/index.module.scss: -------------------------------------------------------------------------------- 1 | .cover { 2 | left: 0; 3 | top: 0; 4 | z-index: 2; 5 | transition-property: background; 6 | transition-duration: 0.2s; 7 | &_active { 8 | background-color: rgba(40, 40, 40, 0.6); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /components/Overlay/desktop.tsx: -------------------------------------------------------------------------------- 1 | import { useBodyLock, useOverlayBody } from "hook/useOverlay"; 2 | import { useShowAndHideAnimate } from "hook/useAnimate"; 3 | import { flexBetween, getClass } from "utils/dom"; 4 | import { OverlayType } from "types/components"; 5 | import { getScrollBarSize } from "utils/action"; 6 | 7 | const Overlay: OverlayType = ({ head, body, foot, closeHandler, showState, className = "", clear }) => { 8 | 9 | const { animateRef: ref } = useShowAndHideAnimate({ 10 | state: showState || false, 11 | showClassName: "fadeInDown", 12 | hideClassName: "fadeOutDown", 13 | startShow: () => { 14 | document.body.style.paddingRight = `${getScrollBarSize()}px`; 15 | }, 16 | hideDone: () => { 17 | document.body.style.paddingRight = "0px"; 18 | clear && clear(); 19 | }, 20 | }); 21 | 22 | useBodyLock({ ref }); 23 | 24 | const bodyContent = useOverlayBody({ body, closeHandler }); 25 | 26 | return ( 27 |
    28 |
    29 | {head} 30 | 33 |
    34 |
    {bodyContent}
    35 | {foot &&
    {foot}
    } 36 |
    37 | ); 38 | }; 39 | 40 | export default Overlay; 41 | -------------------------------------------------------------------------------- /components/Overlay/index.module.scss: -------------------------------------------------------------------------------- 1 | .modalContainer { 2 | position: absolute; 3 | width: 100%; 4 | height: 100%; 5 | left: 0; 6 | bottom: 0; 7 | overflow: hidden; 8 | } 9 | 10 | .mobileModal { 11 | width: 100%; 12 | background-color: #fff; 13 | border-radius: 8px 8px 0 0; 14 | overflow: hidden; 15 | filter: drop-shadow(0 0 0.75rem rgba(100, 100, 100, 0.6)); 16 | } 17 | 18 | .mobileHead { 19 | height: 18px; 20 | display: flex; 21 | justify-content: center; 22 | align-items: center; 23 | background-color: rgba(220, 220, 220, 0.6); 24 | } 25 | 26 | .indicator { 27 | width: 18px; 28 | height: 4px; 29 | border-radius: 99px; 30 | background-color: rgba(100, 100, 100, 0.6); 31 | &:not(:first-of-type) { 32 | margin-left: 2px; 33 | } 34 | } 35 | 36 | .mobileContent { 37 | // margin-top: 10px; 38 | overflow: auto; 39 | height: calc(100% - 40px); 40 | } 41 | -------------------------------------------------------------------------------- /components/Overlay/index.tsx: -------------------------------------------------------------------------------- 1 | import DesktopOverlay from "./desktop"; 2 | import MobileOverlay from "./mobile"; 3 | 4 | export { DesktopOverlay, MobileOverlay }; 5 | -------------------------------------------------------------------------------- /components/PageFoot/index.tsx: -------------------------------------------------------------------------------- 1 | import { flexBetween, getClass } from "utils/dom"; 2 | import { FootPageType } from "types/components"; 3 | 4 | export const FootPage: FootPageType = ({ page, increaseAble, decreaseAble, increasePage, decreasePage, className = "" }) => { 5 | return ( 6 |
    7 | 10 | {page} 11 | 14 |
    15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /components/Publish/index.module.scss: -------------------------------------------------------------------------------- 1 | .editor { 2 | overflow: hidden; 3 | font-family: consolas; 4 | position: relative; 5 | } 6 | -------------------------------------------------------------------------------- /components/Publish/publishEditor.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { useCallback } from "react"; 3 | import { useEditor } from "hook/useBlog"; 4 | import { getClass } from "utils/dom"; 5 | import { markNOLineNumber } from "utils/markdown"; 6 | 7 | import style from "./index.module.scss"; 8 | import "react-markdown-editor-lite/lib/index.css"; 9 | 10 | const MdEditor = dynamic(() => import("react-markdown-editor-lite"), { 11 | ssr: false, 12 | }); 13 | 14 | export const PublishEditor = ({ blogId, blogContent = "" }: { blogId: string; blogContent?: string }) => { 15 | useEditor(blogId); 16 | 17 | const mdRender = useCallback<(t: string) => string>((text) => markNOLineNumber.render(text), []); 18 | 19 | return ( 20 |
    21 | 28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/Publish/publishHead.tsx: -------------------------------------------------------------------------------- 1 | import { Drop } from "components/Drop"; 2 | import { blogOrigin } from "config/publish"; 3 | import { BlogProps } from "types"; 4 | 5 | export const PublishHead = ({ blogOriginState, blogTitle }: Partial>) => { 6 | return ( 7 |
    8 | 9 | fieldName="blogOriginState" 10 | className="col-1 border-info rounded-left" 11 | data={blogOrigin} 12 | initData={blogOriginState !== undefined ? [blogOriginState] : []} 13 | /> 14 | 15 |
    16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /components/Publish/publishState.tsx: -------------------------------------------------------------------------------- 1 | import { Drop } from "components/Drop"; 2 | import { CheckBox } from "components/CheckBox"; 3 | import { blogState } from "config/publish"; 4 | import { BlogProps } from "types"; 5 | 6 | export const PublishState = (props: Partial>) => { 7 | return ( 8 | <> 9 | {blogState.map(({ fieldName, name, value }, idx) => { 10 | if (Array.isArray(value)) { 11 | const init = props[fieldName] !== undefined ? [props[fieldName]].map((it) => value.findIndex(({ value: subValue }) => subValue === String(it))) : []; 12 | return ( 13 |
    14 | {name} 15 | fieldName={fieldName} data={value} className="rounded" _style={{ minWidth: "120px", zIndex: "1" }} initData={init} /> 16 |
    17 | ); 18 | } else { 19 | const init = Boolean(props[fieldName]); 20 | return ( 21 |
    22 | {name ? name : value} 23 | 24 |
    25 | ); 26 | } 27 | })} 28 | 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/Publish/publishTag.tsx: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { Drop } from "components/Drop"; 3 | import { LoadRender } from "components/LoadRender"; 4 | import { ClientTagProps, ServerTagProps } from "types"; 5 | 6 | export const PublishTag = ({ tagId }: Partial>) => { 7 | return ( 8 |
    9 |
    10 | 14 | 标签 15 | 16 |
    17 | 18 | needInitialData 19 | apiPath={apiName.tag} 20 | loaded={(res) => { 21 | const data: { name?: string; value: string }[] = res.map(({ tagContent, tagId }) => { 22 | return { name: tagContent, value: tagId! }; 23 | }); 24 | const init = tagId !== undefined && data.length ? tagId.map((it) => data.findIndex(({ value }) => value === it)) : []; 25 | return fieldName="tagId" className="form-control" placeHolder="添加标签" data={data} multiple initData={init} />; 26 | }} 27 | /> 28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/Publish/publishType.tsx: -------------------------------------------------------------------------------- 1 | import { Drop } from "components/Drop"; 2 | import { LoadRender } from "components/LoadRender"; 3 | import { apiName } from "config/api"; 4 | import { TypeProps } from "types"; 5 | 6 | export const PublishType = ({ typeId }: Partial>) => { 7 | return ( 8 |
    9 |
    10 | 14 | 分类 15 | 16 |
    17 | 18 | needInitialData 19 | apiPath={apiName.type} 20 | loaded={(res) => { 21 | const data: { name?: string; value: string }[] = res.map(({ typeContent, typeId }) => { 22 | return { name: typeContent, value: typeId! }; 23 | }); 24 | const init = typeId !== undefined && data.length ? [typeId].map((it) => data.findIndex(({ value }) => value === it)) : []; 25 | return maxHeight={200} fieldName="typeId" className="form-control" placeHolder="添加分类" data={data} initData={init} />; 26 | }} 27 | /> 28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /components/Publish/publishTypeAndTag.tsx: -------------------------------------------------------------------------------- 1 | import { PublishType } from "./publishType"; 2 | import { PublishTag } from "./publishTag"; 3 | import { ClientTagProps, TypeProps } from "types"; 4 | 5 | export const PublishTypeAndTag = ({ typeId, tagId }: Partial & Pick>) => { 6 | return ( 7 |
    8 | 9 | 10 |
    11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /components/Tag/index.module.scss: -------------------------------------------------------------------------------- 1 | .tagItem { 2 | background-color: #eee; 3 | overflow: hidden; 4 | transition-property: background; 5 | transition-duration: 0.2s; 6 | width: 130px; 7 | &__left { 8 | width: 70%; 9 | position: relative; 10 | color: white; 11 | flex-shrink: 0; 12 | 13 | &__icon { 14 | width: 40px; 15 | flex-shrink: 0; 16 | text-align: center; 17 | } 18 | 19 | &__text { 20 | width: calc(100% - 40px); 21 | overflow: hidden; 22 | white-space: nowrap; 23 | text-overflow: ellipsis; 24 | flex-shrink: 0; 25 | } 26 | } 27 | 28 | &__left::before { 29 | content: ""; 30 | position: absolute; 31 | width: 60%; 32 | height: 99%; 33 | left: 41%; 34 | background: inherit; 35 | z-index: -1; 36 | } 37 | 38 | &__left::after { 39 | content: ""; 40 | vertical-align: middle; 41 | position: absolute; 42 | clip-path: polygon(0 0, 100% 50%, 0 100%); 43 | top: 0; 44 | width: 10px; 45 | height: 100%; 46 | left: calc(100% - 0.22px); 47 | background: inherit; 48 | } 49 | 50 | &__right { 51 | width: 30%; 52 | padding-left: 14px; 53 | } 54 | } 55 | 56 | .hoverItem { 57 | cursor: pointer; 58 | &:hover { 59 | background-color: #ccc; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /components/Tag/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useTag } from "hook/useTag"; 3 | import { flexCenter, getClass } from "utils/dom"; 4 | import { TagType } from "types/components"; 5 | 6 | import style from "./index.module.scss"; 7 | 8 | const Tag: TagType = ({ tagContent, tagCount, className = "", hoverAble = true }) => { 9 | return ( 10 |
    14 |
    15 | 16 | {tagContent} 17 |
    18 | {tagCount} 19 |
    20 | ); 21 | }; 22 | 23 | const WithChangeTag: TagType = ({ tagContent, tagCount, className }) => { 24 | const { changeCurrentTag } = useTag(); 25 | 26 | const changeTag = useCallback(() => changeCurrentTag(tagContent), [tagContent]); 27 | 28 | return ( 29 |
    30 | 31 |
    32 | ); 33 | }; 34 | 35 | export { Tag, WithChangeTag }; 36 | -------------------------------------------------------------------------------- /components/Toast/index.module.scss: -------------------------------------------------------------------------------- 1 | .toast { 2 | opacity: 1; 3 | min-width: 180px; 4 | } 5 | 6 | .close { 7 | outline: none !important; 8 | transition-property: opacity; 9 | transition-duration: 0.2s; 10 | } 11 | -------------------------------------------------------------------------------- /components/Type/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useType } from "hook/useType"; 3 | import { getClass } from "utils/dom"; 4 | import { TypeType } from "types/components"; 5 | 6 | const Type: TypeType = ({ typeCount, typeContent, className = "", hoverAble = true }) => { 7 | return ( 8 |
    9 | {typeContent} 10 | {typeCount} 11 |
    12 | ); 13 | }; 14 | 15 | const WithChangeType: TypeType = ({ typeCount, typeContent, className }) => { 16 | const { changeCurrentType } = useType(); 17 | 18 | const changeType = useCallback(() => changeCurrentType(typeContent), []); 19 | 20 | return ( 21 |
    22 | 23 |
    24 | ); 25 | }; 26 | 27 | export { Type, WithChangeType }; 28 | -------------------------------------------------------------------------------- /components/UserHover/index.module.scss: -------------------------------------------------------------------------------- 1 | .imgHover { 2 | width: 40px; 3 | & img { 4 | // position: relative; 5 | transition-property: transform; 6 | transition-duration: 0.3s; 7 | } 8 | &:hover { 9 | & img { 10 | transform: scale(1.1, 1.1); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /components/UserHover/index.tsx: -------------------------------------------------------------------------------- 1 | import { Hover } from "components/Hover"; 2 | import { UserHoverItem } from "./hoverItem"; 3 | import { UserHoverType } from "types/components"; 4 | 5 | export const UserHover: UserHoverType = (props) => { 6 | const { userId, children = null } = props; 7 | if (userId && children) { 8 | return ( 9 | 12 | 13 |
    14 | } 15 | > 16 | {children} 17 | 18 | ); 19 | } else { 20 | return children; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /config/action.ts: -------------------------------------------------------------------------------- 1 | // 获取action操作名称 2 | enum actionName { 3 | currentType = "currentType", 4 | currentTag = "currentTag", 5 | currentToken = "currentToken", 6 | currentHeader = "currentHeader", 7 | currentHomePage = "currentHomePage", 8 | currentTypePage = "currentTypePage", 9 | currentTagPage = "currentTagPage", 10 | currentBlogId = "currentBlogId", 11 | currentArchive = "currentArchive", 12 | currentUser = "currentUser", 13 | currentIp = "currentIp", 14 | currentResult = "currentResult", 15 | } 16 | 17 | export { actionName }; 18 | -------------------------------------------------------------------------------- /config/archive.ts: -------------------------------------------------------------------------------- 1 | export const archiveLength = 12; -------------------------------------------------------------------------------- /config/blogItem.ts: -------------------------------------------------------------------------------- 1 | import { BlogContentItem } from "types/config"; 2 | 3 | const blogContentArray: Array = [ 4 | { icon: "ri-thumb-up-line", content: "点赞" }, 5 | { icon: "ri-star-line", content: "收藏" }, 6 | { icon: "ri-eye-line", content: "观看" }, 7 | ]; 8 | 9 | export { blogContentArray }; 10 | -------------------------------------------------------------------------------- /config/footer.ts: -------------------------------------------------------------------------------- 1 | import { FootContentType } from "types/config"; 2 | 3 | const footContentLength = 2; 4 | 5 | const footRecommendContent: FootContentType = [ 6 | { content: "MDN", hrefTo: "https://dev.mozilla.org/", column: 1 }, 7 | { content: "Next.js", hrefTo: "https://nextjs.org/", column: 1 }, 8 | ]; 9 | 10 | const footContactMe: FootContentType = [ 11 | { head: "qq", content: "2711470541", column: 2 }, 12 | { head: "email", content: "2711470541@qq.com", column: 2 }, 13 | ]; 14 | 15 | export { footContentLength, footRecommendContent, footContactMe }; 16 | -------------------------------------------------------------------------------- /config/header.ts: -------------------------------------------------------------------------------- 1 | import { HeaderContentType } from "types/config"; 2 | 3 | export const HeaderContent: HeaderContentType = [ 4 | { value: "首页", hrefTo: "/", icon: "ri-home-heart-fill" }, 5 | { value: "分类", hrefTo: "/type", icon: "ri-lightbulb-flash-fill" }, 6 | { value: "标签", hrefTo: "/tag", icon: "ri-price-tag-fill" }, 7 | { value: "归档", hrefTo: "/archive", icon: "ri-archive-drawer-fill" }, 8 | { value: "关于我", hrefTo: "/about", icon: "ri-information-fill" }, 9 | ]; 10 | -------------------------------------------------------------------------------- /config/highLight.ts: -------------------------------------------------------------------------------- 1 | // 语法高亮 2 | import Hljs from "highlight.js/lib/core"; 3 | import css from "highlight.js/lib/languages/css"; 4 | import json from "highlight.js/lib/languages/json"; 5 | import java from "highlight.js/lib/languages/java"; 6 | import javascript from "highlight.js/lib/languages/javascript"; 7 | import typescript from "highlight.js/lib/languages/typescript"; 8 | import less from "highlight.js/lib/languages/less"; 9 | import scss from "highlight.js/lib/languages/scss"; 10 | import shell from "highlight.js/lib/languages/shell"; 11 | import xml from "highlight.js/lib/languages/xml"; 12 | import sql from 'highlight.js/lib/languages/sql'; 13 | // import "highlight.js/styles/monokai-sublime.css"; 14 | import "highlight.js/styles/github-dark.css"; 15 | 16 | Hljs.registerLanguage("css", css); 17 | Hljs.registerLanguage("json", json); 18 | Hljs.registerLanguage("java", java); 19 | Hljs.registerLanguage("javascript", javascript); 20 | Hljs.registerLanguage("typescript", typescript); 21 | Hljs.registerLanguage("less", less); 22 | Hljs.registerLanguage("scss", scss); 23 | Hljs.registerLanguage("shell", shell); 24 | Hljs.registerLanguage("xml", xml); 25 | Hljs.registerLanguage("sql", sql); 26 | 27 | export default Hljs; 28 | -------------------------------------------------------------------------------- /config/home.ts: -------------------------------------------------------------------------------- 1 | import { MainRightHeader } from "types/config"; 2 | 3 | // 每一页显示的内容数量 4 | export const pageContentLength = 4; 5 | 6 | // home 每一项下方的显示配置,依次为:作者头像,赞,收藏,观看 7 | enum mainRight { 8 | type, 9 | tag, 10 | recommend, 11 | } 12 | 13 | const mainRightHeader: MainRightHeader = { 14 | [mainRight.type]: { icon: "ri-lightbulb-flash-fill", content: "分类", hrefTo: "/type" }, 15 | [mainRight.tag]: { icon: "ri-price-tag-3-fill", content: "标签", hrefTo: "/tag" }, 16 | [mainRight.recommend]: { icon: "ri-bookmark-fill", content: "推荐" }, 17 | }; 18 | 19 | export { mainRightHeader, mainRight }; 20 | -------------------------------------------------------------------------------- /config/manage.ts: -------------------------------------------------------------------------------- 1 | import { AddModuleType } from "types/config"; 2 | 3 | const addModule: AddModuleType = { 4 | input: { 5 | regexp: /^[^\s]{2,7}$/, 6 | success: "格式正确", 7 | fail: "输入应为2-7个字符", 8 | }, 9 | }; 10 | 11 | const manageLength = 4; 12 | 13 | export { addModule, manageLength }; 14 | -------------------------------------------------------------------------------- /config/message.ts: -------------------------------------------------------------------------------- 1 | // 每一页主评论显示的最大长度 2 | export const primaryMessageLength = 4; 3 | 4 | // 每一个主评论显示的子评论初始长度,超出后显示更多按钮 5 | export const childMessageLength = 3; 6 | -------------------------------------------------------------------------------- /config/publish.ts: -------------------------------------------------------------------------------- 1 | import { BlogStateType } from "types/config"; 2 | 3 | const blogOrigin = [ 4 | { 5 | name: "原创", 6 | value: "0", 7 | }, 8 | { 9 | name: "翻译", 10 | value: "1", 11 | }, 12 | { 13 | name: "转载", 14 | value: "2", 15 | }, 16 | ]; 17 | 18 | const blogState: BlogStateType = [ 19 | { 20 | fieldName: "blogState", 21 | name: "状态", 22 | value: [ 23 | { 24 | name: "暂存", 25 | value: "0", 26 | }, 27 | { 28 | name: "隐藏", 29 | value: "1", 30 | }, 31 | { 32 | name: "发布", 33 | value: "2", 34 | }, 35 | { 36 | name: "推荐", 37 | value: "3", 38 | }, 39 | ], 40 | }, 41 | { 42 | fieldName: "blogPriseState", 43 | name: "打赏", 44 | value: "1", 45 | }, 46 | { 47 | fieldName: "blogCommentState", 48 | name: "评论", 49 | value: "1", 50 | }, 51 | ]; 52 | 53 | const editorId = "blogEditor"; 54 | 55 | export { blogOrigin, blogState, editorId }; 56 | -------------------------------------------------------------------------------- /config/ssr.ts: -------------------------------------------------------------------------------- 1 | import { actionName } from "config/action"; 2 | import { setDataSuccess_client } from "store/reducer/client/share/action"; 3 | import { AutoDispatchTokenHandler, AutoDispatchTokenHandlerProps } from "types/config"; 4 | 5 | const autoDispatchTokenHandler: AutoDispatchTokenHandler = (action) => { 6 | return (store) => 7 | async ({ req, res, ...etc }: AutoDispatchTokenHandlerProps) => { 8 | if (store.getState().client[actionName.currentToken].data !== req.session["apiToken"]) { 9 | store.dispatch(setDataSuccess_client({ name: actionName.currentToken, data: req.session["apiToken"] })); 10 | } 11 | return await action({ store, req, res, ...etc }); 12 | }; 13 | }; 14 | 15 | export { autoDispatchTokenHandler }; 16 | -------------------------------------------------------------------------------- /config/toast.ts: -------------------------------------------------------------------------------- 1 | // 显示提示框时的配置 2 | enum toastState { 3 | success = "text-info", 4 | fail = "text-danger", 5 | } 6 | 7 | export { toastState }; 8 | -------------------------------------------------------------------------------- /config/type&tag.ts: -------------------------------------------------------------------------------- 1 | // 每一页显示的数量 2 | export const pageContentLength = 4; 3 | -------------------------------------------------------------------------------- /config/user.ts: -------------------------------------------------------------------------------- 1 | import { LoginType } from "types/config"; 2 | 3 | const login: LoginType = { 4 | username: { 5 | regexp: /^\w{2,7}$/, 6 | success: "格式正确", 7 | fail: "用户名为2-7个字符", 8 | }, 9 | password: { 10 | regexp: /^\d{5,10}$/, 11 | success: "格式正确", 12 | fail: "密码为5-10个数字", 13 | }, 14 | }; 15 | 16 | export { login }; 17 | -------------------------------------------------------------------------------- /containers/About/aboutLeft.tsx: -------------------------------------------------------------------------------- 1 | import { Splide, SplideSlide } from "@splidejs/react-splide"; 2 | import { apiName } from "config/api"; 3 | import { usePinch } from "hook/usePinch"; 4 | import { Image } from "components/Image"; 5 | import { LoadRender } from "components/LoadRender"; 6 | import { SimpleElement } from "types/components"; 7 | 8 | export const AboutLeft: SimpleElement = () => { 9 | return ( 10 |
    11 |
    12 | 13 | token 14 | apiPath={apiName.allImage} 15 | loaded={(data) => ( 16 | 17 | {[process.env.NEXT_PUBLIC_ABOUT].concat(data.map((it) => it.relativeUrl)).map((relativeUrl) => ( 18 | 19 | 20 | 21 | ))} 22 | 23 | )} 24 | loading={() => me} 25 | /> 26 |
    27 |
    28 | ); 29 | }; 30 | 31 | const ImgItem = ({ relativeUrl }: { relativeUrl: string }) => { 32 | const [pinchRef, coverRef] = usePinch(); 33 | 34 | return ( 35 |
    36 |
    37 | {relativeUrl} 38 |
    39 |
    40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /containers/About/aboutRight.tsx: -------------------------------------------------------------------------------- 1 | import { AboutRightLink } from "./aboutRightLink"; 2 | import { AboutRightAbout } from "./aboutRightAbout"; 3 | import { SimpleElement } from "types/components"; 4 | 5 | export const AboutRight: SimpleElement = () => { 6 | return ( 7 |
    8 |
    9 |
    10 | 关于我 11 |
    12 |
      13 | 14 | 15 |
    16 |
    17 |
    18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /containers/About/aboutRightAbout.tsx: -------------------------------------------------------------------------------- 1 | import { SimpleElement } from "types/components"; 2 | 3 | export const AboutRightAbout: SimpleElement = () => { 4 | return ( 5 |
  5. 6 |
    7 |

    一个小菜鸟, 目标是成为一个全栈大佬.

    8 |

    入门学习:

    9 |

    计算机专业知识 + C Language

    10 |

    后端技术学习:

    11 |

    Java、Sql、Mybatis、SpringMVC、SSM、Node.js...

    12 |

    前端技术学习:

    13 |

    HTML、CSS、JavaScript、TypeScript、React、Vue...

    14 |

    关于这个博客

    15 |

    Express + Next.js博客应用

    16 |
    17 |
  6. 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /containers/About/aboutRightLink.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "components/Image"; 2 | import { SimpleElement } from "types/components"; 3 | 4 | export const AboutRightLink: SimpleElement = () => { 5 | return ( 6 |
  7. 7 | 8 | github 9 | 10 | 11 | leetCode 12 | 13 |
  8. 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /containers/About/index.tsx: -------------------------------------------------------------------------------- 1 | import { AboutLeft } from "./aboutLeft"; 2 | import { AboutRight } from "./aboutRight"; 3 | import { SimpleElement } from "types/components"; 4 | 5 | export const About: SimpleElement = () => { 6 | return ( 7 |
    8 | 9 | 10 |
    11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /containers/Archive/archiveContent.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { momentTo } from "utils/time"; 3 | import { flexBetween, flexCenter, getClass } from "utils/dom"; 4 | import { AnimationList } from "components/AnimationList"; 5 | import { HomeProps } from "store/reducer/server/action/home"; 6 | 7 | import style from "./index.module.scss"; 8 | 9 | export const ArchiveContent = ({ year, blogProps }: { year: string; blogProps: HomeProps }) => { 10 | return ( 11 | <> 12 |

    {year}

    13 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /containers/Archive/archiveHead.tsx: -------------------------------------------------------------------------------- 1 | import { useArchive } from "hook/useArchive"; 2 | import { flexBetween, getClass } from "utils/dom"; 3 | import { SimpleElement } from "types/components"; 4 | 5 | export const ArchiveHead: SimpleElement = () => { 6 | const { allCount } = useArchive(); 7 | return ( 8 |
    9 |
    10 | 归档 11 |
    12 | 13 | {allCount} 14 | 篇博客 15 |
    16 |
    17 |
    18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /containers/Archive/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .archivePoint { 4 | font-size: 0.4em; 5 | } 6 | 7 | .archiveTitle { 8 | @include desktop { 9 | font-size: 18px; 10 | max-width: 180px; 11 | } 12 | @include mobile { 13 | font-size: 16px; 14 | max-width: 120px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /containers/Archive/index.tsx: -------------------------------------------------------------------------------- 1 | import { ArchiveHead } from "./archiveHead"; 2 | import { ArchiveContent } from "./archiveContent"; 3 | import { Loading } from "components/Loading"; 4 | import { useArchive, useAutoLoadArchive } from "hook/useArchive"; 5 | import { SimpleElement } from "types/components"; 6 | 7 | export const Archive: SimpleElement = () => { 8 | const { value, canLoad, loadMore } = useArchive(); 9 | 10 | useAutoLoadArchive({ canLoad, loadMore, breakPoint: 600 }); 11 | 12 | return ( 13 | <> 14 | 15 |
    16 | {Object.keys(value).map((year) => ( 17 | 18 | ))} 19 | {canLoad && } 20 |
    21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /containers/Blog/blogContentBody.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { mark } from "utils/markdown"; 3 | import { getClass } from "utils/dom"; 4 | import { BlogProps } from "types"; 5 | 6 | import style from "./index.module.scss"; 7 | 8 | export const BlogContentBody = ({ blogTitle, blogContent }: Pick) => { 9 | const html = useMemo(() => mark.render(blogContent || ""), [blogContent]); 10 | return ( 11 |
  9. 12 |
    13 |

    {blogTitle}

    14 |
    15 |
    16 |
    17 |
  10. 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /containers/Blog/blogContentCheckCodeModule.tsx: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { Button } from "components/Button"; 3 | import { flexCenter, getClass } from "utils/dom"; 4 | import { useAutoLoadCheckCodeImg } from "hook/useAuto"; 5 | import { useCheckCodeModuleToSubmit } from "hook/useMessage"; 6 | import { BlogProps } from "types"; 7 | import { AutoRequestType } from "types/utils"; 8 | 9 | export const BlogContentCheckCodeModule = ({ 10 | blogId, 11 | request, 12 | requestCallback, 13 | }: { 14 | blogId: BlogProps["blogId"]; 15 | request: AutoRequestType; 16 | requestCallback: () => void; 17 | }) => { 18 | const imgRef = useAutoLoadCheckCodeImg({ imgUrl: apiName.captcha, strUrl: apiName.captchaStr }); 19 | const { formRef, inputRef, loading, canSubmit } = useCheckCodeModuleToSubmit({ request, requestCallback, blogId }); 20 | 21 | return ( 22 |
    23 | 26 | 验证码 27 |
    28 | 29 |
    30 | 33 |
    34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /containers/Blog/blogContentDeleteModule.tsx: -------------------------------------------------------------------------------- 1 | import { flexEnd, getClass } from "utils/dom"; 2 | import { Button } from "components/Button"; 3 | import { useDeleteModuleToSubmit } from "hook/useMessage"; 4 | import { AutoRequestType } from "types/utils"; 5 | import { ChildMessageProps, PrimaryMessageProps } from "types/components"; 6 | 7 | export const BlogContentDeleteModule = ({ props, request }: { props: T; request: AutoRequestType }) => { 8 | const { formRef, loading } = useDeleteModuleToSubmit({ props, request }); 9 | 10 | return ( 11 |
    12 |
    26 | 30 | {blogPriseState ? ( 31 | 35 | ) : null} 36 |
    37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /containers/Blog/blogContentLikeToPayModule.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { usePinch } from "hook/usePinch"; 3 | 4 | export const BlogContentLikeToPayModule = ({ aliUrl, wChatUrl }: { aliUrl: string; wChatUrl: string }) => { 5 | const coverRef = useRef(null); 6 | const [pinchRef1] = usePinch({ forWardCoverRef: coverRef }); 7 | const [pinchRef2] = usePinch({ forWardCoverRef: coverRef }); 8 | return ( 9 |
    10 |
    11 |
    12 |
    微信打赏
    13 |
    14 | ... 15 |
    16 |
    17 |
    18 |
    支付宝打赏
    19 |
    20 | ... 21 |
    22 |
    23 |
    24 |
    25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /containers/Blog/blogContentMessage.tsx: -------------------------------------------------------------------------------- 1 | import { useRef } from "react"; 2 | import { apiName } from "config/api"; 3 | import { flexCenter, getClass } from "utils/dom"; 4 | import { useInViewport } from "hook/useInView"; 5 | import { LoadRender } from "components/LoadRender"; 6 | import { BlogContentPrimaryMessage } from "./blogContentMessagePrimary"; 7 | import { BlogProps, PrimaryCommentProps, UserProps } from "types"; 8 | 9 | export const BlogContentMessage = ({ blogId }: Pick) => { 10 | const ref = useRef(null); 11 | const isInView = useInViewport({ ref }); 12 | const isViewRef = useRef(isInView); 13 | isViewRef.current = isViewRef.current || isInView; 14 | return ( 15 |
  11. 16 |
    17 |
    留言区
    18 | {isViewRef.current && ( 19 | > 20 | token 21 | query={{ blogId }} 22 | revalidateOnMount={isInView} 23 | apiPath={apiName.primaryMessage} 24 | loaded={(data) => 25 | data.length ? :
    暂时没有留言
    26 | } 27 | /> 28 | )} 29 |
    30 |
  12. 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /containers/Blog/blogContentMessageMarkdown.tsx: -------------------------------------------------------------------------------- 1 | import { memo, MutableRefObject, useCallback, useEffect, useRef } from "react"; 2 | import MdEditor from "react-markdown-editor-lite"; 3 | import { useEditor } from "hook/useBlog"; 4 | import { log } from "utils/log"; 5 | import { markNOLineNumber } from "utils/markdown"; 6 | 7 | import "react-markdown-editor-lite/lib/index.css"; 8 | 9 | const BlogContentMessageMarkdown = ({ 10 | forwardRef, 11 | name = "content", 12 | defaultValue = "", 13 | className = "", 14 | }: { name?: string; defaultValue?: string; forwardRef?: MutableRefObject; className?: string } = {}) => { 15 | const ref = useRef(null); 16 | 17 | useEffect(() => { 18 | if (forwardRef && ref.current) { 19 | forwardRef.current = ref.current.querySelector(`#editor_${name}_md`); 20 | return () => { 21 | forwardRef.current = null; 22 | }; 23 | } else { 24 | log("markdown element handle error", "error"); 25 | } 26 | }, []); 27 | 28 | useEditor(name); 29 | 30 | const mdRender = useCallback((text) => markNOLineNumber.render(text), []); 31 | 32 | return ( 33 |
    34 | 42 |
    43 | ); 44 | }; 45 | 46 | // use memo to prevent render 47 | export const BlogContentMessageMarkdownWithMemo = memo(BlogContentMessageMarkdown); 48 | -------------------------------------------------------------------------------- /containers/Blog/blogContentTypeAndTag.tsx: -------------------------------------------------------------------------------- 1 | import { blogOrigin } from "config/publish"; 2 | import { BlogProps, ClientTagProps, TypeProps } from "types"; 3 | import { flexBetween, getClass } from "utils/dom"; 4 | 5 | export const BlogContentTypeAndTag = ({ 6 | typeContent, 7 | tagContent, 8 | blogOriginState, 9 | }: { 10 | typeContent: TypeProps["typeContent"]; 11 | tagContent: ClientTagProps["tagContent"]; 12 | blogOriginState: BlogProps["blogOriginState"]; 13 | }) => ( 14 |
  13. 15 |
    16 |
    17 | 分类 > 18 | {typeContent} 19 | | 20 | 来源 > 21 | {blogOrigin.find((it) => Number(it.value) === blogOriginState)?.name} 22 |
    23 |
    24 | {tagContent && 25 | tagContent.length && 26 | tagContent.map((item, i) => ( 27 | 28 | {item} 29 | 30 | ))} 31 |
    32 |
    33 |
  14. 34 | ); 35 | -------------------------------------------------------------------------------- /containers/Blog/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .imgHover { 4 | & img { 5 | position: relative; 6 | transition-property: transform; 7 | transition-duration: 0.3s; 8 | } 9 | &:hover { 10 | & img { 11 | transform: scale(1.1, 1.1); 12 | } 13 | } 14 | } 15 | 16 | .loadMore { 17 | font-size: 12px; 18 | } 19 | 20 | .footPage { 21 | font-size: 12px; 22 | & * { 23 | font-size: inherit; 24 | } 25 | } 26 | 27 | .replayModule, 28 | .deleteModule, 29 | .updateModule { 30 | @include mobile { 31 | font-size: 14px; 32 | } 33 | } 34 | 35 | .deleteModule { 36 | @include desktop { 37 | min-width: 500px; 38 | } 39 | @include mobile { 40 | min-width: 340px; 41 | } 42 | } 43 | 44 | .checkCodeRow { 45 | @include mobile { 46 | font-size: 14px; 47 | & > * { 48 | height: 32px; 49 | } 50 | } 51 | } 52 | 53 | .blogContent { 54 | @include mobile { 55 | margin: 0 -1.2rem; 56 | } 57 | } 58 | 59 | .markdown { 60 | position: relative; 61 | min-width: 60px; 62 | display: flow-root; 63 | cursor: pointer; 64 | & > span { 65 | opacity: 0; 66 | } 67 | & > i { 68 | position: absolute; 69 | top: 50%; 70 | left: 50%; 71 | transform: translate(-50%, -50%); 72 | font-size: 30px; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /containers/Blog/index.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import { BlogHead } from "components/BlogHead"; 3 | import { useUpdateBlogRead } from "hook/useBlog"; 4 | import { BlogContentImg } from "./blogContentImg"; 5 | import { BlogContentTypeAndTag } from "./blogContentTypeAndTag"; 6 | import { BlogContentBody } from "./blogContentBody"; 7 | import { BlogContentLike } from "./blogContentLike"; 8 | import { BlogContentMessagePut } from "./blogContentMessagePut"; 9 | import { BlogProps, ClientTagProps, TypeProps, UserProps } from "types"; 10 | 11 | const BlogContentMessage = dynamic<{ blogId: string }>(() => import("./blogContentMessage").then((r) => r.BlogContentMessage)); 12 | 13 | export const Blog = (props: BlogProps & UserProps & TypeProps & ClientTagProps) => { 14 | const { blogImgLink, typeContent, tagContent, blogTitle, blogContent, blogId, blogOriginState, blogPriseState, blogCommentState, userId } = props; 15 | 16 | useUpdateBlogRead(blogId); 17 | 18 | return ( 19 |
    20 | 21 |
      22 | 23 | 24 | 25 | 26 | {blogCommentState ? : null} 27 | {blogCommentState ? : null} 28 |
    29 |
    30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /containers/Editor/editorSubmit.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "components/Button"; 2 | import { useRouter } from "next/dist/client/router"; 3 | 4 | export const EditorSubmit = ({ submit }: { submit: () => Promise }) => { 5 | const router = useRouter(); 6 | 7 | return ( 8 |
    9 |
    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /containers/Editor/index.tsx: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { PublishHead as EditorHead } from "components/Publish/publishHead"; 3 | import { PublishEditor as EditorEditor } from "components/Publish/publishEditor"; 4 | import { PublishTypeAndTag as EditorTypeTag } from "components/Publish/publishTypeAndTag"; 5 | import { PublishImage as EditorImage } from "containers/Publish/publishImage"; 6 | import { PublishState as EditorState } from "components/Publish/publishState"; 7 | import { EditorSubmit } from "./editorSubmit"; 8 | import { useUserRequest } from "hook/useUser"; 9 | import { useUpdateBlog } from "hook/useBlog"; 10 | import { BlogProps, ClientTagProps, TypeProps, UserProps } from "types"; 11 | 12 | export const Editor = (props: BlogProps & UserProps & TypeProps & ClientTagProps) => { 13 | const request = useUserRequest({ method: "post", apiPath: apiName.updateBlog, header: { apiToken: true }, data: { oldProps: props }, cache: false }); 14 | 15 | const [ref, submit] = useUpdateBlog({ request, id: props.blogId }); 16 | 17 | return ( 18 |
    19 |
    20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
    28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /containers/Login/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .loginForm { 4 | position: relative; 5 | box-shadow: 0px 0px 8px 2px rgba(100, 100, 100, 0.2); 6 | @include desktop { 7 | width: 50%; 8 | } 9 | @include mobile { 10 | width: 80%; 11 | } 12 | } 13 | 14 | .success { 15 | position: absolute; 16 | font-size: 12px; 17 | color: green; 18 | right: 2px; 19 | top: 50%; 20 | transform: translateY(-50%); 21 | } 22 | 23 | .fail { 24 | position: absolute; 25 | font-size: 12px; 26 | color: red; 27 | right: 2px; 28 | top: 50%; 29 | transform: translateY(-50%); 30 | } 31 | 32 | .back { 33 | right: 10px; 34 | top: 14px; 35 | text-decoration: none !important; 36 | transition-property: color; 37 | transition-duration: 0.2s; 38 | } 39 | 40 | .checkCode { 41 | transition-property: height; 42 | transition-duration: 0.3s; 43 | } 44 | -------------------------------------------------------------------------------- /containers/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useState } from "react"; 3 | import { LoginUsername } from "./loginUsername"; 4 | import { LoginPassword } from "./loginPassword"; 5 | import { LoginCheckCode } from "./loginCheckCode"; 6 | import { LoginSubmit } from "./loginSubmit"; 7 | import { useLogin } from "hook/useUser"; 8 | import { flexCenter, getClass } from "utils/dom"; 9 | import { SimpleElement } from "types/components"; 10 | 11 | import style from "./index.module.scss"; 12 | 13 | export const Login: SimpleElement = () => { 14 | const formRef = useLogin(); 15 | 16 | const [username, setUsername] = useState(false); 17 | 18 | const [password, setPassword] = useState(false); 19 | 20 | return ( 21 |
    22 | 23 | 24 | 25 | 返回首页 26 | 27 | 28 |

    登录

    29 |
    30 | 31 | 32 | 33 | 34 | 35 |
    36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /containers/Login/loginCheckCode.tsx: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { AnimationItem } from "components/AnimationList"; 3 | import { useAutoLoadCheckCodeImg, useAutoSetHeight } from "hook/useAuto"; 4 | import { clearImg } from "utils/image"; 5 | import { actionHandler } from "utils/action"; 6 | import { flexBetween, getClass } from "utils/dom"; 7 | 8 | import style from "./index.module.scss"; 9 | 10 | export const LoginCheckCode = ({ show }: { show: boolean }) => { 11 | const [ref, height] = useAutoSetHeight(); 12 | 13 | const imgRef = useAutoLoadCheckCodeImg({ imgUrl: apiName.captcha, strUrl: apiName.captchaStr, state: show }); 14 | 15 | return ( 16 | actionHandler(imgRef.current, (ele) => clearImg(ele))}> 17 |
    22 | 25 |
    26 | 27 | 28 |
    29 |
    30 |
    31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /containers/Login/loginPassword.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "components/Input"; 2 | import { login } from "config/user"; 3 | 4 | export const LoginPassword = ({ setState }: { setState: (p: boolean) => void }) => { 5 | return ( 6 |
    7 | 10 | 11 |
    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /containers/Login/loginSubmit.tsx: -------------------------------------------------------------------------------- 1 | export const LoginSubmit = ({ enabled }: { enabled: boolean }) => { 2 | return ( 3 |
    4 | 7 |
    8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /containers/Login/loginUsername.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "components/Input"; 2 | import { login } from "config/user"; 3 | 4 | export const LoginUsername = ({ setState }: { setState: (p: boolean) => void }) => { 5 | return ( 6 |
    7 | 10 | 11 |
    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /containers/Main/index.tsx: -------------------------------------------------------------------------------- 1 | import { MainLeft } from "./mainLeft"; 2 | import { MainRight } from "./mainRight"; 3 | import { SimpleElement } from "types/components"; 4 | 5 | export const Main: SimpleElement = () => { 6 | return ( 7 |
    8 | 9 | 10 |
    11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /containers/Main/mainLeft.tsx: -------------------------------------------------------------------------------- 1 | import { MainLeftHead } from "./mainLeftHead"; 2 | import { MainLeftFoot } from "./mainLeftFoot"; 3 | import { MainLeftContent } from "./mainLeftContent"; 4 | import { SimpleElement } from "types/components"; 5 | 6 | export const MainLeft: SimpleElement = () => { 7 | return ( 8 |
    9 |
    10 | 11 | 12 | 13 |
    14 |
    15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /containers/Main/mainLeftContent.tsx: -------------------------------------------------------------------------------- 1 | import { useHome } from "hook/useHome"; 2 | import { WithReadBlogItem as MainLeftItem } from "components/BlogItem"; 3 | import { AnimationList } from "components/AnimationList"; 4 | import { SimpleElement } from "types/components"; 5 | 6 | export const MainLeftContent: SimpleElement = () => { 7 | const { currentPageBlogs } = useHome(); 8 | 9 | return ( 10 | 11 | {currentPageBlogs.map((props) => ( 12 | 13 | ))} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /containers/Main/mainLeftFoot.tsx: -------------------------------------------------------------------------------- 1 | import { FootPage } from "components/PageFoot"; 2 | import { useHome } from "hook/useHome"; 3 | import { SimpleElement } from "types/components"; 4 | 5 | export const MainLeftFoot: SimpleElement = () => { 6 | let { currentPage, increaseAble, decreaseAble, increasePage, decreasePage } = useHome(); 7 | 8 | return ( 9 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /containers/Main/mainLeftHead.tsx: -------------------------------------------------------------------------------- 1 | import { useHome } from "hook/useHome"; 2 | import { SimpleElement } from "types/components"; 3 | import { flexBetween, getClass } from "utils/dom"; 4 | 5 | export const MainLeftHead: SimpleElement = () => { 6 | let { allPage } = useHome(); 7 | 8 | return ( 9 |
    10 | 博客 11 |
    12 | 共{allPage}页 13 |
    14 |
    15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /containers/Main/mainRight.tsx: -------------------------------------------------------------------------------- 1 | import { mainRight } from "config/home"; 2 | import { AnimationItem } from "components/AnimationList"; 3 | import { MainRightType } from "./mainRightType"; 4 | import { MainRightTag } from "./mainRightTag"; 5 | import { MainRightCommend } from "./mainRightCommend"; 6 | import { SimpleElement } from "types/components"; 7 | 8 | export const MainRight: SimpleElement = () => { 9 | return ( 10 | 11 |
    12 | 13 | 14 | 15 |
    16 |
    17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /containers/Main/mainRightCommend.tsx: -------------------------------------------------------------------------------- 1 | import { CardHead as MainRightHead } from "components/CardHead"; 2 | import { MainRightCommendItem } from "./mainRightCommendItem"; 3 | import { mainRightHeader } from "config/home"; 4 | import { useCommend } from "hook/useHome"; 5 | 6 | export const MainRightCommend = ({ index }: { index: number }) => { 7 | const { commendBlogs } = useCommend(); 8 | 9 | const { icon, content, hrefTo } = mainRightHeader[index]; 10 | 11 | return ( 12 |
    13 | 14 |
    15 | {commendBlogs.map(({ blogId, blogTitle }) => ( 16 | 17 | ))} 18 |
    19 |
    20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /containers/Main/mainRightCommendItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { HomeBlogProps } from "types"; 3 | 4 | export const MainRightCommendItem = ({ blogTitle, blogId }: Pick) => { 5 | return ( 6 | 7 | {blogTitle} 8 | 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /containers/Main/mainRightTag.tsx: -------------------------------------------------------------------------------- 1 | import { LoadRender } from "components/LoadRender"; 2 | import { CardHead as MainRightHead } from "components/CardHead"; 3 | import { AnimationList } from "components/AnimationList"; 4 | import { MainRightTagItem } from "./mainRightTagItem"; 5 | import { mainRightHeader } from "config/home"; 6 | import { apiName } from "config/api"; 7 | import { useTag } from "hook/useTag"; 8 | import { ServerTagProps } from "types"; 9 | 10 | export const MainRightTag = ({ index }: { index: number }) => { 11 | const { tag, changeCurrentTag } = useTag({ needInitTag: true }); 12 | 13 | const { icon, content, hrefTo } = mainRightHeader[index]; 14 | 15 | return ( 16 |
    17 | 18 |
    19 | 20 | needUpdate 21 | needInitialData 22 | initialData={tag} 23 | apiPath={apiName.tag} 24 | loaded={(data) => ( 25 | 26 | {data.map(({ tagId, tagContent, tagCount }) => ( 27 | 28 | ))} 29 | 30 | )} 31 | /> 32 |
    33 |
    34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /containers/Main/mainRightTagItem.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import Link from "next/link"; 3 | import { Tag } from "components/Tag"; 4 | 5 | export const MainRightTagItem = ({ tagName, tagCount, changeCurrentTag }: { tagName: string; tagCount: number; changeCurrentTag: (p: string) => void }) => { 6 | const clickHandler = useCallback(() => changeCurrentTag(tagName), [changeCurrentTag, tagName]); 7 | 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /containers/Main/mainRightType.tsx: -------------------------------------------------------------------------------- 1 | import { LoadRender } from "components/LoadRender"; 2 | import { CardHead as MainRightHead } from "components/CardHead"; 3 | import { AnimationList } from "components/AnimationList"; 4 | import { MainRightTypeItem } from "./mainRightTypeItem"; 5 | import { mainRightHeader } from "config/home"; 6 | import { apiName } from "config/api"; 7 | import { useType } from "hook/useType"; 8 | import { TypeProps } from "types"; 9 | 10 | export const MainRightType = ({ index }: { index: number }) => { 11 | const { type, changeCurrentType } = useType({ needInitType: true }); 12 | 13 | const { icon, content, hrefTo } = mainRightHeader[index]; 14 | 15 | return ( 16 |
    17 | 18 |
    19 |
    20 | 21 | needUpdate 22 | needInitialData 23 | initialData={type} 24 | apiPath={apiName.type} 25 | loaded={(data) => ( 26 | 27 | {data.map(({ typeId, typeContent, typeCount }) => ( 28 | 29 | ))} 30 | 31 | )} 32 | /> 33 |
    34 |
    35 |
    36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /containers/Main/mainRightTypeItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useCallback } from "react"; 3 | import { flexBetween, getClass } from "utils/dom"; 4 | 5 | export const MainRightTypeItem = ({ 6 | typeName, 7 | typeCount, 8 | changeCurrentType, 9 | }: { 10 | typeName: string; 11 | typeCount: number; 12 | changeCurrentType: (t: string) => void; 13 | }) => { 14 | const clickHandler = useCallback(() => changeCurrentType(typeName), [changeCurrentType, typeName]); 15 | 16 | return ( 17 | 18 | 19 | {typeName} 20 | {typeCount} 21 | 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /containers/Manage/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/variable.module"; 2 | @import "../../styles/basic.module"; 3 | 4 | .closeIcon { 5 | cursor: pointer; 6 | &:hover { 7 | color: indigo; 8 | } 9 | } 10 | 11 | .item { 12 | min-width: 160px; 13 | } 14 | 15 | .swatchLink { 16 | text-decoration: none !important; 17 | transition-property: color; 18 | transition-duration: 0.2s; 19 | cursor: pointer; 20 | } 21 | 22 | .blogItem { 23 | .caverItem { 24 | position: absolute; 25 | width: 50%; 26 | height: 0; 27 | left: 50%; 28 | top: 0; 29 | overflow: hidden; 30 | background-color: transparent; 31 | transition-property: background; 32 | transition-duration: 0.2s; 33 | } 34 | 35 | &:hover { 36 | .caverItem { 37 | height: 100%; 38 | background-color: rgba(100, 100, 100, 0.6); 39 | } 40 | } 41 | } 42 | 43 | .deleteBlogItem { 44 | 45 | @include desktop { 46 | max-width: 600px; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /containers/Manage/index.tsx: -------------------------------------------------------------------------------- 1 | import { ManageLeft } from "./manageLeft"; 2 | import { ManageRight } from "./manageRight"; 3 | 4 | export const Manage = ({ userId }: { userId: string }) => { 5 | return ( 6 |
    7 | 8 | 9 |
    10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /containers/Manage/manageAddModule.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Input } from "components/Input"; 3 | import { Button } from "components/Button"; 4 | import { addModule } from "config/manage"; 5 | import { useAddRequest } from "hook/useManage"; 6 | import { AutoRequestType } from "types/utils"; 7 | import { apiName } from "config/api"; 8 | 9 | export const ManageAddModule = ({ 10 | request, 11 | judgeApiName, 12 | fieldName, 13 | successCallback, 14 | }: { 15 | request: AutoRequestType; 16 | judgeApiName: apiName; 17 | fieldName: string; 18 | successCallback: () => void; 19 | }) => { 20 | const [bool, setBool] = useState(false); 21 | 22 | const [ref, loading] = useAddRequest({ 23 | request, 24 | successCallback, 25 | }); 26 | 27 | return ( 28 |
    29 |
    30 | 31 |
    34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /containers/Manage/manageAddTagButton.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | import { mutate } from "swr"; 3 | import { apiName } from "config/api"; 4 | import { ManageAddModule } from "./manageAddModule"; 5 | import { createRequest } from "utils/fetcher"; 6 | import { useUserRequest } from "hook/useUser"; 7 | import { useManageToAddModule } from "hook/useManage"; 8 | import { SimpleElement } from "types/components"; 9 | 10 | export const ManageAddTagButton: SimpleElement = () => { 11 | const request = useUserRequest({ method: "post", apiPath: apiName.addTag, cache: false }); 12 | 13 | const successCallback = useCallback(() => { 14 | const tagRequest = createRequest({ apiPath: apiName.tag }); 15 | tagRequest.deleteCache(); 16 | mutate(tagRequest.cacheKey); 17 | }, []); 18 | 19 | const body = useMemo( 20 | () => , 21 | [request, successCallback] 22 | ); 23 | 24 | const click = useManageToAddModule({ 25 | body, 26 | title: "添加标签", 27 | }); 28 | 29 | return ( 30 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /containers/Manage/manageAddTypeButton.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from "react"; 2 | import { mutate } from "swr"; 3 | import { apiName } from "config/api"; 4 | import { ManageAddModule } from "./manageAddModule"; 5 | import { useUserRequest } from "hook/useUser"; 6 | import { createRequest } from "utils/fetcher"; 7 | import { useManageToAddModule } from "hook/useManage"; 8 | import { SimpleElement } from "types/components"; 9 | 10 | export const ManageAddTypeButton: SimpleElement = () => { 11 | const request = useUserRequest({ method: "post", apiPath: apiName.addType, cache: false }); 12 | 13 | const successCallback = useCallback(() => { 14 | const typeRequest = createRequest({ apiPath: apiName.type }); 15 | typeRequest.deleteCache(); 16 | mutate(typeRequest.cacheKey); 17 | }, []); 18 | 19 | const body = useMemo(() => , [request, successCallback]) 20 | 21 | const click = useManageToAddModule({ 22 | body, 23 | title: "添加分类", 24 | }); 25 | 26 | return ( 27 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /containers/Manage/manageDeleteModule.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "components/Button"; 2 | import { useDeleteRequest } from "hook/useManage"; 3 | import { AutoRequestType } from "types/utils"; 4 | 5 | export const ManageDeleteModule = ({ 6 | request, 7 | deleteItem, 8 | successHandler, 9 | }: { 10 | request: AutoRequestType; 11 | deleteItem: JSX.Element; 12 | successHandler: () => void; 13 | }) => { 14 | const click = useDeleteRequest({ request, successHandler }); 15 | 16 | return ( 17 |
    18 |
    删除以下项目:
    19 | {deleteItem} 20 |
    21 |
    23 |
    24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /containers/Manage/manageLeft.tsx: -------------------------------------------------------------------------------- 1 | import { ManageSearch } from "./manageSearch"; 2 | import { ManageResult } from "./manageResult"; 3 | import { UserProps } from "types"; 4 | 5 | export const ManageLeft = ({ userId }: Pick) => { 6 | return ( 7 |
    8 | 9 | 10 |
    11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /containers/Manage/manageResultAll.tsx: -------------------------------------------------------------------------------- 1 | import { FootPage } from "components/PageFoot"; 2 | import { WithWriteBlogItem as SearchResult } from "components/BlogItem"; 3 | import { useBasePage } from "hook/useBase"; 4 | import type { ClientTagProps, HomeBlogProps, TypeProps, UserProps } from "types"; 5 | 6 | export const ManageResultAll = ({ data }: { data: Array }) => { 7 | const { currentPage, currentPageData, increaseAble, increasePage, decreaseAble, decreasePage } = useBasePage({ 8 | data, 9 | }); 10 | 11 | return ( 12 | <> 13 | {currentPageData.length ? ( 14 | <> 15 | {currentPageData.map((props) => ( 16 | 17 | ))} 18 | 19 | 20 | ) : ( 21 |
    没有结果显示!
    22 | )} 23 | 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /containers/Manage/manageResultSearch.tsx: -------------------------------------------------------------------------------- 1 | import { FootPage } from "components/PageFoot"; 2 | import { Loading } from "components/Loading"; 3 | import { ManageDeleteBlogItem } from "./manageDeleteBlogItem"; 4 | import { getClass } from "utils/dom"; 5 | import { useBasePage } from "hook/useBase"; 6 | import { manageLength } from "config/manage"; 7 | import { ClientReducer } from "store/reducer/client/type"; 8 | import { actionName } from "config/action"; 9 | 10 | import style from "./index.module.scss"; 11 | 12 | export const ManageResultSearch = (props: { data?: ClientReducer[actionName.currentResult]["data"]; loading?: boolean } = {}) => { 13 | const { data = [], loading = false } = props; 14 | 15 | const { currentPageData, currentPage, increaseAble, increasePage, decreaseAble, decreasePage } = useBasePage({ 16 | data, 17 | pageLength: manageLength, 18 | }); 19 | 20 | return ( 21 | <> 22 | {loading ? ( 23 | 24 | ) : currentPageData.length ? ( 25 | <> 26 | {currentPageData.map((props) => ( 27 |
    28 | 29 |
    30 | ))} 31 | 32 | 33 | ) : ( 34 |
    没有搜索结果!
    35 | )} 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /containers/Manage/manageRight.tsx: -------------------------------------------------------------------------------- 1 | import { ManageTag } from "./manageTag"; 2 | import { ManageType } from "./manageType"; 3 | import { ManageAddTagButton } from "./manageAddTagButton"; 4 | import { ManageAddTypeButton } from "./manageAddTypeButton"; 5 | import { SimpleElement } from "types/components"; 6 | 7 | export const ManageRight: SimpleElement = () => { 8 | return ( 9 |
    10 |
    11 |
    12 | 标签 13 |
    14 | 15 |
    16 | 17 |
    18 |
    19 |
    20 |
    21 | 分类 22 |
    23 | 24 |
    25 | 26 |
    27 |
    28 |
    29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /containers/Manage/manageSearch.tsx: -------------------------------------------------------------------------------- 1 | import { Drop } from "components/Drop"; 2 | import { Button } from "components/Button"; 3 | import { LoadRender } from "components/LoadRender"; 4 | import { useSearch } from "hook/useManage"; 5 | import { apiName } from "config/api"; 6 | import { DropItemProps, SimpleElement } from "types/components"; 7 | import { ServerTagProps, TypeProps } from "types"; 8 | 9 | export const ManageSearch: SimpleElement = () => { 10 | const [ref, search] = useSearch(); 11 | 12 | return ( 13 |
    14 |
    15 | 16 | 17 | needUpdate 18 | needInitialData 19 | apiPath={apiName.type} 20 | loaded={(res) => { 21 | const data: DropItemProps[] = res.map(({ typeContent, typeId }) => ({ name: typeContent, value: typeId! })); 22 | return _style={{ zIndex: "3" }} fieldName="typeId" className="form-control m-2" placeHolder="选择分类" data={data} />; 23 | }} 24 | /> 25 | 26 | needUpdate 27 | needInitialData 28 | apiPath={apiName.tag} 29 | loaded={(res) => { 30 | const data: DropItemProps[] = res.map(({ tagContent, tagId }) => ({ name: tagContent, value: tagId! })); 31 | return fieldName="tagId" className="form-control m-2" placeHolder="选择标签" data={data} multiple />; 32 | }} 33 | /> 34 |
    37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /containers/Manage/manageTag.tsx: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { LoadRender } from "components/LoadRender"; 3 | import { ManageDeleteTagItem } from "./manageDeleteTagItem"; 4 | import { SimpleElement } from "types/components"; 5 | import { ServerTagProps } from "types"; 6 | 7 | export const ManageTag: SimpleElement = () => { 8 | return ( 9 |
    10 | 11 | needUpdate 12 | needInitialData 13 | apiPath={apiName.tag} 14 | loaded={(data) => { 15 | return ( 16 | <> 17 | {data.map(({ tagCount, tagContent, tagId, tagState }) => ( 18 |
    19 | 20 |
    21 | ))} 22 | 23 | ); 24 | }} 25 | /> 26 |
    27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /containers/Manage/manageType.tsx: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { LoadRender } from "components/LoadRender"; 3 | import { ManageDeleteTypeItem } from "./manageDeleteTypeItem"; 4 | import { SimpleElement } from "types/components"; 5 | import { TypeProps } from "types"; 6 | 7 | export const ManageType: SimpleElement = () => { 8 | return ( 9 |
    10 | 11 | needUpdate 12 | needInitialData 13 | apiPath={apiName.type} 14 | loaded={(data) => { 15 | return ( 16 | <> 17 | {data.map(({ typeId, typeContent, typeCount, typeState }) => ( 18 |
    19 | 20 |
    21 | ))} 22 | 23 | ); 24 | }} 25 | /> 26 |
    27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /containers/NotLogin/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { SimpleElement } from "types/components"; 3 | 4 | export const NotLogin: SimpleElement = () => { 5 | return ( 6 |
    7 |

    访问错误!

    8 |

    请登录后继续!

    9 |
    10 |

    点击按钮去登录

    11 | 12 | 13 | 去登录 14 | 15 | 16 |
    17 | ); 18 | }; -------------------------------------------------------------------------------- /containers/Publish/index.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/basic.module'; 2 | 3 | .btn { 4 | right: 4px; 5 | top: 50%; 6 | z-index: 1; 7 | transform: translateY(-50%); 8 | } 9 | 10 | .imgContainer { 11 | padding-bottom: 56%; 12 | @include desktop { 13 | width: 400px; 14 | } 15 | @include mobile { 16 | width: 100%; 17 | max-width: 400px; 18 | min-width: 300px; 19 | } 20 | } 21 | 22 | .imgItem { 23 | top: 50%; 24 | left: 50%; 25 | transform: translate(-50%, -50%); 26 | cursor: pointer; 27 | } 28 | 29 | .imgLoading { 30 | top: 50%; 31 | left: 50%; 32 | transform: translate(-50%, -50%); 33 | } 34 | -------------------------------------------------------------------------------- /containers/Publish/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { apiName } from "config/api"; 3 | import { editorId } from "config/publish"; 4 | import { usePublish } from "hook/useBlog"; 5 | import { createRequest } from "utils/fetcher"; 6 | import { PublishHead } from "components/Publish/publishHead"; 7 | import { PublishState } from "components/Publish/publishState"; 8 | import { PublishEditor } from "components/Publish/publishEditor"; 9 | import { PublishTypeAndTag } from "components/Publish/publishTypeAndTag"; 10 | import { PublishImage } from "./publishImage"; 11 | import { PublishSubmit } from "./publishSubmit"; 12 | import { SimpleElement } from "types/components"; 13 | 14 | export const Publish: SimpleElement = () => { 15 | const request = useMemo(() => createRequest({ method: "post", apiPath: apiName.publishBlog, header: { apiToken: true }, cache: false }), []); 16 | 17 | const [ref, submit] = usePublish({ request, id: editorId }); 18 | 19 | return ( 20 |
    21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
    30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /containers/Publish/publishImage.tsx: -------------------------------------------------------------------------------- 1 | import { RefObject, useCallback } from "react"; 2 | import { getClass } from "utils/dom"; 3 | import { useInputToImageModule } from "hook/useBlog"; 4 | import { PublishImageModule } from "./publishImageModule"; 5 | import { BlogProps } from "types"; 6 | 7 | import style from "./index.module.scss"; 8 | 9 | export const PublishImage = ({ blogImgLink }: Partial>) => { 10 | const body = useCallback<(ref: RefObject) => (closeHandler: () => void) => JSX.Element>( 11 | (ref) => (closeHandler) => { 12 | const WithImage = ; 13 | return WithImage; 14 | }, 15 | [blogImgLink] 16 | ); 17 | 18 | const [inputRef, click] = useInputToImageModule({ 19 | body, 20 | }); 21 | 22 | return ( 23 |
    24 |
    25 | 29 | 首图 30 | 31 |
    32 | 35 | 36 |
    37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /containers/Publish/publishSubmit.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "components/Button"; 2 | import { useRouter } from "next/dist/client/router"; 3 | 4 | export const PublishSubmit = ({ submit }: { submit: () => Promise }) => { 5 | const router = useRouter(); 6 | 7 | return ( 8 |
    9 |
    12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /containers/ReLogin/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { Button } from "components/Button"; 3 | import { useLogout } from "hook/useUser"; 4 | import { useRouter } from "next/dist/client/router"; 5 | import { flexBetween, getClass } from "utils/dom"; 6 | import { SimpleElement } from "types/components"; 7 | 8 | export const ReLogin: SimpleElement = () => { 9 | const logout = useLogout(); 10 | 11 | const router = useRouter(); 12 | 13 | const reLogin = useCallback(() => logout().then(router.reload).catch(router.reload), [router, logout]); 14 | 15 | return ( 16 |
    17 |

    访问错误!

    18 |

    当前已经是登录状态,不能重复登录!

    19 |
    20 |

    点击下面按钮返回,或者点击使用其他身份登录

    21 |
    22 | 25 |
    27 |
    28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /containers/Tag/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .autoHide { 4 | max-height: 206px; 5 | overflow-y: auto; 6 | @include mobile { 7 | display: none; 8 | } 9 | } 10 | 11 | .tagActive { 12 | background-color: #ccc; 13 | & * { 14 | background-color: inherit; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /containers/Tag/index.tsx: -------------------------------------------------------------------------------- 1 | import { TagHead } from "./tagHead"; 2 | import { TagFoot } from "./tagFoot"; 3 | import { TagContent } from "./tagContent"; 4 | import { LoadRender } from "components/LoadRender"; 5 | import { useHome } from "hook/useHome"; 6 | import { apiName } from "config/api"; 7 | import { SimpleElement } from "types/components"; 8 | import { HomeProps } from "store/reducer/server/action/home"; 9 | 10 | export const Tag: SimpleElement = () => { 11 | const { blogs } = useHome(); 12 | 13 | return ( 14 | <> 15 | 16 |
    17 | 18 | needUpdate 19 | needInitialData 20 | initialData={blogs} 21 | apiPath={apiName.home} 22 | loaded={(data) => { 23 | return ( 24 | <> 25 | 26 | 27 | 28 | ); 29 | }} 30 | /> 31 |
    32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /containers/Tag/tagFoot.tsx: -------------------------------------------------------------------------------- 1 | import { FootPage } from "components/PageFoot"; 2 | import { useTag } from "hook/useTag"; 3 | import { HomeProps } from "store/reducer/server/action/home"; 4 | 5 | export const TagFoot = ({ blogs }: { blogs: HomeProps }) => { 6 | let { currentPage, increaseAble, decreaseAble, increasePage, decreasePage, currentPageBlogs } = useTag({ blogs }); 7 | 8 | return currentPageBlogs.length ? ( 9 | 17 | ) : null; 18 | }; 19 | -------------------------------------------------------------------------------- /containers/Tag/tagHead.tsx: -------------------------------------------------------------------------------- 1 | import { useTag } from "hook/useTag"; 2 | import { WithChangeTag as TagItem } from "components/Tag"; 3 | import { flexBetween, getClass } from "utils/dom"; 4 | import { SimpleElement } from "types/components"; 5 | 6 | import style from "./index.module.scss"; 7 | 8 | export const TagHead: SimpleElement = () => { 9 | const { tag, currentTag } = useTag(); 10 | 11 | return ( 12 |
    13 |
    14 | 标签 15 |
    16 | 17 | {tag.length} 18 | 19 |
    20 |
    21 |
    22 | {tag.length && 23 | tag.map(({ tagId, tagContent, tagCount }) => ( 24 |
    25 | 26 |
    27 | ))} 28 |
    29 |
    30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /containers/Type/index.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/basic.module"; 2 | 3 | .autoHide { 4 | max-height: 206px; 5 | overflow-y: auto; 6 | @include mobile { 7 | display: none; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /containers/Type/index.tsx: -------------------------------------------------------------------------------- 1 | import { TypeHead } from "./typeHead"; 2 | import { TypeContent } from "./typeContent"; 3 | import { TypeFoot } from "./typeFoot"; 4 | import { LoadRender } from "components/LoadRender"; 5 | import { useHome } from "hook/useHome"; 6 | import { apiName } from "config/api"; 7 | import { SimpleElement } from "types/components"; 8 | import { HomeProps } from "store/reducer/server/action/home"; 9 | 10 | export const Type: SimpleElement = () => { 11 | const { blogs } = useHome(); 12 | 13 | return ( 14 | <> 15 | 16 |
    17 | 18 | needUpdate 19 | needInitialData 20 | initialData={blogs} 21 | apiPath={apiName.home} 22 | loaded={(data) => { 23 | return ( 24 | <> 25 | 26 | 27 | 28 | ); 29 | }} 30 | /> 31 |
    32 | 33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /containers/Type/typeFoot.tsx: -------------------------------------------------------------------------------- 1 | import { FootPage } from "components/PageFoot"; 2 | import { useType } from "hook/useType"; 3 | import { HomeProps } from "store/reducer/server/action/home"; 4 | 5 | export const TypeFoot = ({ blogs }: { blogs: HomeProps }) => { 6 | let { currentPage, increaseAble, decreaseAble, increasePage, decreasePage, currentPageBlogs } = useType({ blogs }); 7 | 8 | return currentPageBlogs.length ? ( 9 | 17 | ) : null; 18 | }; 19 | -------------------------------------------------------------------------------- /containers/Type/typeHead.tsx: -------------------------------------------------------------------------------- 1 | import { useType } from "hook/useType"; 2 | import { WithChangeType as TypeItem } from "components/Type"; 3 | import { flexBetween, flexCenter, getClass } from "utils/dom"; 4 | import { SimpleElement } from "types/components"; 5 | 6 | export const TypeHead: SimpleElement = () => { 7 | const { type, currentType } = useType(); 8 | 9 | return ( 10 |
    11 |
    12 | 分类 13 |
    14 | 15 | {type.length} 16 | 17 |
    18 |
    19 |
    20 | {type.length ? ( 21 | type.map(({ typeId, typeContent, typeCount }) => ( 22 |
    23 | 24 |
    25 | )) 26 | ) : ( 27 |
    nothing to display
    28 | )} 29 |
    30 |
    31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module NodeJS { 2 | interface Process { 3 | browser: boolean; 4 | } 5 | interface ProcessEnv { 6 | BING_URL: string; 7 | BING_API: string; 8 | DATABASE: string; 9 | COOKIE_PARSER: string; 10 | NEXT_PUBLIC_MAN: string; 11 | NEXT_PUBLIC_WOMEN: string; 12 | NEXT_PUBLIC_ABOUT: string; 13 | NEXT_PUBLIC_ADMIN: string; 14 | NEXT_PUBLIC_ONE_SAY: string; 15 | NEXT_PUBLIC_STRING: string; 16 | NEXT_PUBLIC_API_HOST: string; 17 | NEXT_PUBLIC_API_TOKEN: string; 18 | NEXT_PUBLIC_IP_ADDRESS: string; 19 | } 20 | } 21 | 22 | interface Window { 23 | __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: Function; 24 | __cache: any 25 | } 26 | -------------------------------------------------------------------------------- /hook/useHeader.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | import { useRouter } from "next/dist/client/router"; 3 | import { actionName } from "config/action"; 4 | import { useCurrentState } from "./useBase"; 5 | import { setDataSuccess_client } from "store/reducer/client/share/action"; 6 | 7 | interface UseHeaderItemType { 8 | (props?: { needInitHead?: boolean }): { currentHeader: string; changeCurrentHeader: (headItem: string) => void }; 9 | } 10 | 11 | const useHeaderItem: UseHeaderItemType = (props = {}) => { 12 | const { needInitHead = false } = props; 13 | const { asPath } = useRouter(); 14 | const { state: currentHeader, dispatch } = useCurrentState((state) => state.client[actionName.currentHeader]["data"]); 15 | const ref = useRef(currentHeader); 16 | ref.current = currentHeader; 17 | const changeCurrentHeader = useCallback<(props: string) => void>( 18 | (headItem) => { 19 | if (ref.current !== headItem) { 20 | dispatch(setDataSuccess_client({ name: actionName.currentHeader, data: headItem })); 21 | } 22 | }, 23 | [dispatch] 24 | ); 25 | useEffect(() => { 26 | if (needInitHead && asPath !== ref.current) { 27 | changeCurrentHeader(asPath); 28 | } 29 | }, [asPath, changeCurrentHeader, needInitHead]); 30 | return { currentHeader, changeCurrentHeader }; 31 | }; 32 | 33 | export { useHeaderItem }; 34 | -------------------------------------------------------------------------------- /hook/useHome.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { apiName } from "config/api"; 3 | import { actionName } from "config/action"; 4 | import { pageContentLength } from "config/home"; 5 | import { useCurrentState } from "./useBase"; 6 | import { setDataSuccess_client } from "store/reducer/client/share/action"; 7 | 8 | export const useHome = () => { 9 | // 首页全部数据 10 | const { state: blogs, dispatch } = useCurrentState((state) => state.server[apiName.home]["data"]); 11 | // 获取当前页数 12 | const { state: currentPage } = useCurrentState((state) => state.client[actionName.currentHomePage]["data"]); 13 | // 获取所有页数 14 | const allPage = Math.ceil(blogs.length / pageContentLength); 15 | const increasePage = useCallback(() => dispatch(setDataSuccess_client({ name: actionName.currentHomePage, data: currentPage + 1 })), [currentPage, dispatch]); 16 | const decreasePage = useCallback(() => dispatch(setDataSuccess_client({ name: actionName.currentHomePage, data: currentPage - 1 })), [currentPage, dispatch]); 17 | const increaseAble = currentPage < allPage; 18 | const decreaseAble = currentPage > 1; 19 | const currentPageBlogs = blogs.slice((currentPage - 1) * pageContentLength, currentPage * pageContentLength); 20 | if (allPage > 0 && currentPage > allPage) { 21 | setDataSuccess_client({ name: actionName.currentHomePage, data: allPage }); 22 | } 23 | return { currentPage, allPage, blogs, currentPageBlogs, increaseAble, decreaseAble, increasePage, decreasePage }; 24 | }; 25 | 26 | export const useCommend = () => { 27 | const { state: blogs } = useCurrentState((state) => state.server[apiName.home]["data"]); 28 | const commendBlogs = blogs.filter(({ blogState }) => Number(blogState) === 3); 29 | return { commendBlogs }; 30 | }; 31 | -------------------------------------------------------------------------------- /hook/useInView.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useState, useEffect } from "react"; 2 | 3 | export const useInViewport = ({ ref, getElement }: { ref?: RefObject; getElement?: () => T }) => { 4 | const [inViewPort, setInViewport] = useState(false); 5 | useEffect(() => { 6 | const el = ref ? ref.current : getElement ? getElement() : null; 7 | if (!el) { 8 | return () => {}; 9 | } 10 | 11 | const observer = new IntersectionObserver((entries) => { 12 | for (const entry of entries) { 13 | if (entry.isIntersecting) { 14 | setInViewport(true); 15 | } else { 16 | setInViewport(false); 17 | } 18 | } 19 | }); 20 | 21 | observer.observe(el as HTMLElement); 22 | 23 | return () => { 24 | observer.disconnect(); 25 | }; 26 | }, [ref, getElement]); 27 | 28 | return inViewPort; 29 | }; 30 | -------------------------------------------------------------------------------- /hook/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useIsMounted = () => { 4 | const [mounted, setMounted] = useState(false); 5 | useEffect(() => { 6 | setMounted(true); 7 | }, []); 8 | 9 | return mounted; 10 | }; 11 | -------------------------------------------------------------------------------- /md/1.博客需求总结.md: -------------------------------------------------------------------------------- 1 | ### 结合之前实现的博客,定下这个博客的需求 2 | 3 | 1. 管理员方面:博客拥有唯一的一个管理员,该管理员拥有超级权限,可以对博客上的文章,留言,用户信息进行统一管理 4 | 5 | 2. 作者方面:多人博客,通过邀请码可以实现作者信息注册登录,包括相关个人信息的完善,作者可以以个人身份发布文章(包括第三方转载、翻译),并且拥有这部分的管理权限,相关权限包括:博客发布,删除,修改(因为博客分为了几个部分,要每一部分都能够单独修改),隐藏(需要注册用户才能观看,?引入付费机制),?评论信息删除,?作者收益 6 | 7 | 3. 页面视图方面:整体以简约为主,大体样式还是之前的博客样式,细节需要处理好,响应式的效果还是参考之前的设计吧,其他设计可以参考知乎(?浅色,深色主题) 8 | 9 | 4. 文章方面:一个文章分为以下几个部分,文章分类(作者可以使用已有分类,管理员可以创建分类),文章标签(作者可以使用,也可以创建。一个文章可以有多个标签),文章来源(自创,转载,翻译...,管理员进行来源种类的管理),文章首图(?写入时可预览),文章内容(md格式,提供书写的实时预览,?支持插入图片,可保存不发布,发布时将简略内容单独保存作为列表显示内容),文章打赏,文章隐藏,文章留言(分页显示,多级留言,长信息显示缩略确保一夜不显示太长,作者以及注册用户个人中心,可以快速跳转) 10 | 11 | 5. 个人信息方面:个人信息页面,包括联系方式,头像,暱称,警言,个人拥有修改权限(?邮箱短信验证),注册用户与作者可以直接沟通交流(功能设计参考知乎) 12 | 13 | 6. 访客方面:普通访客拥有一般文章的浏览,留言,打赏权限,可以注册登录,完善个人信息后成为博客会员,拥有身份id,权限提高,支持即时沟通(?历史记录),身份转换后的信息同步,这... 14 | 15 | 7. 想到再说吧 -------------------------------------------------------------------------------- /md/3.ssr的redux.md: -------------------------------------------------------------------------------- 1 | ### 服务端渲染结合 redux 总结 2 | 3 | 传统的 SPA 应用的 redux 使用方式比较简单,只需要在组件的最外层使用 provider 进行 store 的提供就行了,在 ssr 中由于有服务端的预渲染,情况会比较不一样,结合 next-redux-wrapper 说明一下自己理解的渲染流程 4 | 5 | redux 的服务端渲染分为两个部分,服务器初始化阶段 -> 本地浏览器接管阶段 6 | 7 | 1. 服务器对于每一个新的请求都会生成使用初始化方法生成一个新的 store,如果在服务端使用了 dispatch 方法,则会首先初始化一个新的 store,再执行这个 dispatch 的 action 操作,最后会触发本地的 HYDRATE 方法,与本地的 store 进行合并,生成本次新的 state 8 | 2. 如果只是在浏览器端执行的 dispatch 方法,则不会对服务器端造成任何影响 9 | 10 | ### 设计 11 | 12 | 1. 使用服务器端与客户端分离的 state 结构,每次进行 HYDRATE 时会将服务器端的最新数据同步到 client 端的 state 中 13 | 14 | 2. 服务器端的初始化使用之前为每一个用户绑定的session中获取,实现每一个用户当前状态的保存与恢复 15 | 16 | 3. 使用特定接口向服务器端传递当前需要保存的状态,用户相关的存储在数据库中 17 | -------------------------------------------------------------------------------- /md/4.ts书写格式.md: -------------------------------------------------------------------------------- 1 | ### 书写格式规范 2 | 3 | ##### 主题文件格式分为三个部分 4 | 1. import 内容导入 5 | 2. interface declare 类型声明 6 | 3. 具体逻辑实现 7 | 8 | 文档注释 卸载逻辑实现的函数上 9 | 10 | 好处:可以从最上方看到所有函数的参数与返回值,具体的逻辑注释则可以从下方的详细实现中找寻 11 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | images: { 3 | domains: ["cn.bing.com", "h2.ioliu.cn", "github.com", "assets.leetcode.com"], 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["server"], 3 | "exec": "cross-env NODE_ENV=development ts-node --compiler ttypescript --project tsconfig.server.json ./server/index.ts", 4 | "ext": "js ts tsx" 5 | } 6 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { animateFadeIn, getClass } from "utils/dom"; 3 | import { MyNextComponent } from "./_app"; 4 | 5 | const NotFound: MyNextComponent = () => { 6 | return ( 7 |
    8 |
    9 |

    访问错误!

    10 |

    访问资源不存在!

    11 |
    12 |

    可能过期,删除,或者移动了⛆

    13 | 14 | 15 | 返回首页 16 | 17 | 18 |
    19 |
    20 | ); 21 | }; 22 | 23 | NotFound.title = "未知页面"; 24 | 25 | export default NotFound; 26 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from "react"; 2 | import { NextComponentType } from "next"; 3 | import { AppProps } from "next/app"; 4 | import { wrapper } from "store"; 5 | import { Layout } from "components/Layout"; 6 | 7 | import 'animate.css'; 8 | 9 | import '@splidejs/react-splide/css'; 10 | 11 | import "../styles/globals.css"; 12 | 13 | type MyNextComponent = NextComponentType<{}, {}, T> & { container?: boolean; title?: string; routerIn?: string; routerOut?: string }; 14 | 15 | interface MyAppProps extends AppProps { 16 | Component: MyNextComponent; 17 | } 18 | 19 | const WrappedApp: FC = ({ Component, pageProps }) => { 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export type { MyNextComponent }; 28 | 29 | export default wrapper.withRedux(WrappedApp); 30 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | {/* 字体图标 */} 8 | 9 | {/* bootstrap */} 10 | {/* */} 11 | 17 | {/* animate css */} 18 | {/* */} 19 | {/* */} 20 | {/* */} 21 | 22 | 23 |
    24 | 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import { About as AboutContainer } from "containers/About"; 2 | import { animateFadeIn, getClass } from "utils/dom"; 3 | import { MyNextComponent } from "./_app"; 4 | 5 | const About: MyNextComponent = () => { 6 | return
    ; 7 | }; 8 | 9 | About.title = "关于我"; 10 | 11 | export default About; 12 | -------------------------------------------------------------------------------- /pages/archive.tsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "store"; 2 | import { END } from "redux-saga"; 3 | import groupBy from "lodash/groupBy"; 4 | import { Archive as ArchiveContent } from "containers/Archive"; 5 | import { MyNextComponent } from "./_app"; 6 | import { apiName } from "config/api"; 7 | import { actionName } from "config/action"; 8 | import { autoDispatchTokenHandler } from "config/ssr"; 9 | import { animateFadeIn, getClass } from "utils/dom"; 10 | import { setDataSuccess_client } from "store/reducer/client/share/action"; 11 | import { getDataAction_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 12 | 13 | const Archive: MyNextComponent = () => { 14 | return ( 15 |
    16 | 17 |
    18 | ); 19 | }; 20 | 21 | Archive.title = "归档"; 22 | 23 | export default Archive; 24 | 25 | export const getServerSideProps = wrapper.getServerSideProps( 26 | autoDispatchTokenHandler(async ({ store }) => { 27 | // action 28 | store.dispatch(getDataAction_Server({ name: apiName.home })); 29 | // end the saga 30 | store.dispatch(END); 31 | // wait saga end 32 | await store.sagaTask?.toPromise(); 33 | // add home state 34 | store.dispatch(getDataSuccess_Server({ name: apiName.home, data: store.getState().server[apiName.home]["data"] })); 35 | // 当前页面需要的数据{'2020': [...], '2021': [....]} 36 | const blogs = store.getState().server[apiName.home]["data"]; 37 | const groupBlogs = groupBy(blogs, "blogCreateYear"); 38 | store.dispatch(setDataSuccess_client({ name: actionName.currentArchive, data: groupBlogs })); 39 | }) 40 | ); 41 | -------------------------------------------------------------------------------- /pages/editor/[id].tsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "store"; 2 | import { END } from "redux-saga"; 3 | import { apiName } from "config/api"; 4 | import { actionName } from "config/action"; 5 | import { autoDispatchTokenHandler } from "config/ssr"; 6 | import { animateFadeIn, getClass } from "utils/dom"; 7 | import { Editor } from "containers/Editor"; 8 | import { setDataSuccess_client } from "store/reducer/client/share/action"; 9 | import { getDataAction_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 10 | import { MyNextComponent } from "pages/_app"; 11 | import { BlogProps, ClientTagProps, TypeProps, UserProps } from "types"; 12 | 13 | const EditorContent: MyNextComponent<{ blogContent: BlogProps & TypeProps & ClientTagProps & UserProps }> = ({ blogContent }) => { 14 | return ( 15 |
    16 | 17 |
    18 | ); 19 | }; 20 | 21 | EditorContent.title = "编辑"; 22 | 23 | export const getServerSideProps = wrapper.getServerSideProps( 24 | autoDispatchTokenHandler(async ({ store, ...etc }) => { 25 | // 获取当前需要加载的博客详细信息 26 | const { 27 | params: { id }, 28 | } = etc; 29 | store.dispatch(setDataSuccess_client({ name: actionName.currentBlogId, data: id })); 30 | store.dispatch(getDataAction_Server({ name: apiName.blog })); 31 | // end the saga 32 | store.dispatch(END); 33 | // wait saga end 34 | await store.sagaTask?.toPromise(); 35 | store.dispatch(getDataSuccess_Server({ name: apiName.blog, data: { [id]: store.getState().server[apiName.blog]["data"][id] } })); 36 | return { props: { blogContent: store.getState().server[apiName.blog]["data"][id] } }; 37 | }) 38 | ); 39 | 40 | export default EditorContent; 41 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "store"; 2 | import { END } from "redux-saga"; 3 | import { Main } from "containers/Main"; 4 | import { MyNextComponent } from "./_app"; 5 | import { apiName } from "config/api"; 6 | import { autoDispatchTokenHandler } from "config/ssr"; 7 | import { animateFadeIn, getClass } from "utils/dom"; 8 | import { getDataAction_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 9 | 10 | const Home: MyNextComponent = () => { 11 | return ( 12 |
    13 |
    14 |
    15 | ); 16 | }; 17 | 18 | Home.title = "首页"; 19 | 20 | export default Home; 21 | 22 | // 加载home页面数据 23 | export const getServerSideProps = wrapper.getServerSideProps( 24 | autoDispatchTokenHandler(async ({ store }) => { 25 | // action 26 | store.dispatch(getDataAction_Server({ name: apiName.home })); 27 | // end the saga 28 | store.dispatch(END); 29 | // wait saga end 30 | await store.sagaTask?.toPromise(); 31 | // dispatch action to change state 32 | store.dispatch(getDataSuccess_Server({ name: apiName.home, data: store.getState().server[apiName.home]["data"] })); 33 | }) 34 | ); 35 | -------------------------------------------------------------------------------- /pages/login.tsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "store"; 2 | import { MyNextComponent } from "./_app"; 3 | import { Login as LoginContent } from "containers/Login"; 4 | import { ReLogin as ReLoginContent } from "containers/ReLogin"; 5 | import { autoDispatchTokenHandler } from "config/ssr"; 6 | import { animateFadeIn, flexCenter, getClass } from "utils/dom"; 7 | 8 | const Login: MyNextComponent<{ isLogin: boolean }> = ({ isLogin }) => { 9 | return
    {isLogin ? : }
    ; 10 | }; 11 | 12 | Login.container = false; 13 | 14 | Login.title = "登录"; 15 | 16 | // 判断是否已经登录 17 | export const getServerSideProps = wrapper.getServerSideProps( 18 | autoDispatchTokenHandler(async ({ req }) => ({ 19 | props: { 20 | isLogin: req.session["userCache"], 21 | }, 22 | })) 23 | ); 24 | 25 | export default Login; 26 | -------------------------------------------------------------------------------- /pages/manage.tsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "store"; 2 | import { MyNextComponent } from "./_app"; 3 | import { NotLogin } from "containers/NotLogin"; 4 | import { Manage as ManageContent } from "containers/Manage"; 5 | import { autoDispatchTokenHandler } from "config/ssr"; 6 | import { animateFadeIn, getClass } from "utils/dom"; 7 | 8 | const Manage: MyNextComponent<{ isLogin: boolean; userId: string }> = ({ isLogin, userId }) => { 9 | return
    {isLogin ? : }
    ; 10 | }; 11 | 12 | Manage.title = "管理"; 13 | 14 | // 判断是否已经登录 15 | export const getServerSideProps = wrapper.getServerSideProps( 16 | autoDispatchTokenHandler(async ({ req }) => ({ 17 | props: { 18 | isLogin: !!req.session["userCache"], 19 | userId: req.session["userCache"]?.userId || null, 20 | }, 21 | })) 22 | ); 23 | 24 | export default Manage; 25 | -------------------------------------------------------------------------------- /pages/publish.tsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "store"; 2 | import { NotLogin } from "containers/NotLogin"; 3 | import { Publish as PublishContent } from "containers/Publish"; 4 | import { animateFadeIn, getClass } from "utils/dom"; 5 | import { autoDispatchTokenHandler } from "config/ssr"; 6 | import { MyNextComponent } from "./_app"; 7 | 8 | const Publish: MyNextComponent<{ isLogin: boolean }> = ({ isLogin }) => { 9 | return
    {isLogin ? : }
    ; 10 | }; 11 | 12 | Publish.title = "发布"; 13 | 14 | // 判断是否已经登录 15 | export const getServerSideProps = wrapper.getServerSideProps( 16 | autoDispatchTokenHandler(async ({ req }) => ({ 17 | props: { 18 | isLogin: !!req.session["userCache"], 19 | }, 20 | })) 21 | ); 22 | 23 | export default Publish; 24 | -------------------------------------------------------------------------------- /pages/tag.tsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "store"; 2 | import { END } from "redux-saga"; 3 | import { Tag as TagContent } from "containers/Tag"; 4 | import { MyNextComponent } from "./_app"; 5 | import { apiName } from "config/api"; 6 | import { autoDispatchTokenHandler } from "config/ssr"; 7 | import { animateFadeIn, getClass } from "utils/dom"; 8 | import { getDataAction_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 9 | 10 | const Tag: MyNextComponent = () => { 11 | return ( 12 |
    13 | 14 |
    15 | ); 16 | }; 17 | 18 | Tag.title = "标签"; 19 | 20 | export default Tag; 21 | 22 | // 加载tag页面数据 23 | export const getServerSideProps = wrapper.getServerSideProps( 24 | autoDispatchTokenHandler(async ({ store }) => { 25 | // action 26 | store.dispatch(getDataAction_Server({ name: apiName.tag })); 27 | // end the saga 28 | store.dispatch(END); 29 | // wait saga end 30 | await store.sagaTask?.toPromise(); 31 | // dispatch action to change tag state 32 | store.dispatch(getDataSuccess_Server({ name: apiName.tag, data: store.getState().server[apiName.tag]["data"] })); 33 | }) 34 | ); 35 | -------------------------------------------------------------------------------- /pages/type.tsx: -------------------------------------------------------------------------------- 1 | import { wrapper } from "store"; 2 | import { END } from "redux-saga"; 3 | import { Type as TypeContent } from "containers/Type"; 4 | import { MyNextComponent } from "./_app"; 5 | import { apiName } from "config/api"; 6 | import { autoDispatchTokenHandler } from "config/ssr"; 7 | import { animateFadeIn, getClass } from "utils/dom"; 8 | import { getDataAction_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 9 | 10 | const Type: MyNextComponent = () => { 11 | return ( 12 |
    13 | 14 |
    15 | ); 16 | }; 17 | 18 | Type.title = "分类"; 19 | 20 | export default Type; 21 | 22 | // 加载type页面数据 23 | export const getServerSideProps = wrapper.getServerSideProps( 24 | autoDispatchTokenHandler(async ({ store }) => { 25 | // action 26 | store.dispatch(getDataAction_Server({ name: apiName.type })); 27 | // end the saga 28 | store.dispatch(END); 29 | // wait saga end 30 | await store.sagaTask?.toPromise(); 31 | // dispatch action to change type state 32 | store.dispatch(getDataSuccess_Server({ name: apiName.type, data: store.getState().server[apiName.type]["data"] })); 33 | }) 34 | ); 35 | -------------------------------------------------------------------------------- /public/avatar/about.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/avatar/about.jpg -------------------------------------------------------------------------------- /public/avatar/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/avatar/admin.png -------------------------------------------------------------------------------- /public/avatar/aliPay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/avatar/aliPay.jpg -------------------------------------------------------------------------------- /public/avatar/man.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/avatar/man.jpg -------------------------------------------------------------------------------- /public/avatar/weichatPay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/avatar/weichatPay.png -------------------------------------------------------------------------------- /public/avatar/woman.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/avatar/woman.jpeg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/favicon.ico -------------------------------------------------------------------------------- /public/font/blog.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/font/blog.ttf -------------------------------------------------------------------------------- /public/font/测试1.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/public/font/测试1.ttf -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /server/api/3rd/3rd.ts: -------------------------------------------------------------------------------- 1 | import { Method } from "axios"; 2 | import { createRequest } from "utils/fetcher"; 3 | import { fail, success, wrapperMiddlewareRequest } from "server/middleware/apiHandler"; 4 | 5 | const get3rdRequestAction = wrapperMiddlewareRequest({ 6 | requestHandler: async function thirdRequest({ req, res }) { 7 | const { path, ...lastQuery } = req.query; 8 | const { data } = req.body; 9 | const { origin, referer, cookie, ...lastHeader } = req.headers; 10 | const request = createRequest({ 11 | path: path as string, 12 | method: req.method as Method, 13 | data, 14 | query: lastQuery as { [p: string]: string }, 15 | header: lastHeader, 16 | }); 17 | await request 18 | .run() 19 | .then((data) => success({ res, resDate: { data } })) 20 | .catch((e) => fail({ res, resDate: { data: e.toString() } })); 21 | }, 22 | cacheConfig: { needCache: true, cacheKey: ({ req }) => req.method + ":" + req.query.path }, 23 | paramsConfig: { fromQuery: ["path"] }, 24 | }); 25 | 26 | export { get3rdRequestAction }; 27 | -------------------------------------------------------------------------------- /server/api/3rd/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { get3rdRequestAction } from "./3rd"; 3 | 4 | const thirdRequestHandler = { 5 | [apiName.thirdPath]: get3rdRequestAction, 6 | }; 7 | 8 | export { thirdRequestHandler }; 9 | -------------------------------------------------------------------------------- /server/api/blog/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { 3 | getBlogByBlogIdAction, 4 | deleteBlogByBlogIdAAction, 5 | publishBlogAction, 6 | updateBlogByBlogIdAction, 7 | updateBlogReadAction, 8 | updateBlogByBlogIdAction_v2, 9 | } from "./blog"; 10 | 11 | const blogHandler = { 12 | [apiName.blog]: getBlogByBlogIdAction, 13 | [apiName.publishBlog]: publishBlogAction, 14 | [apiName.updateBlog]: updateBlogByBlogIdAction, 15 | [apiName.deleteBlog]: deleteBlogByBlogIdAAction, 16 | [apiName.addBlogRead]: updateBlogReadAction, 17 | [apiName.updateBlog_v2]: updateBlogByBlogIdAction_v2, 18 | }; 19 | 20 | export { blogHandler }; 21 | -------------------------------------------------------------------------------- /server/api/home/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { getBlogsByParams, getHomeAction, getUserHomeAction } from "./home"; 3 | 4 | const homeHandler = { 5 | [apiName.home]: getHomeAction, 6 | [apiName.search]: getBlogsByParams, 7 | [apiName.userHome]: getUserHomeAction, 8 | }; 9 | 10 | export { homeHandler }; 11 | -------------------------------------------------------------------------------- /server/api/image/captcha.ts: -------------------------------------------------------------------------------- 1 | import svgCaptcha from "svg-captcha"; 2 | import { catchMiddlewareHandler, compose, defaultRunRequestMiddleware, success, wrapperMiddlewareRequest } from "server/middleware/apiHandler"; 3 | 4 | // 获取验证码图片 5 | const getCaptchaAction = wrapperMiddlewareRequest( 6 | { 7 | requestHandler: async function getCaptchaAction({ req, res }) { 8 | const captcha = svgCaptcha.create({ 9 | noise: 4, 10 | background: "#ffffff", 11 | }); 12 | req.session.captcha = captcha.text; 13 | res.type("svg"); 14 | res.send(captcha.data); 15 | }, 16 | }, 17 | compose(catchMiddlewareHandler, defaultRunRequestMiddleware) 18 | ); 19 | 20 | // 获取验证码明文 21 | const getCaptchaStrAction = wrapperMiddlewareRequest( 22 | { 23 | requestHandler: function getCaptchaStrAction({ req, res }) { 24 | success({ res, resDate: { data: req.session.captcha } }); 25 | }, 26 | }, 27 | compose(catchMiddlewareHandler, defaultRunRequestMiddleware) 28 | ); 29 | 30 | export { getCaptchaAction, getCaptchaStrAction }; 31 | -------------------------------------------------------------------------------- /server/api/image/image.ts: -------------------------------------------------------------------------------- 1 | import { getRandom } from "utils/data"; 2 | import { createRequest } from "utils/fetcher"; 3 | import { catchMiddlewareHandler, compose, defaultRunRequestMiddleware, success, wrapperMiddlewareRequest } from "server/middleware/apiHandler"; 4 | 5 | // 获取图片请求链接 6 | const getImagePath = () => { 7 | const idx = String(getRandom(7)); 8 | const n = String(getRandom(1, 7)); 9 | let requestUrl = String(process.env.BING_API); 10 | requestUrl = requestUrl.replace("--n--", n); 11 | requestUrl = requestUrl.replace("--idx--", idx); 12 | return requestUrl; 13 | }; 14 | 15 | // 获取所有随机图片信息 16 | const getImagesAction = wrapperMiddlewareRequest( 17 | { 18 | requestHandler: async function getImagesAction({ res }) { 19 | let { images } = await createRequest({ path: getImagePath() }).run(); 20 | images = images.map((item: { [props: string]: string }) => { 21 | return { ...item, relativeUrl: `${process.env.BING_URL}${item.url}` }; 22 | }); 23 | success({ res, resDate: { data: images } }); 24 | }, 25 | }, 26 | compose(catchMiddlewareHandler, defaultRunRequestMiddleware) 27 | ); 28 | 29 | // 获取随机图片信息 30 | const getRandomImageAction = wrapperMiddlewareRequest( 31 | { 32 | requestHandler: async function getRandomImageAction({ res }) { 33 | const { images } = await createRequest({ path: getImagePath() }).run(); 34 | const [{ relativeUrl }] = images.map((item: { [props: string]: string }) => ({ relativeUrl: `${process.env.BING_URL}${item.url}` })); 35 | success({ res, resDate: { data: relativeUrl } }); 36 | }, 37 | }, 38 | compose(catchMiddlewareHandler, defaultRunRequestMiddleware) 39 | ); 40 | 41 | export { getImagesAction, getRandomImageAction }; 42 | -------------------------------------------------------------------------------- /server/api/image/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { getRandomImageAction, getImagesAction } from "./image"; 3 | import { getCaptchaAction, getCaptchaStrAction } from "./captcha"; 4 | 5 | const imageHandler = { 6 | [apiName.captcha]: getCaptchaAction, 7 | [apiName.image]: getRandomImageAction, 8 | [apiName.allImage]: getImagesAction, 9 | [apiName.captchaStr]: getCaptchaStrAction, 10 | }; 11 | 12 | export { imageHandler }; 13 | -------------------------------------------------------------------------------- /server/api/index.ts: -------------------------------------------------------------------------------- 1 | import { imageHandler } from "./image"; 2 | import { homeHandler } from "./home"; 3 | import { tagHandler } from "./tag"; 4 | import { typeHandler } from "./type"; 5 | import { userHandler } from "./user"; 6 | import { messageHandler } from "./message"; 7 | import { blogHandler } from "./blog"; 8 | import { thirdRequestHandler } from "./3rd"; 9 | import { testHandler } from "./test"; 10 | import { fail } from "server/middleware/apiHandler"; 11 | import { Request, Response } from "express"; 12 | 13 | const allHandler: { [props: string]: Function } = { 14 | ...imageHandler, 15 | ...homeHandler, 16 | ...tagHandler, 17 | ...typeHandler, 18 | ...userHandler, 19 | ...blogHandler, 20 | ...messageHandler, 21 | ...thirdRequestHandler, 22 | ...testHandler, 23 | }; 24 | 25 | const apiHandler = async (req: Request, res: Response) => { 26 | let action = allHandler[req.path.slice(1)]; 27 | if (action) { 28 | await action(req, res); 29 | } else { 30 | // 其他api访问转向next api router 31 | // next(); 32 | fail({ res, resDate: { state: "api路径不正确", data: `请求: ${req.path}` } }); 33 | } 34 | }; 35 | 36 | export { apiHandler }; 37 | -------------------------------------------------------------------------------- /server/api/message/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { 3 | deleteChildMessageByCommentIdAction, 4 | getChildMessageByPrimaryIdAction, 5 | publishChildMessageByPrimaryIdAction, 6 | updateChildMessageByCommentIdAction, 7 | } from "./childMessage"; 8 | import { 9 | deletePrimaryMessageByCommentIdAction, 10 | getPrimaryMessageByBlogIdAction, 11 | publishPrimaryMessageByBlogIdAction, 12 | updatePrimaryMessageByCommentIdAction, 13 | } from "./primaryMessage"; 14 | 15 | const messageHandler = { 16 | [apiName.childMessage]: getChildMessageByPrimaryIdAction, 17 | [apiName.primaryMessage]: getPrimaryMessageByBlogIdAction, 18 | [apiName.putChildMessage]: publishChildMessageByPrimaryIdAction, 19 | [apiName.putPrimaryMessage]: publishPrimaryMessageByBlogIdAction, 20 | [apiName.deleteChildMessage]: deleteChildMessageByCommentIdAction, 21 | [apiName.deletePrimaryMessage]: deletePrimaryMessageByCommentIdAction, 22 | [apiName.updateChildMessage]: updateChildMessageByCommentIdAction, 23 | [apiName.updatePrimaryMessage]: updatePrimaryMessageByCommentIdAction, 24 | }; 25 | 26 | export { messageHandler }; 27 | -------------------------------------------------------------------------------- /server/api/tag/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { addTagAction, checkTagAction, deleteTagAction, getTagAction } from "./tag"; 3 | 4 | const tagHandler = { 5 | [apiName.tag]: getTagAction, 6 | [apiName.addTag]: addTagAction, 7 | [apiName.checkTag]: checkTagAction, 8 | [apiName.deleteTag]: deleteTagAction, 9 | }; 10 | 11 | export { tagHandler }; 12 | -------------------------------------------------------------------------------- /server/api/test/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { 3 | test_author, 4 | test_childComment, 5 | test_insertTag, 6 | test_insertType, 7 | test_insertUserEx, 8 | test_primaryComment, 9 | test_publishBlog, 10 | test_publishHome, 11 | test_registerUser, 12 | } from "./test"; 13 | 14 | const testHandler = { 15 | [apiName.testBlog]: test_publishBlog, 16 | [apiName.testChild]: test_childComment, 17 | [apiName.testHome]: test_publishHome, 18 | [apiName.testPrimary]: test_primaryComment, 19 | [apiName.testTag]: test_insertTag, 20 | [apiName.testType]: test_insertType, 21 | [apiName.testUser]: test_registerUser, 22 | [apiName.testUserEx]: test_insertUserEx, 23 | [apiName.testAuthor]: test_author, 24 | }; 25 | 26 | export { testHandler }; 27 | -------------------------------------------------------------------------------- /server/api/type/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { addTypeAction, checkTypeAction, deleteTypeAction, getTypeAction } from "./type"; 3 | 4 | const typeHandler = { 5 | [apiName.type]: getTypeAction, 6 | [apiName.addType]: addTypeAction, 7 | [apiName.checkType]: checkTypeAction, 8 | [apiName.deleteType]: deleteTypeAction, 9 | }; 10 | 11 | export { typeHandler }; 12 | -------------------------------------------------------------------------------- /server/api/user/index.ts: -------------------------------------------------------------------------------- 1 | // 用户信息的操作 2 | import { apiName } from "config/api"; 3 | import { 4 | autoLoginAction, 5 | loginAction, 6 | logoutAction, 7 | registerAction, 8 | getUserExByUserIdAction, 9 | getUserByUserIdAction, 10 | getUserByUserNameAction, 11 | getAuthorByUserIdAction, 12 | } from "./user"; 13 | 14 | const userHandler = { 15 | [apiName.login]: loginAction, 16 | [apiName.logout]: logoutAction, 17 | [apiName.register]: registerAction, 18 | [apiName.autoLogin]: autoLoginAction, 19 | [apiName.user]: getUserByUserIdAction, 20 | [apiName.userName]: getUserByUserNameAction, 21 | [apiName.userEx]: getUserExByUserIdAction, 22 | [apiName.author]: getAuthorByUserIdAction, 23 | }; 24 | 25 | export { userHandler }; 26 | -------------------------------------------------------------------------------- /server/database/server: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrWangJustToDo/ts-next-blog/10cceff01e8575f1ae1c98e1dc95a7fd5f4cbea1/server/database/server -------------------------------------------------------------------------------- /server/index.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import next from "next"; 3 | import express from "express"; 4 | import { log } from "utils/log"; 5 | import { setUp } from "./setup"; 6 | import { init } from "./init"; 7 | import { apiHandler } from "./api"; 8 | import { detectionToken, generateToken } from "./check"; 9 | 10 | require("dotenv").config(); 11 | 12 | const port = process.env.PORT; 13 | 14 | const nextApp = next({ dev: process.env.NODE_ENV !== "production" }); 15 | 16 | const handle = nextApp.getRequestHandler(); 17 | 18 | const app = express(); 19 | 20 | setUp(app); 21 | 22 | init(app); 23 | 24 | app.use("/api", detectionToken); 25 | 26 | app.use(generateToken); 27 | 28 | app.use("/api", apiHandler); 29 | 30 | nextApp.prepare().then(() => { 31 | app.all("*", (req, res) => { 32 | return handle(req, res); 33 | }); 34 | http.createServer(app).listen(port, () => log(`=== app run on: http://localhost:${port} ===`, "warn")); 35 | }); 36 | -------------------------------------------------------------------------------- /server/init/index.ts: -------------------------------------------------------------------------------- 1 | import type { Express } from "express"; 2 | import { decodeURI, initDBConnect, initSession, initUser } from "./init"; 3 | 4 | const init = (expressApp: Express) => { 5 | expressApp.use(decodeURI); 6 | expressApp.use(initDBConnect); 7 | expressApp.use(initSession); 8 | expressApp.use(initUser); 9 | }; 10 | 11 | export { init }; 12 | -------------------------------------------------------------------------------- /server/setup/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Express } from "express"; 2 | import cors from "cors"; 3 | import session from "express-session"; 4 | import cookieParser from "cookie-parser"; 5 | 6 | const setUp = (expressApp: Express) => { 7 | expressApp.use( 8 | express.static(`${process.cwd()}/public`, { 9 | maxAge: 365 * 24 * 3600000, 10 | }) 11 | ); 12 | 13 | expressApp.use(express.json({ limit: "5mb" })); 14 | 15 | expressApp.use(express.urlencoded({ extended: true })); 16 | 17 | expressApp.use(cookieParser(process.env.COOKIE_PARSER)); 18 | 19 | expressApp.use( 20 | cors({ 21 | maxAge: 86400, 22 | origin: "*", 23 | }) 24 | ); 25 | expressApp.use( 26 | session({ 27 | secret: "keyboard cat", 28 | resave: true, 29 | rolling: true, 30 | saveUninitialized: true, 31 | cookie: { maxAge: 600000 }, 32 | name: "react-next-blog", 33 | }) 34 | ); 35 | }; 36 | 37 | export { setUp }; 38 | -------------------------------------------------------------------------------- /server/utils/error.ts: -------------------------------------------------------------------------------- 1 | class ServerError extends Error { 2 | constructor(message: string, readonly code: number) { 3 | super(message); 4 | } 5 | } 6 | 7 | export { ServerError }; 8 | -------------------------------------------------------------------------------- /server/utils/merge.ts: -------------------------------------------------------------------------------- 1 | import mergeWith from "lodash/mergeWith"; 2 | import cloneDeep from "lodash/cloneDeep"; 3 | import { BlogProps, ClientTagProps, HomeBlogProps, ServerTagProps, TypeProps, UserProps } from "types"; 4 | 5 | export function mergeTypeTagToBlog( 6 | blog: HomeBlogProps & UserProps, 7 | type: TypeProps[], 8 | tag: ServerTagProps[] 9 | ): HomeBlogProps & UserProps & TypeProps & ClientTagProps; 10 | 11 | export function mergeTypeTagToBlog(blog: BlogProps & UserProps, type: TypeProps[], tag: ServerTagProps[]): BlogProps & UserProps & TypeProps & ClientTagProps; 12 | 13 | export function mergeTypeTagToBlog(blog: (BlogProps & UserProps) | (HomeBlogProps & UserProps), type: TypeProps[], tag: ServerTagProps[]) { 14 | const currentTagArr = cloneDeep(tag); 15 | const currentTypeArr = cloneDeep(type); 16 | const currentType = currentTypeArr.find((it) => blog.typeId === it.typeId); 17 | const currentTagIds = blog.tagId.split(","); 18 | const currentTagArray = currentTagArr.filter((it) => currentTagIds.includes(it.tagId)); 19 | let currentTag; 20 | if (currentTagArray.length > 1) { 21 | const head = currentTagArray.pop(); 22 | currentTag = mergeWith(head, ...currentTagArray, (res: ServerTagProps[], srcValue: ServerTagProps) => { 23 | if (!Array.isArray(res)) { 24 | res = [res]; 25 | } 26 | return res.concat(srcValue); 27 | }); 28 | } else if (currentTagArray.length === 1) { 29 | const head = currentTagArray[0] as any; 30 | currentTag = Object.keys(head).reduce((p, c) => { 31 | p[c] = [head[c]]; 32 | return p; 33 | }, {}) as unknown as ClientTagProps; 34 | } 35 | return { ...blog, ...currentType, ...currentTag }; 36 | } -------------------------------------------------------------------------------- /store/index.ts: -------------------------------------------------------------------------------- 1 | import createSagaMiddleware from "redux-saga"; 2 | import { legacy_createStore as createStore, applyMiddleware, compose } from "redux"; 3 | import { MakeStore, createWrapper } from "next-redux-wrapper"; 4 | import { rootReducer } from "./reducer"; 5 | import { rootSaga } from "./saga"; 6 | import type { SagaStore } from "./type"; 7 | import { SagaManager } from "./saga/util"; 8 | 9 | export const makeStore: MakeStore = () => { 10 | const devtools = 11 | typeof window !== "undefined" && 12 | typeof window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ === "function" && 13 | window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ actionsDenylist: [] }); 14 | 15 | const composeEnhancers = devtools || compose; 16 | 17 | // 1: Create the middleware 18 | const sagaMiddleware = createSagaMiddleware(); 19 | 20 | // 2: Add an extra parameter for applying middleware: 21 | const store = createStore(rootReducer, composeEnhancers(applyMiddleware(sagaMiddleware))) as SagaStore; 22 | 23 | // 3: Run your sagas on server 24 | store.sagaTask = SagaManager.startSagas(rootSaga, sagaMiddleware); 25 | 26 | // 4: now return the store: 27 | return store; 28 | }; 29 | 30 | export const wrapper = createWrapper(makeStore, { debug: false }); 31 | -------------------------------------------------------------------------------- /store/reducer/client/action/currentHeader.ts: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | import { Reducer } from "redux"; 3 | import { actionName } from "config/action"; 4 | import { clientAction } from "../share/action"; 5 | import { ReducerState, ReducerStateAction, ReducerStateActionMapType } from "store/reducer/type"; 6 | 7 | type CurrentState = ReducerState; 8 | 9 | const initState: CurrentState = { data: "/", error: null, loaded: false, loading: false }; 10 | 11 | const headerReducer: Reducer = (state: CurrentState = initState, action: ReducerStateAction) => { 12 | let actionReducer = actionReducerMap[action.type]; 13 | if (actionReducer) { 14 | return actionReducer(state, action); 15 | } else { 16 | return state; 17 | } 18 | }; 19 | 20 | const actionReducerMap: ReducerStateActionMapType = { 21 | [clientAction.SET_DATA_LOADING(actionName.currentHeader)]: (state, action) => 22 | produce(state, (proxy) => { 23 | proxy.data = ""; 24 | proxy.error = null; 25 | proxy.loading = action.loadingState || true; 26 | proxy.loaded = false; 27 | }), 28 | [clientAction.SET_DATA_SUCCESS(actionName.currentHeader)]: (state, action) => 29 | produce(state, (proxy) => { 30 | proxy.data = action.data || ""; 31 | proxy.error = null; 32 | proxy.loading = false; 33 | proxy.loaded = true; 34 | }), 35 | [clientAction.SET_DATA_FAIL(actionName.currentHeader)]: (state, action) => 36 | produce(state, (proxy) => { 37 | proxy.data = ""; 38 | proxy.error = action.error; 39 | proxy.loading = false; 40 | proxy.loaded = true; 41 | }), 42 | }; 43 | 44 | export { headerReducer }; 45 | -------------------------------------------------------------------------------- /store/reducer/client/action/currentHomePage.ts: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | import { Reducer } from "redux"; 3 | import { actionName } from "config/action"; 4 | import { ReducerState, ReducerStateAction, ReducerStateActionMapType } from "store/reducer/type"; 5 | import { clientAction } from "../share/action"; 6 | 7 | type CurrentState = ReducerState; 8 | 9 | const initState: CurrentState = { data: 1, error: null, loading: false, loaded: false }; 10 | 11 | const homePageReducer: Reducer = (state: CurrentState = initState, action: ReducerStateAction) => { 12 | let actionReducer = actionReducerMap[action.type]; 13 | if (actionReducer) { 14 | return actionReducer(state, action); 15 | } else { 16 | return state; 17 | } 18 | }; 19 | 20 | const actionReducerMap: ReducerStateActionMapType = { 21 | [clientAction.SET_DATA_LOADING(actionName.currentHomePage)]: (state, action) => 22 | produce(state, (proxy) => { 23 | proxy.data = 1; 24 | proxy.error = null; 25 | proxy.loading = action.loadingState || true; 26 | proxy.loaded = false; 27 | }), 28 | [clientAction.SET_DATA_SUCCESS(actionName.currentHomePage)]: (state, action) => 29 | produce(state, (proxy) => { 30 | proxy.data = action.data || 1; 31 | proxy.error = null; 32 | proxy.loading = false; 33 | proxy.loaded = true; 34 | }), 35 | [clientAction.SET_DATA_FAIL(actionName.currentHomePage)]: (state, action) => 36 | produce(state, (proxy) => { 37 | proxy.data = 1; 38 | proxy.error = action.error; 39 | proxy.loading = false; 40 | proxy.loaded = true; 41 | }), 42 | }; 43 | 44 | export { homePageReducer }; 45 | -------------------------------------------------------------------------------- /store/reducer/client/action/currentIp.ts: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | import { Reducer } from "redux"; 3 | import { actionName } from "config/action"; 4 | import { clientAction } from "../share/action"; 5 | import type { IpAddressProps } from "types"; 6 | import type { ReducerState, ReducerStateAction, ReducerStateActionMapType } from "store/reducer/type"; 7 | 8 | type CurrentState = ReducerState; 9 | 10 | const initState: CurrentState = { data: {}, error: null, loading: false, loaded: false }; 11 | 12 | const ipReducer: Reducer = (state: CurrentState = initState, action: ReducerStateAction) => { 13 | let actionReducer = actionReducerMap[action.type]; 14 | if (actionReducer) { 15 | return actionReducer(state, action); 16 | } else { 17 | return state; 18 | } 19 | }; 20 | 21 | const actionReducerMap: ReducerStateActionMapType = { 22 | [clientAction.SET_DATA_LOADING(actionName.currentIp)]: (state, action) => 23 | produce(state, (proxy) => { 24 | proxy.data = {}; 25 | proxy.error = null; 26 | proxy.loading = action.loadingState || true; 27 | proxy.loaded = false; 28 | }), 29 | [clientAction.SET_DATA_SUCCESS(actionName.currentIp)]: (state, action) => 30 | produce(state, (proxy) => { 31 | proxy.data = action.data || {}; 32 | proxy.error = null; 33 | proxy.loading = false; 34 | proxy.loaded = true; 35 | }), 36 | [clientAction.SET_DATA_FAIL(actionName.currentIp)]: (state, action) => 37 | produce(state, (proxy) => { 38 | proxy.data = {}; 39 | proxy.error = action.error; 40 | proxy.loading = false; 41 | proxy.loaded = true; 42 | }), 43 | }; 44 | 45 | export { ipReducer }; 46 | -------------------------------------------------------------------------------- /store/reducer/client/action/currentTag.ts: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | import { Reducer } from "redux"; 3 | import { actionName } from "config/action"; 4 | import { clientAction } from "../share/action"; 5 | import type { ReducerState, ReducerStateAction, ReducerStateActionMapType } from "store/reducer/type"; 6 | 7 | type CurrentState = ReducerState; 8 | 9 | const initState: CurrentState = { data: "", error: null, loaded: false, loading: false }; 10 | 11 | const tagReducer: Reducer = (state: CurrentState = initState, action: ReducerStateAction) => { 12 | let actionReducer = actionReducerMap[action.type]; 13 | if (actionReducer) { 14 | return actionReducer(state, action); 15 | } else { 16 | return state; 17 | } 18 | }; 19 | 20 | const actionReducerMap: ReducerStateActionMapType = { 21 | [clientAction.SET_DATA_LOADING(actionName.currentTag)]: (state, action) => 22 | produce(state, (proxy) => { 23 | proxy.data = ""; 24 | proxy.error = null; 25 | proxy.loading = action.loadingState || true; 26 | proxy.loaded = false; 27 | }), 28 | [clientAction.SET_DATA_SUCCESS(actionName.currentTag)]: (state, action) => 29 | produce(state, (proxy) => { 30 | proxy.data = action.data || ""; 31 | proxy.error = null; 32 | proxy.loading = false; 33 | proxy.loaded = true; 34 | }), 35 | [clientAction.SET_DATA_FAIL(actionName.currentTag)]: (state, action) => 36 | produce(state, (proxy) => { 37 | proxy.data = ""; 38 | proxy.error = action.error; 39 | proxy.loading = false; 40 | proxy.loaded = true; 41 | }), 42 | }; 43 | 44 | export { tagReducer }; 45 | -------------------------------------------------------------------------------- /store/reducer/client/action/currentTagPage.ts: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | import { Reducer } from "redux"; 3 | import { actionName } from "config/action"; 4 | import { ReducerState, ReducerStateAction, ReducerStateActionMapType } from "store/reducer/type"; 5 | import { clientAction } from "../share/action"; 6 | 7 | type CurrentState = ReducerState; 8 | 9 | const initState: CurrentState = { data: 1, error: null, loaded: false, loading: false }; 10 | 11 | const tagPageReducer: Reducer = (state: CurrentState = initState, action: ReducerStateAction) => { 12 | let actionReducer = actionReducerMap[action.type]; 13 | if (actionReducer) { 14 | return actionReducer(state, action); 15 | } else { 16 | return state; 17 | } 18 | }; 19 | 20 | const actionReducerMap: ReducerStateActionMapType = { 21 | [clientAction.SET_DATA_LOADING(actionName.currentTagPage)]: (state, action) => 22 | produce(state, (proxy) => { 23 | proxy.data = 1; 24 | proxy.error = null; 25 | proxy.loading = action.loadingState || true; 26 | proxy.loaded = false; 27 | }), 28 | [clientAction.SET_DATA_SUCCESS(actionName.currentTagPage)]: (state, action) => 29 | produce(state, (proxy) => { 30 | proxy.data = action.data || 1; 31 | proxy.error = null; 32 | proxy.loading = false; 33 | proxy.loaded = true; 34 | }), 35 | [clientAction.SET_DATA_FAIL(actionName.currentTagPage)]: (state, action) => 36 | produce(state, (proxy) => { 37 | proxy.data = 1; 38 | proxy.error = action.error; 39 | proxy.loading = false; 40 | proxy.loaded = true; 41 | }), 42 | }; 43 | 44 | export { tagPageReducer }; 45 | -------------------------------------------------------------------------------- /store/reducer/client/action/currentType.ts: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | import { Reducer } from "redux"; 3 | import { actionName } from "config/action"; 4 | import { ReducerState, ReducerStateAction, ReducerStateActionMapType } from "store/reducer/type"; 5 | import { clientAction } from "../share/action"; 6 | 7 | type CurrentState = ReducerState; 8 | 9 | const initState: CurrentState = { data: "", error: null, loaded: false, loading: false }; 10 | 11 | const typeReducer: Reducer = (state: CurrentState = initState, action: ReducerStateAction) => { 12 | let actionReducer = actionReducerMap[action.type]; 13 | if (actionReducer) { 14 | return actionReducer(state, action); 15 | } else { 16 | return state; 17 | } 18 | }; 19 | 20 | const actionReducerMap: ReducerStateActionMapType = { 21 | [clientAction.SET_DATA_LOADING(actionName.currentType)]: (state, action) => 22 | produce(state, (proxy) => { 23 | proxy.data = ""; 24 | proxy.error = null; 25 | proxy.loading = action.loadingState || true; 26 | proxy.loaded = false; 27 | }), 28 | [clientAction.SET_DATA_SUCCESS(actionName.currentType)]: (state, action) => 29 | produce(state, (proxy) => { 30 | proxy.data = action.data || ""; 31 | proxy.error = null; 32 | proxy.loading = false; 33 | proxy.loaded = true; 34 | }), 35 | [clientAction.SET_DATA_FAIL(actionName.currentType)]: (state, action) => 36 | produce(state, (proxy) => { 37 | proxy.data = ""; 38 | proxy.error = action.error; 39 | proxy.loading = false; 40 | proxy.loaded = true; 41 | }), 42 | }; 43 | 44 | export { typeReducer }; 45 | -------------------------------------------------------------------------------- /store/reducer/client/action/currentTypePage.ts: -------------------------------------------------------------------------------- 1 | import { produce } from "immer"; 2 | import { Reducer } from "redux"; 3 | import { actionName } from "config/action"; 4 | import { ReducerState, ReducerStateAction, ReducerStateActionMapType } from "store/reducer/type"; 5 | import { clientAction } from "../share/action"; 6 | 7 | type CurrentState = ReducerState; 8 | 9 | const initState: CurrentState = { data: 1, error: null, loaded: false, loading: false }; 10 | 11 | const typePageReducer: Reducer = (state: CurrentState = initState, action: ReducerStateAction) => { 12 | let actionReducer = actionReducerMap[action.type]; 13 | if (actionReducer) { 14 | return actionReducer(state, action); 15 | } else { 16 | return state; 17 | } 18 | }; 19 | 20 | const actionReducerMap: ReducerStateActionMapType = { 21 | [clientAction.SET_DATA_LOADING(actionName.currentTypePage)]: (state, action) => 22 | produce(state, (proxy) => { 23 | proxy.data = 1; 24 | proxy.error = null; 25 | proxy.loading = action.loadingState || true; 26 | proxy.loaded = false; 27 | }), 28 | [clientAction.SET_DATA_SUCCESS(actionName.currentTypePage)]: (state, action) => 29 | produce(state, (proxy) => { 30 | proxy.data = action.data || 1; 31 | proxy.error = null; 32 | proxy.loading = false; 33 | proxy.loaded = true; 34 | }), 35 | [clientAction.SET_DATA_FAIL(actionName.currentTypePage)]: (state, action) => 36 | produce(state, (proxy) => { 37 | proxy.data = 1; 38 | proxy.error = action.error; 39 | proxy.loading = false; 40 | proxy.loaded = true; 41 | }), 42 | }; 43 | 44 | export { typePageReducer }; 45 | -------------------------------------------------------------------------------- /store/reducer/client/action/index.ts: -------------------------------------------------------------------------------- 1 | export { archiveReducer } from "./currentArchive"; 2 | export { blogIdReducer } from "./currentBlogId"; 3 | export { headerReducer } from "./currentHeader"; 4 | export { homePageReducer } from "./currentHomePage"; 5 | export { ipReducer } from "./currentIp"; 6 | export { resultReducer } from "./currentResult"; 7 | export { tagReducer } from "./currentTag"; 8 | export { tagPageReducer } from "./currentTagPage"; 9 | export { tokenReducer } from "./currentToken"; 10 | export { typeReducer } from "./currentType"; 11 | export { typePageReducer } from "./currentTypePage"; 12 | export { userReducer } from "./currentUser"; 13 | -------------------------------------------------------------------------------- /store/reducer/client/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { actionName } from "config/action"; 3 | import { 4 | archiveReducer, 5 | blogIdReducer, 6 | headerReducer, 7 | homePageReducer, 8 | ipReducer, 9 | resultReducer, 10 | tagPageReducer, 11 | tagReducer, 12 | tokenReducer, 13 | typePageReducer, 14 | typeReducer, 15 | userReducer, 16 | } from "./action"; 17 | 18 | export const client = combineReducers({ 19 | [actionName.currentHeader]: headerReducer, 20 | [actionName.currentHomePage]: homePageReducer, 21 | [actionName.currentToken]: tokenReducer, 22 | [actionName.currentType]: typeReducer, 23 | [actionName.currentTypePage]: typePageReducer, 24 | [actionName.currentTag]: tagReducer, 25 | [actionName.currentTagPage]: tagPageReducer, 26 | [actionName.currentBlogId]: blogIdReducer, 27 | [actionName.currentArchive]: archiveReducer, 28 | [actionName.currentUser]: userReducer, 29 | [actionName.currentIp]: ipReducer, 30 | [actionName.currentResult]: resultReducer, 31 | }); 32 | -------------------------------------------------------------------------------- /store/reducer/client/share/action.ts: -------------------------------------------------------------------------------- 1 | import { ClientActionType, CreateClientActionProps, CreateClientActionType } from "./type"; 2 | 3 | const clientAction: ClientActionType = { 4 | SET_DATA_LOADING: (name) => `setDataAction_client_${name}`, 5 | SET_DATA_ACTION: (name) => `setDataAction_client_${name}`, 6 | SET_DATA_SUCCESS: (name) => `setDataSuccess_client_${name}`, 7 | SET_DATA_FAIL: (name) => `setDataFail_client_${name}`, 8 | }; 9 | 10 | const setDataLoading_client: CreateClientActionType = ({ name }) => ({ type: clientAction.SET_DATA_LOADING(name), loadingState: true }); 11 | 12 | const setDataAction_client: CreateClientActionType = ({ name }) => ({ type: clientAction.SET_DATA_ACTION(name), loadingState: true }); 13 | 14 | const setDataSuccess_client: CreateClientActionType = ({ name, data }: CreateClientActionProps) => ({ 15 | type: clientAction.SET_DATA_SUCCESS(name), 16 | data, 17 | loadingState: false, 18 | }); 19 | 20 | const setDataFail_client: CreateClientActionType = ({ name, error }: CreateClientActionProps) => ({ 21 | type: clientAction.SET_DATA_FAIL(name), 22 | error, 23 | loadingState: false, 24 | }); 25 | 26 | export { clientAction, setDataLoading_client, setDataAction_client, setDataSuccess_client, setDataFail_client }; 27 | -------------------------------------------------------------------------------- /store/reducer/client/share/type.ts: -------------------------------------------------------------------------------- 1 | import { ReducerStateAction } from "store/reducer/type"; 2 | import { ClientReducerKey } from "../type"; 3 | 4 | interface ClientActionType { 5 | SET_DATA_LOADING: (name: ClientReducerKey) => string; 6 | SET_DATA_ACTION: (name: ClientReducerKey) => string; 7 | SET_DATA_SUCCESS: (name: ClientReducerKey) => string; 8 | SET_DATA_FAIL: (name: ClientReducerKey) => string; 9 | } 10 | interface CreateClientActionProps { 11 | name: ClientReducerKey; 12 | data?: T; 13 | error?: any; 14 | } 15 | interface CreateClientActionType { 16 | (props: CreateClientActionProps): ReducerStateAction; 17 | } 18 | 19 | export type { ClientActionType, CreateClientActionProps, CreateClientActionType }; 20 | -------------------------------------------------------------------------------- /store/reducer/client/type.ts: -------------------------------------------------------------------------------- 1 | import { actionName } from "config/action"; 2 | import { 3 | archiveReducer, 4 | blogIdReducer, 5 | headerReducer, 6 | homePageReducer, 7 | ipReducer, 8 | resultReducer, 9 | tagPageReducer, 10 | tagReducer, 11 | tokenReducer, 12 | typePageReducer, 13 | typeReducer, 14 | userReducer, 15 | } from "./action"; 16 | 17 | export type ClientReducerKey = actionName; 18 | 19 | export type ClientReducer = { 20 | [actionName.currentArchive]: ReturnType; 21 | [actionName.currentBlogId]: ReturnType; 22 | [actionName.currentHeader]: ReturnType; 23 | [actionName.currentHomePage]: ReturnType; 24 | [actionName.currentIp]: ReturnType; 25 | [actionName.currentResult]: ReturnType; 26 | [actionName.currentTag]: ReturnType; 27 | [actionName.currentTagPage]: ReturnType; 28 | [actionName.currentToken]: ReturnType; 29 | [actionName.currentType]: ReturnType; 30 | [actionName.currentTypePage]: ReturnType; 31 | [actionName.currentUser]: ReturnType; 32 | }; 33 | -------------------------------------------------------------------------------- /store/reducer/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { client } from "./client"; 3 | import { server } from "./server"; 4 | 5 | export const rootReducer = combineReducers({ 6 | client, 7 | server, 8 | }); 9 | -------------------------------------------------------------------------------- /store/reducer/server/action/index.ts: -------------------------------------------------------------------------------- 1 | export { blogReducer } from "./blog"; 2 | export { homeReducer } from "./home"; 3 | export { tagReducer } from "./tag"; 4 | export { typeReducer } from "./type"; 5 | export { userHomeReducer } from "./userHome"; 6 | -------------------------------------------------------------------------------- /store/reducer/server/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { apiName } from "config/api"; 3 | import { homeReducer, blogReducer, typeReducer, tagReducer, userHomeReducer } from "./action"; 4 | 5 | export const server = combineReducers({ 6 | [apiName.home]: homeReducer, 7 | [apiName.type]: typeReducer, 8 | [apiName.tag]: tagReducer, 9 | [apiName.blog]: blogReducer, 10 | [apiName.userHome]: userHomeReducer, 11 | }); 12 | -------------------------------------------------------------------------------- /store/reducer/server/share/action.ts: -------------------------------------------------------------------------------- 1 | import { CreateServerActionProps, CreateServerActionType, ServerActionType } from "./type"; 2 | 3 | const serverAction: ServerActionType = { 4 | GET_DATA_LOADING: (name) => `getDataLoading_server_${name}`, 5 | GET_DATA_ACTION: (name) => `getDataAction_server_${name}`, 6 | GET_DATA_SUCCESS: (name) => `getDataSuccess_server_${name}`, 7 | GET_DATA_FAIL: (name) => `getDataFail_server_${name}`, 8 | }; 9 | 10 | const getDataLoading_server: CreateServerActionType = ({ name }) => ({ type: serverAction.GET_DATA_LOADING(name), loadingState: true }); 11 | 12 | const getDataAction_Server: CreateServerActionType = ({ name }) => ({ type: serverAction.GET_DATA_ACTION(name), loadingState: true }); 13 | 14 | const getDataSuccess_Server: CreateServerActionType = ({ name, data }: CreateServerActionProps) => ({ 15 | type: serverAction.GET_DATA_SUCCESS(name), 16 | data, 17 | loadingState: false, 18 | }); 19 | 20 | const getDataFail_Server: CreateServerActionType = ({ name, error }: CreateServerActionProps) => ({ 21 | type: serverAction.GET_DATA_FAIL(name), 22 | error, 23 | loadingState: false, 24 | }); 25 | 26 | export { serverAction, getDataLoading_server, getDataAction_Server, getDataSuccess_Server, getDataFail_Server }; 27 | -------------------------------------------------------------------------------- /store/reducer/server/share/type.ts: -------------------------------------------------------------------------------- 1 | import { ReducerStateAction } from "store/reducer/type"; 2 | import { ServerReducerKey } from "../type"; 3 | 4 | interface ServerActionType { 5 | GET_DATA_LOADING: (name: ServerReducerKey) => string; 6 | GET_DATA_ACTION: (name: ServerReducerKey) => string; 7 | GET_DATA_SUCCESS: (name: ServerReducerKey) => string; 8 | GET_DATA_FAIL: (name: ServerReducerKey) => string; 9 | } 10 | 11 | interface CreateServerActionProps { 12 | name: ServerReducerKey; 13 | data?: T; 14 | error?: any; 15 | } 16 | 17 | interface CreateServerActionType { 18 | (props: CreateServerActionProps): ReducerStateAction; 19 | } 20 | 21 | export type { ServerActionType, CreateServerActionProps, CreateServerActionType }; 22 | -------------------------------------------------------------------------------- /store/reducer/server/type.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { homeReducer, typeReducer, tagReducer, blogReducer, userHomeReducer } from "./action"; 3 | 4 | export type ServerReducerKey = apiName.home | apiName.type | apiName.tag | apiName.blog | apiName.userHome; 5 | 6 | export type ServerReducer = { 7 | [apiName.home]: ReturnType; 8 | [apiName.type]: ReturnType; 9 | [apiName.tag]: ReturnType; 10 | [apiName.blog]: ReturnType; 11 | [apiName.userHome]: ReturnType; 12 | }; 13 | -------------------------------------------------------------------------------- /store/reducer/type.ts: -------------------------------------------------------------------------------- 1 | import type { AnyAction } from "redux"; 2 | 3 | interface ReducerState { 4 | readonly data: T; 5 | readonly error: any; 6 | readonly loading: boolean; 7 | readonly loaded: boolean; 8 | } 9 | interface ReducerStateAction extends AnyAction { 10 | data?: T; 11 | error?: Error; 12 | loadingState?: boolean; 13 | } 14 | interface ReducerStateActionMapType { 15 | [props: string]: (state: ReducerState, action: ReducerStateAction) => ReducerState; 16 | } 17 | 18 | export type { ReducerState, ReducerStateAction, ReducerStateActionMapType }; 19 | -------------------------------------------------------------------------------- /store/saga/action/blog.ts: -------------------------------------------------------------------------------- 1 | import { call, put, select } from "redux-saga/effects"; 2 | import { apiName } from "config/api"; 3 | import { actionName } from "config/action"; 4 | import { createRequest } from "utils/fetcher"; 5 | import { StoreState } from "store/type"; 6 | import { getDataFail_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 7 | 8 | export function* getBlogData() { 9 | const state: StoreState = yield select<(state: StoreState) => StoreState>((state) => state); 10 | const apiToken = state.client[actionName.currentToken]["data"]; 11 | const id = state.client[actionName.currentBlogId]["data"]; 12 | try { 13 | let { code, state, data } = yield call( 14 | (apiName: apiName) => createRequest({ header: { apiToken }, query: { blogId: id }, apiPath: apiName }).run(), 15 | apiName.blog 16 | ); 17 | if (code === 0) { 18 | yield put(getDataSuccess_Server({ name: apiName.blog, data: { [id]: data } })); 19 | } else { 20 | yield put(getDataFail_Server({ name: apiName.blog, data: { [id]: state } })); 21 | } 22 | } catch (e) { 23 | yield put(getDataFail_Server({ name: apiName.blog, data: { [id]: (e as Error).toString() } })); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /store/saga/action/home.ts: -------------------------------------------------------------------------------- 1 | import { call, put, select } from "redux-saga/effects"; 2 | import { apiName } from "config/api"; 3 | import { actionName } from "config/action"; 4 | import { createRequest } from "utils/fetcher"; 5 | import { StoreState } from "store/type"; 6 | import { getDataFail_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 7 | 8 | export function* getHomeData() { 9 | const apiToken: string = yield select((state: StoreState) => state.client[actionName.currentToken].data); 10 | try { 11 | let { code, state, data } = yield call((apiName: apiName) => createRequest({ header: { apiToken }, apiPath: apiName }).run(), apiName.home); 12 | if (code === 0) { 13 | yield put(getDataSuccess_Server({ name: apiName.home, data })); 14 | } else { 15 | yield put(getDataFail_Server({ name: apiName.home, data: state })); 16 | } 17 | } catch (e) { 18 | yield put(getDataFail_Server({ name: apiName.home, data: (e as Error).toString() })); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /store/saga/action/index.ts: -------------------------------------------------------------------------------- 1 | export { getHomeData } from "./home"; 2 | export { getTypeData } from "./type"; 3 | export { getTagData } from "./tag"; 4 | export { getBlogData } from "./blog"; 5 | -------------------------------------------------------------------------------- /store/saga/action/tag.ts: -------------------------------------------------------------------------------- 1 | import { call, put } from "redux-saga/effects"; 2 | import { apiName } from "config/api"; 3 | import { createRequest } from "utils/fetcher"; 4 | import { getDataFail_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 5 | 6 | export function* getTagData() { 7 | try { 8 | let { code, state, data } = yield call((apiName: apiName) => createRequest({ header: { apiToken: true }, apiPath: apiName }).run(), apiName.tag); 9 | if (code === 0) { 10 | yield put(getDataSuccess_Server({ name: apiName.tag, data })); 11 | } else { 12 | yield put(getDataFail_Server({ name: apiName.tag, data: state })); 13 | } 14 | } catch (e) { 15 | yield put(getDataFail_Server({ name: apiName.tag, data: (e as Error).toString() })); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /store/saga/action/type.ts: -------------------------------------------------------------------------------- 1 | import { call, put } from "redux-saga/effects"; 2 | import { apiName } from "config/api"; 3 | import { createRequest } from "utils/fetcher"; 4 | import { getDataFail_Server, getDataSuccess_Server } from "store/reducer/server/share/action"; 5 | 6 | export function* getTypeData() { 7 | try { 8 | let { code, state, data } = yield call((apiName: apiName) => createRequest({ header: { apiToken: true }, apiPath: apiName }).run(), apiName.type); 9 | if (code === 0) { 10 | yield put(getDataSuccess_Server({ name: apiName.type, data })); 11 | } else { 12 | yield put(getDataFail_Server({ name: apiName.type, data: state })); 13 | } 14 | } catch (e) { 15 | yield put(getDataFail_Server({ name: apiName.type, data: (e as Error).toString() })); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /store/saga/index.ts: -------------------------------------------------------------------------------- 1 | import { apiName } from "config/api"; 2 | import { all, takeLatest } from "redux-saga/effects"; 3 | import { serverAction } from "store/reducer/server/share/action"; 4 | import { getHomeData, getTagData, getTypeData, getBlogData } from "./action"; 5 | 6 | function* rootSaga() { 7 | yield all([ 8 | takeLatest(serverAction.GET_DATA_ACTION(apiName.home), getHomeData), 9 | takeLatest(serverAction.GET_DATA_ACTION(apiName.tag), getTagData), 10 | takeLatest(serverAction.GET_DATA_ACTION(apiName.type), getTypeData), 11 | takeLatest(serverAction.GET_DATA_ACTION(apiName.blog), getBlogData), 12 | ]); 13 | } 14 | 15 | export { rootSaga }; 16 | -------------------------------------------------------------------------------- /store/saga/util.ts: -------------------------------------------------------------------------------- 1 | import { fork, take, cancel } from "redux-saga/effects"; 2 | 3 | import type { rootSaga } from "."; 4 | import type createSagaMiddleware from "@redux-saga/core"; 5 | import type { Saga, Task } from "@redux-saga/types"; 6 | import type { SagaStore } from "../type"; 7 | 8 | export const CANCEL_SAGAS_HMR = "CANCEL_SAGAS_HMR"; 9 | 10 | function createAbortAbleSaga(saga: typeof rootSaga) { 11 | if (process.env.NODE_ENV === "development") { 12 | return function* main() { 13 | const sagaTask: Task = yield fork(saga); 14 | 15 | yield take(CANCEL_SAGAS_HMR); 16 | yield cancel(sagaTask); 17 | }; 18 | } else { 19 | return saga; 20 | } 21 | } 22 | 23 | const SagaManager = { 24 | startSagas(saga: typeof rootSaga, sagaMiddleware: ReturnType) { 25 | return sagaMiddleware.run(createAbortAbleSaga(saga) as Saga); 26 | }, 27 | 28 | cancelSagas(store: SagaStore) { 29 | store.dispatch({ 30 | type: CANCEL_SAGAS_HMR, 31 | }); 32 | }, 33 | }; 34 | 35 | export { SagaManager }; 36 | -------------------------------------------------------------------------------- /store/type.ts: -------------------------------------------------------------------------------- 1 | import { Store } from "redux"; 2 | import { Task } from "redux-saga"; 3 | import type { ClientReducer, ClientReducerKey } from "./reducer/client/type"; 4 | import type { ServerReducer, ServerReducerKey } from "./reducer/server/type"; 5 | 6 | export interface StoreState { 7 | server: { [T in ServerReducerKey]: ServerReducer[T] }; 8 | client: { [T in ClientReducerKey]: ClientReducer[T] }; 9 | } 10 | 11 | export interface SagaStore extends Store { 12 | sagaTask?: Task; 13 | } 14 | -------------------------------------------------------------------------------- /styles/_basic.module.scss: -------------------------------------------------------------------------------- 1 | $grid-breakpoints: ( 2 | xs: 0, 3 | sm: 576px, 4 | md: 768px, 5 | lg: 992px, 6 | xl: 1200px, 7 | ) !default; 8 | 9 | @function breakpoint-next($name, $breakpoints: $grid-breakpoints, $breakpoint-names: map-keys($breakpoints)) { 10 | $n: index($breakpoint-names, $name); 11 | @return if($n != null and $n < length($breakpoint-names), nth($breakpoint-names, $n + 1), null); 12 | } 13 | 14 | @mixin media-breakpoint-up($name) { 15 | $min: map-get($grid-breakpoints, $name); 16 | @media (min-width: $min) { 17 | @content; 18 | } 19 | } 20 | 21 | @mixin media-breakpoint-down($name) { 22 | $maxName: breakpoint-next($name); 23 | $max: map-get($grid-breakpoints, $maxName); 24 | @media (max-width: $max) { 25 | @content; 26 | } 27 | } 28 | 29 | @mixin desktop { 30 | @include media-breakpoint-up(lg) { 31 | @content; 32 | } 33 | } 34 | 35 | @mixin mobile { 36 | @include media-breakpoint-down(md) { 37 | @content; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /styles/_variable.module.scss: -------------------------------------------------------------------------------- 1 | $blue: #007bff; 2 | $indigo: #6610f2; 3 | $purple: #6f42c1; 4 | $pink: #e83e8c; 5 | $red: #dc3545; 6 | $orange: #fd7e14; 7 | $yellow: #ffc107; 8 | $green: #28a745; 9 | $teal: #20c997; 10 | $cyan: #17a2b8; 11 | $white: #fff; 12 | $gray: #6c757d; 13 | $gray-dark: #343a40; 14 | $primary: #007bff; 15 | $secondary: #6c757d; 16 | $success: #28a745; 17 | $info: #17a2b8; 18 | $warning: #ffc107; 19 | $danger: #dc3545; 20 | $light: #f8f9fa; 21 | $dark: #343a40; 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "jsx": "preserve", 6 | "lib": [ 7 | "dom", 8 | "es2017" 9 | ], 10 | "baseUrl": ".", 11 | "moduleResolution": "node", 12 | "strict": true, 13 | "allowJs": true, 14 | "noEmit": true, 15 | "allowSyntheticDefaultImports": true, 16 | "esModuleInterop": true, 17 | "skipLibCheck": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "isolatedModules": true, 21 | "removeComments": false, 22 | "preserveConstEnums": true, 23 | "sourceMap": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "resolveJsonModule": true, 26 | "incremental": true 27 | }, 28 | "exclude": [ 29 | "dist", 30 | ".next", 31 | "out", 32 | "next.config.js" 33 | ], 34 | "include": [ 35 | "next-env.d.ts", 36 | "global.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /tsconfig.server.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "target": "es2020", 6 | "module": "commonjs", 7 | "allowJs": true, 8 | "strict": true, 9 | "isolatedModules": false, 10 | "strictPropertyInitialization": false, 11 | "declaration": false, 12 | "skipLibCheck": true, 13 | "moduleResolution": "node", 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "emitDecoratorMetadata": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "incremental": true, 19 | "noEmit": false, 20 | "baseUrl": ".", 21 | "paths": { 22 | "utils/*": ["utils/*"], 23 | "config/*": ["config/*"], 24 | "server/*": ["server/*"], 25 | "types/*": ["types/*"] 26 | }, 27 | "typeRoots": ["global.d.ts", "next-env.d.ts", "types/*"], 28 | "plugins": [{ "transform": "typescript-transform-paths" }] 29 | }, 30 | "include": ["**/*.ts", "**/*.tsx"] 31 | } 32 | -------------------------------------------------------------------------------- /utils/env.ts: -------------------------------------------------------------------------------- 1 | export const isServer = typeof window === "undefined"; 2 | 3 | export const isBrowser = !isServer; 4 | -------------------------------------------------------------------------------- /utils/headers.ts: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | import { GetHeaderType, HeaderProps } from "types/utils"; 3 | 4 | const getHeader: GetHeaderType = (header = {}) => { 5 | const resultHeader: HeaderProps = {}; 6 | for (let key in header) { 7 | if (header[key] === true) { 8 | resultHeader[key] = Cookies.get(`${key}`) || ""; 9 | } else { 10 | resultHeader[key] = header[key]; 11 | } 12 | } 13 | return resultHeader; 14 | }; 15 | 16 | export { getHeader }; 17 | -------------------------------------------------------------------------------- /utils/image.ts: -------------------------------------------------------------------------------- 1 | import { createRequest } from "./fetcher"; 2 | import { transformPath } from "./path"; 3 | import { autoTransformData } from "./data"; 4 | import { ApiRequestResult, LoadImgType } from "types/utils"; 5 | 6 | const loadImg: LoadImgType = ({ imgUrl, strUrl, imgElement, state = true }) => { 7 | if (state) { 8 | return new Promise((resolve) => { 9 | imgElement.setAttribute("src", transformPath({ apiPath: imgUrl, query: { time: String(Date.now()) } })); 10 | imgElement.addEventListener("load", () => resolve(imgElement), { once: true }); 11 | }).then((imgEle) => 12 | createRequest({ apiPath: strUrl, cache: false }) 13 | .run>() 14 | .then((data) => autoTransformData(data)) 15 | .then((value) => imgEle.setAttribute("title", value)) 16 | ); 17 | } else { 18 | return Promise.resolve(); 19 | } 20 | }; 21 | 22 | const clearImg = (imgElement: HTMLImageElement) => { 23 | imgElement.removeAttribute("src"); 24 | imgElement.removeAttribute("title"); 25 | }; 26 | 27 | export { loadImg, clearImg }; 28 | -------------------------------------------------------------------------------- /utils/log.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import PrettyError from "pretty-error"; 3 | import { isBrowser } from "./env"; 4 | 5 | const pre = new PrettyError(); 6 | 7 | const side = isBrowser ? "client" : "server"; 8 | 9 | const log = (message: string | Error, lev: "normal" | "warn" | "error") => { 10 | if (lev === "error") { 11 | if (side === "server") { 12 | if (message instanceof Error) { 13 | console.log(`[${side}]`, pre.render(message)); 14 | } else { 15 | console.log(`[${side}]`, pre.render(new Error(message))); 16 | } 17 | } else { 18 | console.log(`[client]`, chalk.red(message.toString())); 19 | } 20 | } else if (lev === "warn") { 21 | console.log(`[${side}]`, chalk.green(message.toString())); 22 | } else { 23 | if (process.env.NODE_ENV === "development") { 24 | console.log(`[${side}]`, message.toString()); 25 | } 26 | } 27 | }; 28 | 29 | export { log }; 30 | -------------------------------------------------------------------------------- /utils/path.ts: -------------------------------------------------------------------------------- 1 | import { log } from "./log"; 2 | import { TransformPathType } from "types/utils"; 3 | 4 | export const transformPath: TransformPathType = ({ path, apiPath, query, needPre = true }) => { 5 | if (!path && !apiPath) { 6 | log(`transform path not exist`, "warn"); 7 | return ""; 8 | } else if (path && apiPath) { 9 | log(`multiple path discover. path: ${path}, apiPath: ${apiPath}`, "error"); 10 | } 11 | let currentPath = ""; 12 | if (apiPath) { 13 | currentPath = apiPath; 14 | if (!currentPath.startsWith("/")) { 15 | currentPath = "/" + apiPath; 16 | } 17 | if (!currentPath.startsWith("/api")) { 18 | currentPath = "/api" + currentPath; 19 | } 20 | if (needPre) { 21 | currentPath = process.env.NEXT_PUBLIC_API_HOST + currentPath; 22 | if (!currentPath.startsWith("http")) { 23 | currentPath = "http://" + currentPath; 24 | } 25 | } 26 | } else if (path) { 27 | if (!path.startsWith("http")) { 28 | log(`Incomplete path! third part link, path : ${path}`, "warn"); 29 | } else { 30 | log(`third part link, path ${path}`, "normal"); 31 | } 32 | currentPath = path; 33 | } 34 | if (query) { 35 | if (!currentPath.includes("?")) { 36 | currentPath += "?"; 37 | } else { 38 | currentPath += "&"; 39 | } 40 | const queryObject = new URLSearchParams(query); 41 | currentPath += queryObject.toString(); 42 | } 43 | return currentPath; 44 | }; 45 | -------------------------------------------------------------------------------- /utils/time.ts: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs"; 2 | import "dayjs/locale/zh-cn"; 3 | import relativeTime from "dayjs/plugin/relativeTime"; 4 | import calendarPlugin from "dayjs/plugin/calendar"; 5 | import { log } from "./log"; 6 | import type { TimeToString } from "types/utils"; 7 | 8 | dayjs.locale("zh-cn"); 9 | dayjs.extend(relativeTime); 10 | dayjs.extend(calendarPlugin); 11 | 12 | const momentTo: TimeToString = (time) => { 13 | if (typeof time === "string") { 14 | time = new Date(time); 15 | } 16 | if (time instanceof Date) { 17 | return dayjs(new Date()).to(dayjs(time)); 18 | } else { 19 | log(`time parameter error : ${time}`, "error"); 20 | return dayjs().toNow(); 21 | } 22 | }; 23 | 24 | const calendar: TimeToString = (time) => { 25 | if (typeof time === "string") { 26 | time = new Date(time); 27 | } 28 | if (time instanceof Date) { 29 | return dayjs(time).calendar(); 30 | } else { 31 | return dayjs(new Date()).calendar(); 32 | } 33 | }; 34 | 35 | export { momentTo, calendar }; 36 | --------------------------------------------------------------------------------