├── .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 |
10 | -
11 |
12 |
13 | {username}
14 |
15 |
16 | -
17 |
18 | {blogModifyState ? "更新于:" : "发布于:"}
19 | {calendar(blogModifyDate!)}
20 |
21 |
22 | -
23 |
24 | 看过:
25 | {blogReadCount}
26 |
27 |
28 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
37 | {column === 1 ? : }
38 |
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 |
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 |
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 |
18 |
19 |
20 |
21 | {value}
22 |
23 |
24 |
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 |
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 |
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={() => }
25 | />
26 |
27 |
28 | );
29 | };
30 |
31 | const ImgItem = ({ relativeUrl }: { relativeUrl: string }) => {
32 | const [pinchRef, coverRef] = usePinch();
33 |
34 | return (
35 |
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 |
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/containers/About/aboutRightAbout.tsx:
--------------------------------------------------------------------------------
1 | import { SimpleElement } from "types/components";
2 |
3 | export const AboutRightAbout: SimpleElement = () => {
4 | return (
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 |
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 |
8 |
9 |
10 |
11 |
12 |
13 |
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 |
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 |
12 |
13 |
{blogTitle}
14 |
15 |
16 |
17 |
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 |
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 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/containers/Blog/blogContentImg.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { getClass } from "utils/dom";
3 | import { usePinch } from "hook/usePinch";
4 |
5 | import style from "./index.module.scss";
6 |
7 | export const BlogContentImg = ({ src }: { src: string }) => {
8 | const [pinchRef, coverRef] = usePinch();
9 |
10 | return (
11 |
12 | {/* */}
13 |
14 |
15 |
16 | {/*

*/}
17 |
18 |
19 | {/*
*/}
20 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/containers/Blog/blogContentLike.tsx:
--------------------------------------------------------------------------------
1 | import { apiName } from "config/api";
2 | import { LoadRender } from "components/LoadRender";
3 | import { BlogContentLikeToPayModule } from "./blogContentLikeToPayModule";
4 | import { flexCenter, getClass } from "utils/dom";
5 | import { useLikeToPayModule } from "hook/useBlog";
6 | import { AuthorProps, BlogProps, UserProps } from "types";
7 |
8 | export const BlogContentLike = ({ userId, blogPriseState }: { userId: UserProps["userId"]; blogPriseState: BlogProps["blogPriseState"] }) => {
9 | const click = useLikeToPayModule({
10 | body: (
11 |
12 | apiPath={apiName.author}
13 | query={{ userId: userId }}
14 | loaded={({ userAlipay, userWechat }) => }
15 | />
16 | ),
17 | });
18 |
19 | return (
20 |
21 |
22 |
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 |
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 |
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 |
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 |
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 |
10 |
11 |
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 |
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 |
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 |
33 |
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 |
22 |
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 |
36 |
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 |
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 |
10 |
11 |
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 |
26 |
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 |
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 ;
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------