├── .gitignore
├── LICENSE
├── README.md
├── admin-blog
├── .gitignore
├── README.md
├── build.zip
├── craco.config.js
├── package-lock.json
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ ├── noto-sans-sc-700.woff
│ └── robots.txt
└── src
│ ├── App.js
│ ├── App.less
│ ├── App.test.js
│ ├── app
│ └── store.js
│ ├── components
│ ├── AllCategories.jsx
│ ├── AllImages.jsx
│ ├── AllLinks.jsx
│ ├── AllPosts.jsx
│ ├── AllTags.jsx
│ ├── ArticleInfo.jsx
│ ├── BlogDashboard.jsx
│ ├── BlogSetting.jsx
│ ├── CategoryNav.jsx
│ ├── ChangePassword.jsx
│ ├── Clickable.jsx
│ ├── CommentBox.jsx
│ ├── CommentNav.jsx
│ ├── CopyButton.jsx
│ ├── EditLink.jsx
│ ├── EditableTable.jsx
│ ├── FilterSelectWrap.jsx
│ ├── HCenterSpin.jsx
│ ├── ImageCard.jsx
│ ├── ImageUploadBox.jsx
│ ├── LinkForm.jsx
│ ├── LinkNav.jsx
│ ├── Loading.jsx
│ ├── MDComponent.jsx
│ ├── ManageNav.jsx
│ ├── MarkDownEditor.jsx
│ ├── MediaNav.jsx
│ ├── NewLink.jsx
│ ├── NewPost.jsx
│ ├── PanelFooter.jsx
│ ├── PanelHeader.jsx
│ ├── PostCategory.jsx
│ ├── PostComments.jsx
│ ├── PostEdit.jsx
│ ├── PostForm.jsx
│ ├── PostNav.jsx
│ ├── PostPreview.jsx
│ ├── PostSearch.jsx
│ ├── PostTags.jsx
│ ├── RecentComments.jsx
│ ├── RequireAuth.jsx
│ ├── Spinner.jsx
│ ├── TagNav.jsx
│ └── WordCloud.jsx
│ ├── css
│ ├── AdminPanel.css
│ ├── AllImages.module.css
│ ├── AllItems.module.css
│ ├── AllLinks.module.css
│ ├── BlogDashboard.module.css
│ ├── BlogSetting.module.css
│ ├── EditPostForm.module.css
│ ├── HCenterSpin.module.css
│ ├── Loading.module.css
│ ├── Login.module.css
│ ├── MarkDownEditor.module.css
│ ├── NewForm.module.css
│ ├── NewItem.module.css
│ ├── NewLink.module.css
│ ├── PostComments.module.css
│ ├── PostPreview.module.css
│ ├── PostSearch.module.css
│ ├── PostTC.module.css
│ ├── RecentComments.module.css
│ └── ThemeSetting.module.css
│ ├── features
│ ├── auth
│ │ ├── authService.js
│ │ └── authSlice.js
│ ├── comments
│ │ ├── commentService.js
│ │ └── commentSlice.js
│ ├── links
│ │ ├── linkService.js
│ │ └── linkSlice.js
│ ├── posts
│ │ ├── postService.js
│ │ └── postSlice.js
│ └── profile
│ │ ├── profileService.js
│ │ └── profileSlice.js
│ ├── hooks
│ ├── useColumnSearch.js
│ ├── useGetData.js
│ ├── usePrevious.js
│ ├── useRedirect.js
│ └── useStackedLineChart.js
│ ├── images
│ ├── butterfly-logo.svg
│ ├── check-success-svgrepo-com.svg
│ ├── copy-svgrepo-com.svg
│ └── slanted-gradient_blue.svg
│ ├── index.css
│ ├── index.js
│ ├── pages
│ ├── AdminPanel.js
│ ├── Loading.jsx
│ └── Login.jsx
│ ├── reportWebVitals.js
│ ├── setupProxy.js
│ └── setupTests.js
├── backend
├── .envexample
├── .gitignore
├── config
│ └── db.js
├── controllers
│ ├── commentController.js
│ ├── imageController.js
│ ├── linkController.js
│ ├── postController.js
│ ├── profileController.js
│ ├── uploadController.js
│ └── userController.js
├── middleware
│ ├── authMiddleware.js
│ ├── demoMiddleware.js
│ └── errorMiddleware.js
├── models
│ ├── commentModel.js
│ ├── imageModel.js
│ ├── linkModel.js
│ ├── pageModel.js
│ ├── postModel.js
│ ├── profileModel.js
│ └── userModel.js
├── package-lock.json
├── package.json
├── routes
│ ├── commentRoutes.js
│ ├── imageRoutes.js
│ ├── linkRoutes.js
│ ├── postRoutes.js
│ ├── profileRoutes.js
│ ├── uploadRoutes.js
│ └── userRoutes.js
├── server.js
└── uploads
│ └── image
│ ├── avatar.ico
│ ├── logo.svg
│ └── og-image.png
├── docs
└── images
│ ├── blog-system-small.PNG
│ └── blog-system.PNG
└── nextjs-blog
├── .envexample
├── .eslintrc.json
├── .gitignore
├── README.md
├── components
├── CommentForm.js
├── CommentSection.js
├── Comments.js
├── CopyButton.js
├── Footer.js
├── Header.js
├── LoadMoreBtn.js
├── MarkDown.js
├── PostLayout.js
├── PostList.js
├── PostLoading.js
├── PostTimeline.js
├── SEO.js
├── SearchBar.js
├── ShrinkHeader.js
├── ThemeChanger.js
└── TocAndMD.js
├── lib
├── generate-rss.js
├── linksData.js
├── posts.js
├── siteData.js
└── utils
│ ├── db.js
│ ├── dbConnect.js
│ ├── htmlEscaper.js
│ ├── linkModel.js
│ ├── pageModel.js
│ ├── postModel.js
│ ├── profileModel.js
│ └── userModel.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
├── _app.js
├── api
│ └── revalidate.js
├── categories.js
├── categories
│ └── [category].js
├── index.js
├── links.js
├── posts
│ └── [slug].js
├── sitemap.xml.js
├── tags.js
├── tags
│ └── [tag].js
└── timeline.js
├── public
├── favicon.ico
├── rainbow_wave.svg
└── robots.txt
└── styles
├── AnimatePublic.module.css
├── Categories.module.css
├── CategoryPage.module.css
├── CommentForm.module.css
├── Comments.module.css
├── CopyButton.module.css
├── Footer.module.css
├── Header.module.css
├── Home.module.css
├── Links.module.css
├── LoadMoreBtn.module.css
├── MarkDown.module.css
├── PostLayout.module.css
├── PostList.module.css
├── PostLoading.module.css
├── PostTimeline.module.css
├── SearchBar.module.css
├── TagPage.module.css
├── Tags.module.css
├── ThemeChanger.module.css
├── TocAndMD.module.css
├── fonts
├── icomoon.eot
├── icomoon.svg
├── icomoon.ttf
└── icomoon.woff
├── globals.css
├── globals.scss
└── icomoonStyle.css
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | nextjs-blog/public/tags/
4 | nextjs-blog/public/categories/
5 | nextjs-blog/public/feed.xml
6 |
7 | .VSCodeCounter/
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 manfred
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/admin-blog/.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 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 |
26 |
--------------------------------------------------------------------------------
/admin-blog/README.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/admin-blog/README.md
--------------------------------------------------------------------------------
/admin-blog/build.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/admin-blog/build.zip
--------------------------------------------------------------------------------
/admin-blog/craco.config.js:
--------------------------------------------------------------------------------
1 |
2 | const { whenProd } = require("@craco/craco");
3 | const zlib = require("zlib");
4 | // const BundleAnalyzerPlugin =
5 | // require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
6 | const CompressionWebpackPlugin = require("compression-webpack-plugin");
7 |
8 | // 查看打包时间
9 | // const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
10 | // const smp = new SpeedMeasurePlugin();
11 |
12 | const overrideConfig = {
13 | plugins: [
14 |
15 | ],
16 | babel: {
17 | plugins: [
18 |
19 | ],
20 | },
21 | webpack: {
22 | plugins: whenProd(
23 | () => [
24 | //new BundleAnalyzerPlugin(),
25 | new CompressionWebpackPlugin({
26 | filename: "[path][base].gz",
27 | algorithm: "gzip", // 使用gzip压缩
28 | test: new RegExp(
29 | "\\.(js|css|html)$" // 压缩 js 与 css
30 | ),
31 | threshold: 8192, // 资源文件大于8192B时会被压缩
32 | minRatio: 0.8, // 最小压缩比达到0.8时才会被压缩
33 | deleteOriginalAssets: false,
34 | }),
35 | new CompressionWebpackPlugin({
36 | filename: "[path][base].br",
37 | algorithm: "brotliCompress", // 使用brotli算法压缩
38 | test: new RegExp(
39 | "\\.(js|css|html|svg)$" // 压缩 js,html,css,svg
40 | ),
41 | compressionOptions: {
42 | params: {
43 | [zlib.constants.BROTLI_PARAM_QUALITY]: 11,
44 | },
45 | },
46 | threshold: 8192,
47 | minRatio: 0.8,
48 | deleteOriginalAssets: false,
49 | }),
50 | ],
51 | []
52 | ),
53 | configure: whenProd(() => {
54 | return {
55 | mode: "production",
56 | devtool: "nosources-source-map",
57 | };
58 | }, {}),
59 | },
60 | };
61 |
62 | module.exports = overrideConfig;
63 |
--------------------------------------------------------------------------------
/admin-blog/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "admin-blog",
3 | "sideEffects": [
4 | "*.css"
5 | ],
6 | "version": "0.3.0",
7 | "private": true,
8 | "dependencies": {
9 | "@ant-design/icons": "^4.7.0",
10 | "@react-three/drei": "^9.73.0",
11 | "@react-three/fiber": "^8.0.27",
12 | "@reduxjs/toolkit": "^1.8.1",
13 | "@testing-library/jest-dom": "^5.16.4",
14 | "@testing-library/react": "^13.2.0",
15 | "@testing-library/user-event": "^14.2.0",
16 | "antd": "^5.13.2",
17 | "axios": "^0.27.2",
18 | "copy-to-clipboard": "^3.3.1",
19 | "dayjs": "^1.11.10",
20 | "easymde": "^2.16.1",
21 | "echarts": "^5.3.3",
22 | "font-awesome": "^4.7.0",
23 | "github-markdown-css": "^5.1.0",
24 | "http-proxy-middleware": "^2.0.6",
25 | "js-file-download": "^0.4.12",
26 | "parse-numeric-range": "^1.3.0",
27 | "react": "^18.1.0",
28 | "react-copy-to-clipboard": "^5.1.0",
29 | "react-dom": "^18.1.0",
30 | "react-highlight-words": "^0.18.0",
31 | "react-markdown": "^8.0.3",
32 | "react-redux": "^8.0.1",
33 | "react-responsive": "^9.0.0-beta.8",
34 | "react-router-dom": "^6.3.0",
35 | "react-scripts": "5.0.1",
36 | "react-simplemde-editor": "^5.0.2",
37 | "react-syntax-highlighter": "^15.5.0",
38 | "react-toastify": "^9.0.1",
39 | "rehype-katex": "^6.0.2",
40 | "remark-footnotes": "^4.0.1",
41 | "remark-gfm": "^3.0.1",
42 | "remark-math": "^5.1.1",
43 | "three": "^0.141.0",
44 | "web-vitals": "^2.1.4",
45 | "yet-another-react-lightbox": "^3.17.1"
46 | },
47 | "scripts": {
48 | "start": "craco start",
49 | "build": "craco build",
50 | "test": "craco test",
51 | "eject": "react-scripts eject"
52 | },
53 | "eslintConfig": {
54 | "extends": [
55 | "react-app",
56 | "react-app/jest"
57 | ]
58 | },
59 | "browserslist": {
60 | "production": [
61 | ">0.2%",
62 | "not dead",
63 | "not op_mini all"
64 | ],
65 | "development": [
66 | "last 1 chrome version",
67 | "last 1 firefox version",
68 | "last 1 safari version"
69 | ]
70 | },
71 | "devDependencies": {
72 | "@craco/craco": "^7.1.0",
73 | "babel-plugin-import": "^1.13.5",
74 | "compression-webpack-plugin": "^10.0.0",
75 | "speed-measure-webpack-plugin": "^1.5.0",
76 | "webpack-bundle-analyzer": "^4.5.0"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/admin-blog/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/admin-blog/public/favicon.ico
--------------------------------------------------------------------------------
/admin-blog/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | 博客管理
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/admin-blog/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/admin-blog/public/logo192.png
--------------------------------------------------------------------------------
/admin-blog/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/admin-blog/public/logo512.png
--------------------------------------------------------------------------------
/admin-blog/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Blog Admin",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#457fca",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/admin-blog/public/noto-sans-sc-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/admin-blog/public/noto-sans-sc-700.woff
--------------------------------------------------------------------------------
/admin-blog/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/admin-blog/src/App.less:
--------------------------------------------------------------------------------
1 | //@import '~antd/dist/antd.less';
2 |
3 | // .App {
4 | // text-align: center;
5 | // }
6 |
7 | // .App-logo {
8 | // height: 40vmin;
9 | // pointer-events: none;
10 | // }
11 |
12 | // @media (prefers-reduced-motion: no-preference) {
13 | // .App-logo {
14 | // animation: App-logo-float infinite 3s ease-in-out;
15 | // }
16 | // }
17 |
18 | // .App-header {
19 | // min-height: 100vh;
20 | // display: flex;
21 | // flex-direction: column;
22 | // align-items: center;
23 | // justify-content: center;
24 | // font-size: calc(10px + 2vmin);
25 | // }
26 |
27 | // .App-link {
28 | // color: rgb(112, 76, 182);
29 | // }
30 |
31 | // @keyframes App-logo-float {
32 | // 0% {
33 | // transform: translateY(0);
34 | // }
35 | // 50% {
36 | // transform: translateY(10px);
37 | // }
38 | // 100% {
39 | // transform: translateY(0px);
40 | // }
41 | // }
42 |
--------------------------------------------------------------------------------
/admin-blog/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { Provider } from 'react-redux';
4 | import { store } from './app/store';
5 | import App from './App';
6 |
7 | test('renders learn react link', () => {
8 | const { getByText } = render(
9 |
10 |
11 |
12 | );
13 |
14 | expect(getByText(/learn/i)).toBeInTheDocument();
15 | });
16 |
--------------------------------------------------------------------------------
/admin-blog/src/app/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import authReducer from '../features/auth/authSlice'
3 | import linkReducer from '../features/links/linkSlice'
4 | import postReducer from '../features/posts/postSlice'
5 | import profileReducer from '../features/profile/profileSlice'
6 | import commentReducer from '../features/comments/commentSlice'
7 |
8 |
9 | export const store = configureStore({
10 | reducer: {
11 | auth: authReducer,
12 | links:linkReducer,
13 | posts:postReducer,
14 | profile:profileReducer,
15 | comments:commentReducer
16 | },
17 | });
18 |
19 |
--------------------------------------------------------------------------------
/admin-blog/src/components/AllPosts.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {useSearchParams} from "react-router-dom";
3 | import PostEdit from "./PostEdit";
4 | import PostPreview from "./PostPreview";
5 | import PostSearch from "./PostSearch"
6 |
7 | function AllPosts() {
8 | const [searchParams] = useSearchParams();
9 | const previewParams = searchParams.get('preview');
10 | const editParams = searchParams.get('edit');
11 | if(previewParams) {
12 | return ;
13 | }
14 | if(editParams) {
15 | return ;
16 | }
17 |
18 | return (
19 |
20 | )
21 | }
22 |
23 | export default AllPosts
--------------------------------------------------------------------------------
/admin-blog/src/components/ArticleInfo.jsx:
--------------------------------------------------------------------------------
1 | function ArticleInfo({
2 | boxClass,
3 | titleClass,
4 | title,
5 | dateClass,
6 | date,
7 | authorClass,
8 | authors,
9 | }) {
10 | return (
11 |
12 |
{title}
13 |
14 | 发布日期:
15 | {date}
16 |
17 |
18 |
19 | 作者:
20 | {authors.map((author) => (
21 | {`${author} `}
22 | ))}
23 |
24 |
25 | );
26 | }
27 |
28 | export default ArticleInfo;
29 |
--------------------------------------------------------------------------------
/admin-blog/src/components/CategoryNav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet } from "react-router-dom";
3 | import useRedirect from "../hooks/useRedirect.js";
4 |
5 | function CategoryNav() {
6 | useRedirect("/manage/category", "all-categories");
7 | return ;
8 | }
9 |
10 | export default CategoryNav
--------------------------------------------------------------------------------
/admin-blog/src/components/Clickable.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | function Clickable({children}) {
4 | return (
5 |
12 | )
13 | }
14 |
15 | export default Clickable
--------------------------------------------------------------------------------
/admin-blog/src/components/CommentBox.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function CommentBox({ className, author, avatar, content, datetime }) {
4 | return (
5 |
6 |
7 |
15 | {avatar}
16 |
17 |
20 |
28 | {author}
29 | {datetime}
30 |
31 |
{content}
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | export default CommentBox;
39 |
--------------------------------------------------------------------------------
/admin-blog/src/components/CommentNav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet } from "react-router-dom";
3 | import useRedirect from "../hooks/useRedirect.js";
4 |
5 | function CommentNav() {
6 | useRedirect("/manage/comment", "recent-comments");
7 | return ;
8 | }
9 |
10 | export default CommentNav
--------------------------------------------------------------------------------
/admin-blog/src/components/CopyButton.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { CopyToClipboard } from "react-copy-to-clipboard";
3 | import copySvg from "../images/copy-svgrepo-com.svg";
4 | import copiedSvg from "../images/check-success-svgrepo-com.svg";
5 |
6 | function CopyButton({ text, classCopy, classCopied }) {
7 | const [copied, setCopied] = useState(false);
8 | useEffect(() => {
9 | if (copied) {
10 | setTimeout(() => {
11 | setCopied(false);
12 | }, 2000);
13 | }
14 | }, [copied]);
15 | return (
16 | {
19 | if (result) {
20 | setCopied(true);
21 | }
22 | }}
23 | >
24 | {copied ? (
25 |
26 | ) : (
27 |
33 | )}
34 |
35 | );
36 | }
37 |
38 | export default CopyButton;
39 |
--------------------------------------------------------------------------------
/admin-blog/src/components/EditableTable.jsx:
--------------------------------------------------------------------------------
1 | import { Form, Input, InputNumber, Table } from "antd";
2 |
3 | function EditableTable({
4 | form,
5 | data,
6 | columns,
7 | editingKey,
8 | setEditingKey,
9 | tableClass,
10 | tableRowClass,
11 | }) {
12 | const EditableCell = ({
13 | editing,
14 | dataIndex,
15 | title,
16 | inputType,
17 | record,
18 | index,
19 | children,
20 | ...restProps
21 | }) => {
22 | const inputNode = inputType === "number" ? : ;
23 | return (
24 |
25 | {editing ? (
26 |
38 | {inputNode}
39 |
40 | ) : (
41 | children
42 | )}
43 | |
44 | );
45 | };
46 | const isEditing = (record) => record.key === editingKey;
47 | const mergedColumns = columns.map((col) => {
48 | if (!col.editable) {
49 | return col;
50 | }
51 |
52 | return {
53 | ...col,
54 | onCell: (record) => ({
55 | record,
56 | inputType: col.dataIndex === "postNumber" ? "number" : "text",
57 | dataIndex: col.dataIndex,
58 | title: col.title,
59 | editing: isEditing(record),
60 | }),
61 | };
62 | });
63 | const cancel = () => {
64 | setEditingKey("");
65 | };
66 | return (
67 |
83 | );
84 | }
85 |
86 | export default EditableTable;
87 |
--------------------------------------------------------------------------------
/admin-blog/src/components/FilterSelectWrap.jsx:
--------------------------------------------------------------------------------
1 | import { Select } from "antd";
2 | import { useMediaQuery } from "react-responsive";
3 | const { Option } = Select;
4 |
5 | function FilterSelectWrap({
6 | label,
7 | placeholder,
8 | onChange,
9 | selectClass,
10 | selectValue,
11 | allItems,
12 | }) {
13 | const isTabletOrMobile = useMediaQuery({ query: "(max-width: 1224px)" });
14 | return (
15 | <>
16 | {`${label} : `}
17 |
36 | >
37 | );
38 | }
39 |
40 | export default FilterSelectWrap;
41 |
--------------------------------------------------------------------------------
/admin-blog/src/components/HCenterSpin.jsx:
--------------------------------------------------------------------------------
1 | import { LoadingOutlined } from "@ant-design/icons";
2 | import { Spin } from "antd";
3 | import style from "../css/HCenterSpin.module.css";
4 |
5 | const antIcon = (
6 |
12 | );
13 |
14 | function HCenterSpin({verticallyCenter=false}) {
15 | return ;
19 | }
20 |
21 | export default HCenterSpin;
22 |
--------------------------------------------------------------------------------
/admin-blog/src/components/ImageUploadBox.jsx:
--------------------------------------------------------------------------------
1 | import { PlusOutlined } from "@ant-design/icons";
2 | import { Upload } from "antd";
3 |
4 | const uploadButton = (
5 |
6 |
7 |
12 | Upload
13 |
14 |
15 | );
16 |
17 | const defaultUploadProps = {
18 | beforeUpload: (file) => {
19 | return false;
20 | },
21 | maxCount: 1,
22 | name: "file",
23 | listType: "picture-card",
24 | showUploadList: false,
25 | };
26 |
27 | function ImageUploadBox({
28 | imageUrl,
29 | alt,
30 | uploadProps = defaultUploadProps,
31 | onChange = null,
32 | }) {
33 | return (
34 |
35 | {imageUrl ? (
36 |
43 | ) : (
44 | uploadButton
45 | )}
46 |
47 | );
48 | }
49 |
50 | export default ImageUploadBox;
51 |
--------------------------------------------------------------------------------
/admin-blog/src/components/LinkNav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet } from "react-router-dom";
3 | import useRedirect from "../hooks/useRedirect.js";
4 |
5 | function LinkNav(props) {
6 | useRedirect(props.from, "all-links");
7 | return ;
8 | }
9 |
10 | export default LinkNav
--------------------------------------------------------------------------------
/admin-blog/src/components/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Spin } from "antd";
3 | import { LoadingOutlined } from "@ant-design/icons";
4 | import style from "../css/Loading.module.css";
5 |
6 | function Loading() {
7 | const antIcon = ;
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default Loading;
--------------------------------------------------------------------------------
/admin-blog/src/components/ManageNav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet } from "react-router-dom";
3 | import useRedirect from "../hooks/useRedirect.js";
4 |
5 | function ManageNav() {
6 | useRedirect("/manage", "dashboard");
7 | return ;
8 | }
9 |
10 | export default ManageNav
--------------------------------------------------------------------------------
/admin-blog/src/components/MarkDownEditor.jsx:
--------------------------------------------------------------------------------
1 | import "easymde/dist/easymde.min.css";
2 | import "github-markdown-css";
3 | import "katex/dist/katex.min.css";
4 | import { useMemo } from "react";
5 | import ReactDOMServer from "react-dom/server";
6 | import "font-awesome/css/font-awesome.min.css";
7 | import SimpleMDE from "react-simplemde-editor";
8 | import style from "../css/MarkDownEditor.module.css";
9 | import MDComponent from "./MDComponent";
10 |
11 | function MarkDownEditor({
12 | value = null,
13 | onChange = null,
14 | id = "post_content",
15 | autoSave = false,
16 | }) {
17 | const mdeOptions = useMemo(() => {
18 | return {
19 | autoDownloadFontAwesome: false,
20 | autofocus: false,
21 | spellChecker: false,
22 | sideBySideFullscreen: false,
23 | autosave: {
24 | enabled: autoSave,
25 | uniqueId: id,
26 | delay: 60000,
27 | },
28 | showIcons: [
29 | "code",
30 | "strikethrough",
31 | "table",
32 | "horizontal-rule",
33 | "undo",
34 | "redo",
35 | ],
36 | previewRender(value) {
37 | return ReactDOMServer.renderToString(
38 |
45 | );
46 | },
47 | };
48 | }, [autoSave, id]);
49 | return (
50 |
51 | );
52 | }
53 |
54 | export default MarkDownEditor;
55 |
--------------------------------------------------------------------------------
/admin-blog/src/components/MediaNav.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Outlet } from "react-router-dom";
3 | import useRedirect from "../hooks/useRedirect.js";
4 |
5 | function MediaNav(props) {
6 | useRedirect(props.from, "all-images");
7 | return ;
8 | }
9 |
10 | export default MediaNav
--------------------------------------------------------------------------------
/admin-blog/src/components/NewLink.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Form, message as antMessage } from "antd";
2 | import { useEffect, useRef, useState } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import style from "../css/NewLink.module.css";
5 | import { createLink, resetError } from "../features/links/linkSlice";
6 | import LinkForm from "./LinkForm";
7 |
8 | const NewLink = () => {
9 | const dispatch = useDispatch();
10 |
11 | const { isSuccess, isError, message } = useSelector((state) => state.links);
12 |
13 | const formRef = useRef(null);
14 |
15 | const [isImgValid, setIsImgValid] = useState(false);
16 |
17 | useEffect(() => {
18 | if (isError) {
19 | antMessage.error(message);
20 | }
21 | if (isSuccess && message === "成功创建友链") {
22 | antMessage.success(message);
23 | formRef.current.resetFields();
24 | }
25 | return () => {
26 | dispatch(resetError());
27 | };
28 | }, [isError, isSuccess, message, dispatch]);
29 |
30 | const onFinish = (values) => {
31 | //console.log(values);
32 | const linkFormData = new FormData();
33 | linkFormData.append("name", values.link.name);
34 | linkFormData.append("website", values.link.address);
35 | if (values.link.introduction) {
36 | linkFormData.append("description", values.link.introduction);
37 | }
38 | if (values.dragger && isImgValid) {
39 | //console.log("uploaded image: ", values.dragger[0].originFileObj);
40 | const imageFile = values.dragger[0].originFileObj;
41 | linkFormData.append("picture", imageFile);
42 | }
43 | dispatch(createLink(linkFormData));
44 | };
45 |
46 | return (
47 |
55 |
56 |
63 |
64 |
65 | );
66 | };
67 |
68 | export default NewLink;
69 |
--------------------------------------------------------------------------------
/admin-blog/src/components/PanelFooter.jsx:
--------------------------------------------------------------------------------
1 | import { Layout } from "antd";
2 | import React from "react";
3 | import "../css/AdminPanel.css";
4 |
5 | const { Footer } = Layout;
6 |
7 | function PanelFooter() {
8 | return (
9 |
12 | );
13 | }
14 |
15 | export default PanelFooter;
16 |
--------------------------------------------------------------------------------
/admin-blog/src/components/PanelHeader.jsx:
--------------------------------------------------------------------------------
1 | import { DownOutlined, MenuOutlined } from "@ant-design/icons";
2 | import { Avatar, Dropdown, Layout } from "antd";
3 | import React from "react";
4 | import { useDispatch } from "react-redux";
5 | import { useMediaQuery } from "react-responsive";
6 | import { useNavigate } from "react-router-dom";
7 | import "../css/AdminPanel.css";
8 | import { logout, reset } from "../features/auth/authSlice";
9 |
10 | const { Header } = Layout;
11 |
12 | function PanelHeader({ setVisible }) {
13 | const isTabletOrMobile = useMediaQuery({ query: "(max-width: 1224px)" });
14 | const navigate = useNavigate();
15 | const dispatch = useDispatch();
16 |
17 | const showDrawer = () => {
18 | setVisible(true);
19 | };
20 |
21 | //用户下拉菜单点击事件
22 | const handleUserMenuClick = ({ key }) => {
23 | switch (key) {
24 | case "change-password":
25 | navigate("change-password");
26 | break;
27 | case "logout":
28 | dispatch(logout());
29 | dispatch(reset());
30 | break;
31 | default:
32 | navigate("/manage");
33 | }
34 | };
35 |
36 | return (
37 |
41 | {isTabletOrMobile ? (
42 |
43 | ) : null}
44 |
45 | triggerNode.parentNode}
63 | >
64 |
84 |
85 |
86 | );
87 | }
88 |
89 | export default PanelHeader;
90 |
--------------------------------------------------------------------------------
/admin-blog/src/components/PostNav.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Outlet } from "react-router-dom";
3 | import useRedirect from "../hooks/useRedirect.js";
4 |
5 | function PostNav() {
6 | useRedirect("/manage/post", "all-posts");
7 | return ;
8 | }
9 |
10 | export default PostNav;
11 |
--------------------------------------------------------------------------------
/admin-blog/src/components/RequireAuth.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { useLocation, useNavigate } from "react-router-dom";
4 | import { checkJWT, reset } from "../features/auth/authSlice";
5 | import Loading from "../pages/Loading";
6 |
7 | function RequireAuth({ children }) {
8 | const location = useLocation();
9 | const dispatch = useDispatch();
10 | const navigate = useNavigate();
11 | const { isError, isSuccess, message } = useSelector((state) => state.auth);
12 | const [isAuth, setIsAuth] = useState(false);
13 |
14 | useEffect(() => {
15 | dispatch(checkJWT());
16 | }, [dispatch]);
17 |
18 | useEffect(() => {
19 | if (isError) {
20 | console.error("Auth error: ", message);
21 | setIsAuth(false);
22 | navigate("/", { replace: true, state: { from: location } });
23 | dispatch(reset());
24 | }
25 | if (isSuccess) {
26 | setIsAuth(true);
27 | }
28 | }, [dispatch, isError, isSuccess, message, location, navigate]);
29 |
30 | return isAuth ? <>{children}> : ;
31 | }
32 |
33 | export default RequireAuth;
34 |
--------------------------------------------------------------------------------
/admin-blog/src/components/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | function Spinner() {
4 | return (
5 |
8 | )
9 | }
10 |
11 | export default Spinner
--------------------------------------------------------------------------------
/admin-blog/src/components/TagNav.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Outlet } from "react-router-dom";
3 | import useRedirect from "../hooks/useRedirect.js";
4 |
5 | function TagNav() {
6 | useRedirect("/manage/tag", "all-tags");
7 | return ;
8 | }
9 |
10 | export default TagNav;
11 |
--------------------------------------------------------------------------------
/admin-blog/src/components/WordCloud.jsx:
--------------------------------------------------------------------------------
1 | import { Text, TrackballControls } from "@react-three/drei";
2 | import { Canvas, useFrame } from "@react-three/fiber";
3 | import { useMemo, useRef } from "react";
4 | //import * as THREE from "three";
5 | import {Vector3, Quaternion, Spherical} from "three";
6 |
7 |
8 | const defaultColors = [
9 | "BlueViolet",
10 | "GoldenRod",
11 | "Lime",
12 | "Crimson",
13 | "Blue",
14 | "OrangeRed",
15 | ];
16 |
17 | function rotate(L, camera, ctr, speed) {
18 | const vector = ctr.current.target.clone();
19 | const l = new Vector3().subVectors(camera.position, vector).length();
20 | const up = camera.up.clone();
21 | const quaternion = new Quaternion();
22 |
23 | // Zoom correction
24 | camera.translateZ(L - l);
25 |
26 | quaternion.setFromAxisAngle(up, 0.00001 * speed);
27 | camera.position.applyQuaternion(quaternion);
28 | }
29 |
30 | function Word({ children, ctr, speed, cZ, ...props }) {
31 | const fontProps = {
32 | font: "/noto-sans-sc-700.woff",
33 | fontSize: 2.5,
34 | lineHeight: 1,
35 | "material-toneMapped": false,
36 | };
37 | const ref = useRef();
38 |
39 | useFrame(({ camera }) => {
40 | // Make text face the camera
41 | ref.current.quaternion.copy(camera.quaternion);
42 | rotate(cZ, camera, ctr, speed);
43 | });
44 | return ;
45 | }
46 |
47 | function Cloud({
48 | count = 4,
49 | radius = 20,
50 | ctr,
51 | colors,
52 | speed,
53 | cZ,
54 | modifiedWords,
55 | }) {
56 | // Create a count x count words with spherical distribution
57 | const words = useMemo(() => {
58 | const temp = [];
59 | const spherical = new Spherical();
60 | const phiSpan = Math.PI / (count + 1);
61 | const thetaSpan = (Math.PI * 2) / count;
62 | for (let i = 1; i < count + 1; i++)
63 | for (let j = 0; j < count; j++) {
64 | const wordIndex = (i - 1) * count + j;
65 | temp.push([
66 | new Vector3().setFromSpherical(
67 | spherical.set(radius, phiSpan * i, thetaSpan * j)
68 | ),
69 | modifiedWords[wordIndex],
70 | ]);
71 | }
72 |
73 | return temp;
74 | }, [count, radius, modifiedWords]);
75 | return words.map(([pos, word], index) => (
76 |
85 | ));
86 | }
87 |
88 | function getMSquare(n) {
89 | for (let i = 3; i < 30; i++) {
90 | if (n <= i * i) {
91 | return i;
92 | }
93 | }
94 | return 3;
95 | }
96 |
97 | function changeWords(words) {
98 | const wordsN = words.length;
99 | let newWords = words;
100 | const addNumber = getMSquare(wordsN) * getMSquare(wordsN) - wordsN;
101 | for (let i = 0; i < addNumber; i++) {
102 | newWords.push(words[i]);
103 | }
104 | return newWords;
105 | }
106 |
107 | function WordCloud({
108 | colors = defaultColors,
109 | cZ = 30,
110 | radius = 20,
111 | speed = 1,
112 | words,
113 | }) {
114 |
115 | const controlsRef = useRef();
116 | const wordsNumber = useMemo(() => words.length, [words]);
117 | const countCloudT = useMemo(() => getMSquare(wordsNumber), [wordsNumber]);
118 | const modifiedWords = useMemo(() => changeWords(words), [words]);
119 |
120 | return (
121 |
137 | );
138 | }
139 |
140 | export default WordCloud;
141 |
--------------------------------------------------------------------------------
/admin-blog/src/css/AdminPanel.css:
--------------------------------------------------------------------------------
1 | .admin-panel-layout .logo {
2 | height: 32px;
3 | margin: 16px;
4 | }
5 |
6 | .site-layout-sub-header-background {
7 | background: #fff;
8 | box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
9 | padding: 0;
10 | }
11 |
12 | .admin-panel-layout .drawer-trigger {
13 | margin-left: 2rem;
14 | }
15 |
16 | .site-layout-background {
17 | background: #f0f2f5;
18 | }
19 |
20 | .admin-panel-layout .logout-button {
21 | margin-right: 10vw;
22 | }
23 |
24 | .admin-panel-layout .side-box {
25 | position: sticky;
26 | top: 0;
27 | height: 100vh;
28 | }
29 |
30 | .admin-panel-layout .no-style-button {
31 | background: none;
32 | color: inherit;
33 | border: none;
34 | padding: 0;
35 | font: inherit;
36 | outline: inherit;
37 | cursor: pointer;
38 | }
39 |
40 | .admin-panel-layout .user-menu-box {
41 | display: flex;
42 | align-items: center;
43 | margin-right: 5vw;
44 | color: #06223c;
45 | }
46 |
47 | .admin-panel-layout .user-menu-box:hover {
48 | color: #1890ff;
49 | }
50 |
51 | .admin-panel-layout .menu-fold-button {
52 | position: absolute;
53 | bottom: 0rem;
54 | width: 100%;
55 | background: #001c37;
56 | background: transparent;
57 | border: 0px;
58 | border-top: 1px #4d5c6a solid;
59 | padding: 0;
60 | font: inherit;
61 | outline: inherit;
62 | cursor: default;
63 | }
64 |
65 | .admin-panel-layout .trigger {
66 | padding: 30px;
67 | font-size: 18px;
68 | cursor: pointer;
69 | transition: color 0.3s;
70 | color: white;
71 | }
72 |
73 | .admin-panel-layout .trigger:hover {
74 | color: #1890ff;
75 | }
76 |
77 | .admin-panel-layout .logo-svg {
78 | transition: all 0.5s;
79 | }
80 |
81 | .admin-panel-layout .site-layout-sub-header-background {
82 | display: flex;
83 | align-items: center;
84 | justify-content: space-between;
85 | }
86 |
87 | /* 桌面端css */
88 | @media (min-width:1224px) {
89 | .site-layout-sub-header-background {
90 | border-radius: 1rem;
91 | margin: 1.5rem 2rem 0;
92 | box-shadow: none;
93 | }
94 |
95 | .admin-panel-layout .site-layout-sub-header-background {
96 | display: flex;
97 | align-items: center;
98 | justify-content: right;
99 | }
100 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/AllImages.module.css:
--------------------------------------------------------------------------------
1 | .all-images-box {
2 | padding: 1rem;
3 | }
4 |
5 | .small-title {
6 | font-size: 0.8rem;
7 | margin-left: 0.5rem;
8 | }
9 |
10 | .divider {
11 | margin-top: 0.5rem;
12 | }
13 |
14 | .move-to-right {
15 | display: flex;
16 | justify-content: right;
17 | padding-right: 1rem;
18 | }
19 |
20 | .grid-wrapper {
21 | display: grid;
22 | grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
23 | justify-content: space-evenly;
24 | gap: 1rem;
25 | margin-bottom: 2rem;
26 | }
27 |
28 | .title-label {
29 | display: block;
30 | margin-top: 1rem;
31 | }
32 |
33 | .description-label {
34 | display: block;
35 | margin-top: 1rem;
36 | }
37 |
38 | @media (min-width: 1224px) {
39 | .grid-wrapper {
40 | display: grid;
41 | grid-template-columns: repeat(auto-fit, 290px);
42 | }
43 | .all-images-box {
44 | margin: 0 2rem;
45 | background-color: #ffffff;
46 | border-radius: 1rem;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/admin-blog/src/css/AllItems.module.css:
--------------------------------------------------------------------------------
1 | .edit-button {
2 | margin-right: 1rem;
3 | }
4 |
5 |
6 | .edit-button-desktop-box {
7 | color: rgb(62, 159, 255);
8 | padding: 0;
9 | margin: 0;
10 | display: inherit;
11 | }
12 |
13 | .edit-button-desktop-box:hover {
14 | color: rgb(0, 128, 255);
15 | }
16 |
17 | .item-table {
18 | margin: 0;
19 | }
20 |
21 | .no-style-button {
22 | background: none;
23 | color: inherit;
24 | border: none;
25 | padding: 0;
26 | font: inherit;
27 | outline: inherit;
28 | cursor: pointer;
29 | }
30 |
31 | .hover-blue:hover {
32 | color: #6b9dd6;
33 | }
34 |
35 | .delete-button-mobile {
36 | color: rgb(255, 96, 96);
37 | }
38 |
39 | @media (min-width: 1224px) {
40 | .item-table {
41 | margin: 0rem 2rem;
42 | }
43 |
44 | .delete-button-desktop {
45 | margin-left: 2rem;
46 | border-radius: 5px;
47 | padding: 0.5rem;
48 | color: white;
49 | background-color: rgb(255, 96, 96);
50 | }
51 |
52 | .delete-button-desktop:hover {
53 | color: white;
54 | background-color: rgb(255, 57, 57);
55 | }
56 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/AllLinks.module.css:
--------------------------------------------------------------------------------
1 | .links-box {
2 | display: grid;
3 | grid-template-columns: 1fr;
4 | grid-gap: 0.5rem;
5 | margin: 0 0.5rem;
6 | }
7 |
8 | .card-body {
9 | border: 1px solid red;
10 | }
11 |
12 | .card-action {
13 | border: 1px solid purple;
14 | }
15 |
16 | .menu-button {
17 | color: gray;
18 | font-size: 22px;
19 | }
20 |
21 | .menu-button:hover {
22 | color: black;
23 | }
24 |
25 | .spin-center {
26 | width: 100%;
27 | text-align: center;
28 |
29 | }
30 |
31 |
32 | @media (min-width:1224px) {
33 | .links-box {
34 | grid-template-columns: 1fr 1fr;
35 | grid-gap: 1.5em;
36 | margin: 0 2rem;
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/BlogSetting.module.css:
--------------------------------------------------------------------------------
1 | .main-box {
2 | margin:1rem 1.5rem 0 1.5rem;
3 | }
4 |
5 | @media (min-width:1224px) {
6 | .main-box {
7 | background-color: #fff;
8 | border-radius: 1rem;
9 | margin: 0 2rem;
10 | padding: 2rem 5rem;
11 | }
12 |
13 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/EditPostForm.module.css:
--------------------------------------------------------------------------------
1 | .new-form-body {
2 | margin:1rem 1.5rem 0 1.5rem;
3 | }
4 |
5 | .second-button {
6 | margin-left: 1.5rem;
7 | }
8 |
9 | .link-top-box {
10 | display: flex;
11 | justify-content: right;
12 | margin-bottom: 2rem;
13 | margin-right: 0;
14 | }
15 |
16 | @media (min-width:1224px) {
17 | .new-form-body {
18 | background-color: #fff;
19 | border-radius: 1rem;
20 | margin: 0 2rem;
21 | padding: 2rem 5rem;
22 | }
23 |
24 | .link-top-box {
25 | margin-right: 5rem;
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/HCenterSpin.module.css:
--------------------------------------------------------------------------------
1 | .spin-center {
2 | width: 100%;
3 | text-align: center;
4 | }
5 |
6 | .spin-center.v-center {
7 | height: 100%;
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | }
12 |
--------------------------------------------------------------------------------
/admin-blog/src/css/Loading.module.css:
--------------------------------------------------------------------------------
1 | .horizontal-vertical-center {
2 | display: flex;
3 | width: 100%;
4 | height: 100vh;
5 | justify-content:center;
6 | align-items:center;
7 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/Login.module.css:
--------------------------------------------------------------------------------
1 | .login-page-box {
2 | display: flex;
3 | height: 100vh;
4 | justify-content: center;
5 | align-items: center;
6 | background-repeat: no-repeat;
7 | background-attachment: fixed;
8 | background-size: cover;
9 | background-position: top;
10 | background-image: url(../images/slanted-gradient_blue.svg);
11 | }
12 |
13 | .blurred-box {
14 | position: relative;
15 | width: 80vw;
16 | background: inherit;
17 | border-radius: 1.5rem;
18 | overflow: hidden;
19 | box-shadow: inset 0 0 0 200px hsla(0,0%,100%,.05);
20 | }
21 |
22 | .blurred-box::after {
23 | content: '';
24 | width: 95vw;
25 | height: 50vh;
26 | background: inherit;
27 | position: absolute;
28 | left: -55px;
29 | right: 0px;
30 | top: -55px;
31 | bottom: 0;
32 | filter: blur(25px);
33 |
34 | }
35 |
36 | .form-box {
37 | position: relative;
38 | width: 80vw;
39 | display: flex;
40 | flex-direction: column;
41 | align-items: center;
42 | padding: 3rem 2rem 1rem 2rem;
43 | border-radius: 1.5rem;
44 | z-index: 1;
45 | }
46 |
47 | .user-name {
48 | margin-top: 1rem;
49 | margin-bottom: 1rem;
50 | color: white;
51 | font-family: Arial, Helvetica;
52 | letter-spacing: 0.02em;
53 | font-weight: 400;
54 | -webkit-font-smoothing: antialiased;
55 | font-size: 1.1rem;
56 | }
57 |
58 | .input-password>* {
59 | border-radius: 0;
60 | }
61 |
62 | .input-password {
63 | opacity: 0.4;
64 | }
65 |
66 | .label-password {
67 | color: white;
68 | }
69 |
70 |
71 |
72 | .submit-button {
73 | background-image: linear-gradient(to right, #457fca 0%, #5691c8 51%, #457fca 100%);
74 | margin: 0.3rem;
75 | padding: 0.5rem 1.5rem;
76 | text-align: center;
77 | text-transform: uppercase;
78 | transition: 0.5s;
79 | background-size: 200% auto;
80 | color: white;
81 | border: none;
82 | cursor: pointer;
83 | border-radius: 10px;
84 | display: block;
85 | }
86 |
87 |
88 |
89 | @media (min-width:1224px) {
90 | .blurred-box {
91 | width: 30vw;
92 | box-shadow: rgba(0, 0, 0, 0.15) 0px 2px 8px;
93 | }
94 |
95 | .blurred-box::after {
96 | width: 40vw;
97 | }
98 |
99 | .form-box {
100 | width: 30vw;
101 | padding: 3rem 0 1rem 0;
102 | }
103 |
104 |
105 | .submit-button:hover {
106 | background-position: right center;
107 | color: #fff;
108 | text-decoration: none;
109 | }
110 |
111 |
112 |
113 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/MarkDownEditor.module.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .preview-body {
4 | padding:2rem;
5 | margin:0;
6 | background-color: #fafafa;
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/admin-blog/src/css/NewForm.module.css:
--------------------------------------------------------------------------------
1 | .new-form-box {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: stretch;
5 | max-width: 40rem;
6 | padding: 0 1rem;
7 | margin: 0 auto;
8 |
9 | }
10 |
11 | .submit-button {
12 | position: absolute;
13 | left: 40%;
14 | }
15 |
16 | .submit-button-edit {
17 | position: absolute;
18 | left: 20%;
19 | }
20 |
21 | .cancel-button {
22 | position: absolute;
23 | left: 55%
24 | }
25 |
26 | .spin-center {
27 | width: 100%;
28 | text-align: center;
29 |
30 | }
31 |
32 | .warning-info {
33 | color: red;
34 | text-align: center;
35 | }
36 |
37 | .input-password>* {
38 | border-radius: 0;
39 | }
40 |
41 | @media (min-width: 1224px) {
42 | .new-form-box {
43 | max-width: 50rem;
44 | padding-top: 2rem;
45 | padding-bottom: 2rem;
46 | display: block;
47 | }
48 |
49 | .main-box {
50 | background-color: #ffffff;
51 | border-radius: 1rem;
52 | margin: 0 2rem;
53 | }
54 |
55 | .submit-button-box {
56 | text-align: center;
57 | }
58 |
59 | .submit-button {
60 | position: relative;
61 | left: 0;
62 |
63 | }
64 |
65 | .warning-info {
66 | margin-bottom: 1rem;
67 | }
68 |
69 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/NewItem.module.css:
--------------------------------------------------------------------------------
1 | .new-item-submit-button {
2 | width: 100%;
3 | text-align: center;
4 | }
5 |
6 | .add-field-button {
7 | width: auto;
8 | text-align: center;
9 | }
10 |
11 | .font-item-outlined {
12 | font-size: 3rem;
13 | width: 100%;
14 | text-align: center;
15 | margin-bottom: 2rem;
16 | margin-top: 2rem;
17 | color: green;
18 | }
19 |
20 | .input-box>* {
21 | border-radius: 0px;
22 | }
23 |
24 | @media (min-width:1224px) {
25 | .new-item-box {
26 | background-color: #ffffff;
27 | border-radius: 1rem;
28 | margin: 0 2rem;
29 | padding: 0 0 2rem 0;
30 | }
31 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/NewLink.module.css:
--------------------------------------------------------------------------------
1 | .new-link-form {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: stretch;
5 | max-width: 40rem;
6 | padding: 0 1rem;
7 | margin: 0 auto;
8 |
9 | }
10 |
11 | .submit-button {
12 | position: absolute;
13 | left: 40%;
14 | }
15 |
16 | .submit-button-edit {
17 | position: absolute;
18 | left: 20%;
19 | }
20 |
21 | .cancel-button {
22 | position: absolute;
23 | left: 55%
24 | }
25 |
26 | .spin-center {
27 | width: 100%;
28 | text-align: center;
29 |
30 | }
31 |
32 | @media (min-width: 1224px) {
33 | .new-link-form {
34 |
35 | padding: 2rem 15rem 2rem 0;
36 | width: auto;
37 | margin: 0 2rem;
38 | background-color: #ffffff;
39 | border-radius: 1rem;
40 | max-width: 100%;
41 | }
42 |
43 | .submit-button {
44 | width: auto;
45 | left: 40rem;
46 | }
47 |
48 | .submit-button-edit {
49 | width: auto;
50 | left: 35rem;
51 | }
52 |
53 | .cancel-button {
54 | left: 45rem;
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/PostComments.module.css:
--------------------------------------------------------------------------------
1 | .comments-header {
2 | display: block;
3 | }
4 |
5 | .post-comments-box {
6 | margin: 0 0.5rem;
7 | padding: 1rem;
8 | background-color: #ffffff;
9 | border-radius: 1rem;
10 | }
11 |
12 | .pagination {
13 | display: flex;
14 | justify-content: center;
15 | flex-wrap: wrap;
16 | margin: 1rem auto 0rem auto;
17 |
18 | }
19 |
20 | .checkbox-group {
21 | width: 100%;
22 | }
23 |
24 | /* antd5中Checkbox Group为inline-flex而antd4为inline-block,为了使.source-right的marginLeft auto生效 */
25 | .checkbox-group > div {
26 | width: 100%;
27 | }
28 |
29 | .delete-selected-icon-button {
30 | display: inline-block;
31 | border: none;
32 | color: red;
33 | font-size: 1.1rem;
34 | background-color: transparent;
35 | }
36 |
37 | .delete-selected-icon-button:disabled {
38 | background-color: transparent;
39 | color: lightgrey;
40 | }
41 |
42 | .comment-box {
43 | margin-left: 0.5rem;
44 | word-break: break-all;
45 | }
46 |
47 | .divider {
48 | margin-top: 0.5rem;
49 | margin-bottom: 0.5rem;
50 | }
51 |
52 | .source-right {
53 | display: block;
54 | margin: 0rem;
55 | margin-top: 1.5rem;
56 | color: rgb(124, 124, 124);
57 | }
58 |
59 | .delete-comment-button-mobile {
60 | display: block;
61 | margin-left: auto;
62 | margin-right: 1rem;
63 | border: none;
64 | border-radius: 1rem;
65 | color: rgb(255, 255, 255);
66 | background-image: linear-gradient(to right, #EB3349 0%, #F45C43 51%, #EB3349 100%);
67 | background-size: 200% auto;
68 | padding: 0.25rem 0.75rem;
69 | transition: 0.5s;
70 | box-shadow: 0 0 20px #eee;
71 | border-radius: 10px;
72 |
73 | }
74 |
75 | .post-select {
76 | display: block;
77 | width: 15rem;
78 | margin: 0rem 0.3rem 1rem auto;
79 |
80 | }
81 |
82 | @media (min-width:1224px) {
83 |
84 | .comments-header {
85 | display: flex;
86 | justify-content: left;
87 | align-items: center;
88 | padding-right: 5rem;
89 | }
90 |
91 | .comments-header>*+* {
92 | margin-left: 1rem;
93 | }
94 |
95 | .comments-header>.post-select {
96 | margin-left: auto;
97 | }
98 |
99 |
100 |
101 | .post-comments-box {
102 | margin: 0 2rem;
103 | padding: 2rem;
104 | }
105 |
106 | .pagination {
107 | display: flex;
108 | justify-content: right;
109 | margin: 1rem 5rem 0rem auto;
110 | }
111 |
112 | .comment-wrap {
113 | display: flex;
114 | justify-content: left;
115 | align-items: center;
116 | padding-right: 5rem;
117 |
118 | }
119 |
120 | .comment-box {
121 | width: 30vw;
122 |
123 | }
124 |
125 | .source-right {
126 | margin-left: auto;
127 | width: 20rem;
128 | }
129 |
130 | .delete-comment-button {
131 | align-self: end;
132 | border: 0;
133 | color: rgba(255, 66, 66, 0.708);
134 | background-color: inherit;
135 | cursor: pointer;
136 | }
137 |
138 | .delete-comment-button:hover {
139 | color: red;
140 | }
141 |
142 | .post-select {
143 | width: 20rem;
144 | margin: 0;
145 | display: inherit;
146 | }
147 |
148 | .delete-comment-button {
149 | visibility: hidden;
150 | }
151 |
152 | .comment-wrap:hover .delete-comment-button {
153 | visibility: visible;
154 | }
155 | }
--------------------------------------------------------------------------------
/admin-blog/src/css/RecentComments.module.css:
--------------------------------------------------------------------------------
1 | .recent-comment-box {
2 | display: grid;
3 | grid-template-columns: minmax(0, 1fr);
4 | grid-gap: 0.5rem;
5 | margin: 0 0.5rem;
6 | }
7 |
8 | .delete-button {
9 | background: none;
10 | color: rgb(255, 0, 0);
11 | border: none;
12 | padding: 0;
13 | font: inherit;
14 | outline: inherit;
15 | cursor: pointer;
16 | font-size: 1rem;
17 | }
18 |
19 | .source-delete-box {
20 | display: flex;
21 | justify-content: space-between;
22 | }
23 |
24 | .divider {
25 | margin-top: 0rem;
26 | }
27 |
28 | @media (min-width: 1224px) {
29 | /* minmax(0, 1fr)可以防止网格布局中的元素溢出 */
30 | .recent-comment-box {
31 | display: grid;
32 | grid-template-columns: repeat(3, minmax(0, 1fr));
33 | grid-gap: 1rem;
34 | margin: 0 2rem;
35 | }
36 |
37 | .delete-button {
38 | background: none;
39 | color: rgb(241, 54, 54);
40 | border: none;
41 | padding: 0;
42 | font: inherit;
43 | outline: inherit;
44 | cursor: pointer;
45 | }
46 |
47 | .delete-button:hover {
48 | color: red;
49 | }
50 |
51 | .source-delete-box {
52 | display: flex;
53 | justify-content: space-between;
54 | }
55 |
56 | .comment-content {
57 | height: 4rem;
58 | overflow: auto;
59 | }
60 |
61 | .comment-content::-webkit-scrollbar {
62 | width: 6px;
63 | height: 6px;
64 | background-color: transparent;
65 | }
66 |
67 | .comment-content::-webkit-scrollbar-track {
68 | border-radius: 1rem;
69 | background-color: #e7e7e7;
70 | }
71 |
72 | .comment-content::-webkit-scrollbar-thumb {
73 | border-radius: 1rem;
74 | background-color: rgb(175, 175, 175);
75 | }
76 |
77 | .comment-content::-webkit-scrollbar-thumb:hover {
78 | background-color: rgb(145, 145, 145);
79 | width: 0.5rem;
80 | }
81 |
82 | .divider {
83 | margin-top: 0rem;
84 | }
85 |
86 | .delete-button {
87 | visibility: hidden;
88 | }
89 |
90 | .card-box:hover .delete-button {
91 | visibility: visible;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/admin-blog/src/css/ThemeSetting.module.css:
--------------------------------------------------------------------------------
1 | .out-box {
2 | background-color: white;
3 | }
--------------------------------------------------------------------------------
/admin-blog/src/features/auth/authService.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const API_URL = "/api/users/";
4 |
5 | const register = async (userData) => {
6 | const response = await axios.post(API_URL, userData);
7 |
8 | if (response.data) {
9 | localStorage.setItem("user", JSON.stringify(response.data));
10 | }
11 |
12 | //console.log("response.data", response.data);
13 |
14 | return response.data;
15 | };
16 |
17 | const login = async (userData) => {
18 | const response = await axios.post(API_URL + "login", userData);
19 |
20 | if (response.data) {
21 | localStorage.setItem("user", JSON.stringify(response.data));
22 | }
23 |
24 | return response.data;
25 | };
26 |
27 | const getUserData = async (token) => {
28 | const config = {
29 | headers: {
30 | Authorization: `Bearer ${token}`,
31 | },
32 | };
33 |
34 | const response = await axios.get(API_URL + "me", config);
35 | return response.data;
36 | };
37 |
38 | const logout = () => {
39 | localStorage.removeItem("user");
40 | //console.log("清除localStorage中的user");
41 | };
42 |
43 | const changePassword = async (token, passwordData) => {
44 | const config = {
45 | headers: {
46 | Authorization: `Bearer ${token}`,
47 | },
48 | };
49 | const response = await axios.put(
50 | API_URL + "update-password",
51 | passwordData,
52 | config
53 | );
54 | return response.data;
55 | };
56 |
57 | const authService = {
58 | register,
59 | logout,
60 | login,
61 | getUserData,
62 | changePassword,
63 | };
64 |
65 | export default authService;
66 |
--------------------------------------------------------------------------------
/admin-blog/src/features/comments/commentService.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const API_URL = '/api/comments/'
4 |
5 |
6 |
7 | const getComments = async (token)=>{
8 | const config = {
9 | headers: {
10 | Authorization:`Bearer ${token}`
11 | }
12 | }
13 |
14 | const response = await axios.get(API_URL+'all', config)
15 |
16 | return response.data
17 |
18 | }
19 |
20 | const deleteComment = async (commentId, token)=>{
21 | const config = {
22 | headers: {
23 | Authorization:`Bearer ${token}`
24 | }
25 | }
26 |
27 | const response = await axios.delete(API_URL+commentId, config)
28 |
29 | return response.data
30 |
31 | }
32 |
33 | const updateComment = async (commentId, commentData, token)=>{
34 |
35 | commentData.commentId=undefined;
36 | const config = {
37 | headers: {
38 | Authorization:`Bearer ${token}`,
39 |
40 | }
41 | }
42 |
43 | const response = await axios.put(API_URL+commentId, commentData ,config)
44 |
45 | return response.data
46 |
47 | }
48 |
49 |
50 |
51 | const commentService = {
52 |
53 | getComments,
54 | deleteComment,
55 | updateComment
56 | }
57 |
58 | export default commentService;
--------------------------------------------------------------------------------
/admin-blog/src/features/links/linkService.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const API_URL = '/api/links/'
4 |
5 | const createLink = async (linkData, token)=>{
6 | const config = {
7 | headers: {
8 | Authorization:`Bearer ${token}`,
9 | 'content-type': 'multipart/form-data'
10 | }
11 | }
12 |
13 | const response = await axios.post(API_URL, linkData, config)
14 |
15 | return response.data
16 |
17 | }
18 |
19 | const getLinks = async (token)=>{
20 | const config = {
21 | headers: {
22 | Authorization:`Bearer ${token}`
23 | }
24 | }
25 |
26 | const response = await axios.get(API_URL, config)
27 |
28 | return response.data
29 |
30 | }
31 |
32 | const deleteLink = async (linkId, token)=>{
33 | const config = {
34 | headers: {
35 | Authorization:`Bearer ${token}`
36 | }
37 | }
38 |
39 | const response = await axios.delete(API_URL+linkId, config)
40 |
41 | return response.data
42 |
43 | }
44 |
45 | const updateLink = async (linkId, linkData, token)=>{
46 |
47 | linkData.delete('linkId')
48 | const config = {
49 | headers: {
50 | Authorization:`Bearer ${token}`,
51 | 'content-type': 'multipart/form-data'
52 | }
53 | }
54 |
55 | const response = await axios.put(API_URL+linkId, linkData ,config)
56 |
57 | return response.data
58 |
59 | }
60 |
61 |
62 |
63 | const linkService = {
64 | createLink,
65 | getLinks,
66 | deleteLink,
67 | updateLink
68 | }
69 |
70 | export default linkService;
--------------------------------------------------------------------------------
/admin-blog/src/features/posts/postService.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const API_URL = '/api/posts/'
4 |
5 | const createPost = async (postData, token)=>{
6 | const config = {
7 | headers: {
8 | Authorization:`Bearer ${token}`,
9 | 'content-type': 'multipart/form-data'
10 | }
11 | }
12 |
13 | const response = await axios.post(API_URL, postData, config)
14 |
15 | return response.data
16 |
17 | }
18 |
19 | const getPosts = async (token)=>{
20 | const config = {
21 | headers: {
22 | Authorization:`Bearer ${token}`
23 | }
24 | }
25 |
26 | const response = await axios.get(API_URL, config)
27 |
28 | return response.data
29 |
30 | }
31 |
32 | const deletePost = async (postId, token)=>{
33 | const config = {
34 | headers: {
35 | Authorization:`Bearer ${token}`
36 | }
37 | }
38 |
39 | const response = await axios.delete(API_URL+postId, config)
40 |
41 | return response.data
42 |
43 | }
44 |
45 | const updatePost = async (postId, postData, token)=>{
46 |
47 | postData.delete('postId')
48 | const config = {
49 | headers: {
50 | Authorization:`Bearer ${token}`,
51 | 'content-type': 'multipart/form-data'
52 | }
53 | }
54 |
55 | const response = await axios.put(API_URL+postId, postData ,config)
56 |
57 | return response.data
58 |
59 | }
60 |
61 | const updateCategory = async (categoryData, token)=>{
62 | const config = {
63 | headers: {
64 | Authorization:`Bearer ${token}`,
65 | 'content-type': 'multipart/form-data'
66 | }
67 | }
68 | const response = await axios.put(API_URL+'category', categoryData, config)
69 | return response.data
70 | }
71 |
72 | const updateTag = async (tagData, token)=>{
73 | const config = {
74 | headers: {
75 | Authorization:`Bearer ${token}`,
76 | 'content-type': 'multipart/form-data'
77 | }
78 | }
79 | const response = await axios.put(API_URL+'tag', tagData, config)
80 | return response.data
81 | }
82 |
83 | const deleteTag = async (tagData, token)=>{
84 | const config = {
85 | headers: {
86 | Authorization:`Bearer ${token}`,
87 | 'content-type': 'multipart/form-data'
88 | },
89 | data: tagData
90 | }
91 | const response = await axios.delete(API_URL+'tag', config)
92 | return response.data
93 | }
94 |
95 | const getOnePost = async (postId, token)=>{
96 | const config = {
97 | headers: {
98 | Authorization:`Bearer ${token}`
99 | }
100 | }
101 |
102 | const response = await axios.get(API_URL+postId, config)
103 |
104 | return response.data
105 |
106 | }
107 |
108 |
109 |
110 | const postService = {
111 | createPost,
112 | getPosts,
113 | deletePost,
114 | updatePost,
115 | updateCategory,
116 | updateTag,
117 | deleteTag,
118 | getOnePost,
119 | }
120 |
121 | export default postService;
--------------------------------------------------------------------------------
/admin-blog/src/features/profile/profileService.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const API_URL = '/api/profile/'
4 |
5 |
6 | const getProfile = async (token)=>{
7 | const config = {
8 | headers: {
9 | Authorization:`Bearer ${token}`
10 | }
11 | }
12 |
13 | const response = await axios.get(API_URL, config)
14 |
15 | return response.data
16 |
17 | }
18 |
19 | const updateProfile = async (profileId, profileData, token)=>{
20 |
21 | profileData.delete('profileId')
22 | const config = {
23 | headers: {
24 | Authorization:`Bearer ${token}`,
25 | 'content-type': 'multipart/form-data'
26 | }
27 | }
28 |
29 | const response = await axios.put(API_URL+profileId, profileData ,config)
30 |
31 | return response.data
32 |
33 | }
34 |
35 |
36 |
37 | const profileService = {
38 | getProfile,
39 | updateProfile
40 | }
41 |
42 | export default profileService;
--------------------------------------------------------------------------------
/admin-blog/src/features/profile/profileSlice.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
2 | import profileService from "./profileService";
3 |
4 | const initialState = {
5 | profile: [],
6 | isError: false,
7 | isLoading: false,
8 | isLoadEnd: false,
9 | isSuccess: false,
10 | message: "",
11 | };
12 |
13 |
14 | export const getProfile = createAsyncThunk(
15 | "profile/get",
16 | async (_, thunkAPI) => {
17 | try {
18 | const token = thunkAPI.getState().auth.user.token;
19 | return await profileService.getProfile(token);
20 | } catch (error) {
21 | const message =
22 | (error.response &&
23 | error.response.data &&
24 | error.response.data.message) ||
25 | error.message ||
26 | error.toString();
27 | return thunkAPI.rejectWithValue(message);
28 | }
29 | }
30 | );
31 |
32 |
33 |
34 | export const updateProfile = createAsyncThunk(
35 | "profile/update",
36 | async (profileData, thunkAPI) => {
37 | try {
38 | const token = thunkAPI.getState().auth.user.token;
39 | return await profileService.updateProfile(profileData.get('profileId'),profileData,token);
40 | } catch (error) {
41 | const message =
42 | (error.response &&
43 | error.response.data &&
44 | error.response.data.message) ||
45 | error.message ||
46 | error.toString();
47 | return thunkAPI.rejectWithValue(message);
48 | }
49 | }
50 | );
51 |
52 | export const profileSlice = createSlice({
53 | name: "profile",
54 | initialState,
55 | reducers: {
56 | reset: (state) => initialState,
57 | resetError: (state)=>{
58 | state.isError = false;
59 | state.message = "";
60 | },
61 | },
62 | extraReducers: (builder) => {
63 | builder
64 | .addCase(getProfile.pending, (state) => {
65 | state.isLoading = true;
66 | state.isLoadEnd = false;
67 | })
68 | .addCase(getProfile.fulfilled, (state, action) => {
69 | state.isLoading = false;
70 | state.isSuccess = true;
71 | state.profile = action.payload;
72 | state.message = "";
73 | state.isLoadEnd = true;
74 | })
75 | .addCase(getProfile.rejected, (state, action) => {
76 | state.isLoading = false;
77 | state.isError = true;
78 | state.message = action.payload;
79 | state.isLoadEnd = false;
80 | })
81 | .addCase(updateProfile.pending, (state) => {
82 | state.isLoading = true;
83 | })
84 | .addCase(updateProfile.fulfilled, (state, action) => {
85 | state.isLoading = false;
86 | state.isSuccess = true;
87 | state.profile = [action.payload];
88 | state.message = "设置已更改";
89 | })
90 | .addCase(updateProfile.rejected, (state, action) => {
91 | state.isLoading = false;
92 | state.isError = true;
93 | state.isSuccess = false;
94 | state.message = action.payload;
95 | });
96 | },
97 | });
98 |
99 | export const { reset, resetError } = profileSlice.actions;
100 | export default profileSlice.reducer;
101 |
--------------------------------------------------------------------------------
/admin-blog/src/hooks/useColumnSearch.js:
--------------------------------------------------------------------------------
1 | import { SearchOutlined } from "@ant-design/icons";
2 | import { Button, Input, Space } from "antd";
3 | import { useState } from "react";
4 |
5 | export default function useColumnSearch(inputPlaceholder, renderColumn) {
6 | const [searchText, setSearchText] = useState("");
7 | const [searchedColumn, setSearchedColumn] = useState("");
8 |
9 | const handleSearch = (selectedKeys, confirm, dataIndex) => {
10 | confirm();
11 | setSearchText(selectedKeys[0]);
12 | setSearchedColumn(dataIndex);
13 | };
14 |
15 | const handleReset = (clearFilters) => {
16 | clearFilters();
17 | setSearchText("");
18 | };
19 | let searchInput;
20 |
21 | const getColumnSearchProps = (dataIndex) => ({
22 | filterDropdown: ({
23 | setSelectedKeys,
24 | selectedKeys,
25 | confirm,
26 | clearFilters,
27 | }) => (
28 |
29 | {
31 | searchInput = node;
32 | }}
33 | placeholder={inputPlaceholder}
34 | value={selectedKeys[0]}
35 | onChange={(e) =>
36 | setSelectedKeys(e.target.value ? [e.target.value] : [])
37 | }
38 | onPressEnter={() => handleSearch(selectedKeys, confirm, dataIndex)}
39 | style={{ marginBottom: 8, display: "block" }}
40 | />
41 |
42 |
51 |
58 |
69 |
70 |
71 | ),
72 | filterIcon: (filtered) => (
73 |
74 | ),
75 | onFilter: (value, record) =>
76 | record[dataIndex]
77 | ? record[dataIndex]
78 | .toString()
79 | .toLowerCase()
80 | .includes(value.toLowerCase())
81 | : "",
82 | onFilterDropdownOpenChange: (visible) => {
83 | if (visible) {
84 | setTimeout(() => searchInput.select(), 100);
85 | }
86 | },
87 | render: (text) => renderColumn(text, dataIndex, searchedColumn, searchText),
88 | });
89 |
90 | return getColumnSearchProps;
91 | }
92 |
--------------------------------------------------------------------------------
/admin-blog/src/hooks/useGetData.js:
--------------------------------------------------------------------------------
1 | import { message as antMessage } from "antd";
2 | import { useEffect } from "react";
3 | import { useDispatch } from "react-redux";
4 |
5 | export default function useGetData(getData, reset, isError, message, resetError) {
6 | const dispatch = useDispatch();
7 | useEffect(() => {
8 | dispatch(getData());
9 | return () => {
10 | dispatch(reset());
11 | };
12 | }, [dispatch,getData,reset]);
13 |
14 |
15 | useEffect(() => {
16 | if (isError) {
17 | antMessage.error(message);
18 | }
19 | return ()=>{
20 | dispatch(resetError());
21 | }
22 | }, [isError, message, dispatch, resetError]);
23 | }
24 |
--------------------------------------------------------------------------------
/admin-blog/src/hooks/usePrevious.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from "react";
2 |
3 | export default function usePrevious(value) {
4 | const ref = useRef();
5 | useEffect(() => {
6 | ref.current = value;
7 | });
8 | return ref.current;
9 | }
10 |
--------------------------------------------------------------------------------
/admin-blog/src/hooks/useRedirect.js:
--------------------------------------------------------------------------------
1 | import { useNavigate, useLocation } from "react-router-dom";
2 | import { useEffect } from "react";
3 | export default function useRedirect(from, toRelative) {
4 | const navigate = useNavigate();
5 | const location = useLocation();
6 | useEffect(() => {
7 | if (location.pathname === from) {
8 | navigate(toRelative, { replace: true });
9 | }
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/admin-blog/src/images/check-success-svgrepo-com.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
52 |
--------------------------------------------------------------------------------
/admin-blog/src/images/copy-svgrepo-com.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
--------------------------------------------------------------------------------
/admin-blog/src/images/slanted-gradient_blue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admin-blog/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 | [data="highlight"] {
16 | background-color: #353b45;
17 | background-color: black;
18 | background-color: #282c34;
19 | display: block;
20 | position: relative;
21 | left:-5px;
22 | border-left: 5px solid skyblue;
23 |
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/admin-blog/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from "react-dom/client";
3 | import { Provider } from "react-redux";
4 | import { store } from "./app/store";
5 | import App from "./App";
6 | import "./index.css";
7 |
8 | import { App as AntdApp } from "antd";
9 |
10 | import zhCN from "antd/locale/zh_CN";
11 | import { ConfigProvider } from "antd";
12 | import dayjs from "dayjs";
13 | import "dayjs/locale/zh-cn";
14 | dayjs.locale("zh-cn");
15 |
16 |
17 |
18 | const container = document.getElementById("root");
19 | const root = createRoot(container);
20 |
21 | root.render(
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 |
33 | // If you want to start measuring performance in your app, pass a function
34 | // to log results (for example: reportWebVitals(console.log))
35 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
36 | //reportWebVitals(console.log);
37 |
--------------------------------------------------------------------------------
/admin-blog/src/pages/Loading.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Spin } from "antd";
3 | import { LoadingOutlined } from "@ant-design/icons";
4 | import style from "../css/Loading.module.css";
5 |
6 | function Loading() {
7 | const antIcon = ;
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default Loading;
16 |
--------------------------------------------------------------------------------
/admin-blog/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import { Form, Input } from "antd";
2 | import React, { useEffect } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { useLocation, useNavigate } from "react-router-dom";
5 | import { toast } from "react-toastify";
6 | import style from "../css/Login.module.css";
7 | import { checkJWT, login, reset } from "../features/auth/authSlice";
8 |
9 | function Login() {
10 | const navigate = useNavigate();
11 | const dispatch = useDispatch();
12 | const location = useLocation();
13 |
14 | const { user, isError, isSuccess, message } = useSelector(
15 | (state) => state.auth
16 | );
17 |
18 | useEffect(() => {
19 | //console.log('user before : ',user)
20 | dispatch(checkJWT());
21 | //console.log('user after : ',user)
22 | }, [dispatch]);
23 |
24 | const from = location.state?.from?.pathname || "/manage";
25 |
26 | useEffect(() => {
27 | if (isError) {
28 | toast.error(message);
29 | dispatch(reset());
30 | }
31 |
32 | if (isSuccess) {
33 | //console.log('isSuccess; ',isSuccess);
34 | //console.log('user navigate : ',user)
35 | //navigate("/manage",{ replace: true });
36 | navigate(from, { replace: true });
37 | }
38 | }, [user, isError, isSuccess, message, navigate, dispatch, from]);
39 |
40 | const onFinish = (values) => {
41 | //console.log("Success:", values);
42 | const userDate = {
43 | password: values.password,
44 | };
45 | dispatch(login(userDate));
46 | };
47 |
48 | const onFinishFailed = (errorInfo) => {
49 | //console.log("Failed:", errorInfo);
50 | toast.error(errorInfo);
51 | };
52 |
53 | return (
54 | <>
55 |
56 |
57 |
{`密码 :`}
77 | }
78 | name="password"
79 | colon={false}
80 | rules={[
81 | {
82 | required: true,
83 | message: "请输入密码!",
84 | },
85 | ]}
86 | >
87 |
88 |
89 |
90 |
96 |
99 |
100 |
101 |
102 |
103 | >
104 | );
105 | }
106 |
107 | export default Login;
108 |
--------------------------------------------------------------------------------
/admin-blog/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/admin-blog/src/setupProxy.js:
--------------------------------------------------------------------------------
1 | const { createProxyMiddleware } = require('http-proxy-middleware');
2 |
3 | module.exports = function(app) {
4 | app.use(
5 | '/api',
6 | createProxyMiddleware({
7 | target: 'http://localhost:5000',
8 | changeOrigin: true,
9 | })
10 | );
11 | };
--------------------------------------------------------------------------------
/admin-blog/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/backend/.envexample:
--------------------------------------------------------------------------------
1 | NODE_ENV = development
2 | PORT = 5000
3 | MONGO_URI = mongodb://localhost:27017
4 | JWT_SECRET = replaceWithADifferentValue
5 | USER_NAME = admin
6 | USER_PASSWORD = admin
7 | MY_SECRET_TOKEN=replaceWithADifferentValue
8 | DB_NAME = blog
9 | NEXTJS_PORT = 3001
10 | DEMO = 0
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependency directories
2 | node_modules/
3 |
4 | # dotenv environment variable files
5 | .env
6 | .env.development.local
7 | .env.test.local
8 | .env.production.local
9 | .env.local
10 |
11 | # Logs
12 | logs
13 | *.log
14 | npm-debug.log*
15 | yarn-debug.log*
16 | yarn-error.log*
17 | lerna-debug.log*
18 | .pnpm-debug.log*
19 |
20 | # upload images
21 | /uploads/image/*
22 | !/uploads/image/avatar.ico
23 | !/uploads/image/logo.svg
24 | !/uploads/image/og-image.png
25 |
26 | # production
27 | /build
28 |
--------------------------------------------------------------------------------
/backend/config/db.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const bcrypt = require("bcryptjs");
3 | const User = require("../models/userModel");
4 | const Profile = require("../models/profileModel");
5 | const Page = require("../models/pageModel");
6 | const dbName = process.env.DB_NAME;
7 | const nextJsPort = process.env.NEXTJS_PORT;
8 |
9 | const connectDB = async () => {
10 | try {
11 | const conn = await mongoose.connect(process.env.MONGO_URI, {
12 | dbName: dbName,
13 | });
14 | console.log(`MongoDB Connected: ${conn.connection.host}`);
15 | } catch (error) {
16 | console.log(error);
17 | process.exit(1);
18 | }
19 | };
20 |
21 | //设置管理员默认账号
22 | const setDefaultPasswd = async () => {
23 | const name = process.env.USER_NAME;
24 | const userExists = await User.findOne({ name });
25 |
26 | if (!userExists) {
27 | const password = process.env.USER_PASSWORD;
28 | const salt = await bcrypt.genSalt(10);
29 | const hashedPassword = await bcrypt.hash(password, salt);
30 | const user = await User.create({
31 | name,
32 | password: hashedPassword,
33 | });
34 | if (user) {
35 | console.log(`初始化密码设置成功`);
36 | } else {
37 | throw new Error("初始化密码设置失败");
38 | }
39 | }
40 | };
41 |
42 | //设置默认的博客网站信息
43 | const setDefaultProfile = async () => {
44 | const userName = process.env.USER_NAME;
45 | const userExists = await User.findOne({ name: userName });
46 | let profileExists = null;
47 | if (userExists) {
48 | profileExists = await Profile.findOne({ user: userExists.id });
49 | } else {
50 | throw new Error("初始用户创建失败");
51 | }
52 | if (!profileExists) {
53 | const profileObj = {
54 | user: userExists.id,
55 | name: `${userName}的个人博客`,
56 | title: `${userName}的个人网站`,
57 | author: "Doe",
58 | language: "zh-CN",
59 | siteUrl: `http://localhost:${nextJsPort}`,
60 | siteRepo: "https://github.com",
61 | locale: "zh-CN",
62 | email: "name@site.com",
63 | description: "个人博客",
64 | logo: "/api/image/logo.svg",
65 | avatar: "/api/image/avatar.ico",
66 | socialBanner: "/api/image/og-image.png",
67 | };
68 | try {
69 | await Profile.create(profileObj);
70 | } catch (error) {
71 | throw new Error(error);
72 | }
73 | }
74 | };
75 |
76 | //设置默认的待构建页面表
77 | const setDefaultPage = async () => {
78 | const userName = process.env.USER_NAME;
79 | const userExists = await User.findOne({ name: userName });
80 | let pageExists = null;
81 | if (userExists) {
82 | pageExists = await Page.findOne({ user: userExists.id });
83 | } else {
84 | throw new Error("初始用户创建失败");
85 | }
86 | if (!pageExists) {
87 | const pageObj = {
88 | user: userExists.id,
89 | pages: [],
90 | };
91 | try {
92 | await Page.create(pageObj);
93 | } catch (error) {
94 | throw new Error(error);
95 | }
96 | }
97 | };
98 |
99 | module.exports = {
100 | connectDB,
101 | setDefaultPasswd,
102 | setDefaultProfile,
103 | setDefaultPage,
104 | };
105 |
--------------------------------------------------------------------------------
/backend/controllers/commentController.js:
--------------------------------------------------------------------------------
1 | const asyncHandler = require("express-async-handler");
2 |
3 | const Comment = require("../models/commentModel");
4 | const Post = require("../models/postModel");
5 |
6 | // @desc Get comments by post id
7 | // @route GET /api/comments/:postId
8 | // @access Public
9 | const getComments = asyncHandler(async (req, res) => {
10 | const comments = await Comment.find({ post: req.params.postId }).select(
11 | "-email -adminUser"
12 | ); //with unpublished comments
13 | res.status(200).json(comments);
14 | });
15 |
16 | // @desc Get all comments
17 | // @route GET /api/comments/all
18 | // @access Private
19 | const getAllComments = asyncHandler(async (req, res) => {
20 | const comments = await Comment.find({ adminUser: req.user.id });
21 | res.status(200).json(comments);
22 | });
23 |
24 | // @desc Set comments
25 | // @route POST /api/comments
26 | // @access Public
27 | const setComments = asyncHandler(async (req, res) => {
28 | if (!req.body.postId) {
29 | res.status(400);
30 | throw new Error("Please set the postId");
31 | }
32 | if (!req.body.username) {
33 | res.status(400);
34 | throw new Error("Please add a username");
35 | }
36 | if (!req.body.email) {
37 | res.status(400);
38 | throw new Error("Please add an email");
39 | }
40 | if (!req.body.comment) {
41 | res.status(400);
42 | throw new Error("Comment is empty");
43 | }
44 | const post = await Post.findById(req.body.postId);
45 |
46 | const userExists = await Comment.findOne({ username: req.body.username });
47 |
48 | if (userExists && userExists.email !== req.body.email) {
49 | res.status(400);
50 | throw new Error("User already exists");
51 | }
52 | try {
53 | const comment = await Comment.create({
54 | adminUser: post.user,
55 | source: post.title,
56 | post: req.body.postId,
57 | username: req.body.username,
58 | email: req.body.email,
59 | comment: req.body.comment,
60 | });
61 | comment.adminUser = undefined;
62 | res.status(200).json(comment);
63 | } catch (error) {
64 | res.status(500);
65 | throw new Error(error);
66 | }
67 | });
68 |
69 | // @desc Publish/Unpublish comment
70 | // @route PUT /api/comments/:id
71 | // @access Private
72 | const updateComment = asyncHandler(async (req, res) => {
73 | if (!req.body.published) {
74 | res.status(400);
75 | throw new Error("Please set the value of published");
76 | }
77 | const comment = await Comment.findById(req.params.id);
78 | if (!comment) {
79 | res.status(400);
80 | throw new Error("Comment not found");
81 | }
82 |
83 | //const user = await User.findById(req.user.id)
84 | if (!req.user) {
85 | res.status(401);
86 | throw new Error("User not founded");
87 | }
88 |
89 | if (comment.adminUser.toString() !== req.user.id) {
90 | res.status(401);
91 | throw new Error("User not authorized");
92 | }
93 | const updatedComment = await Comment.findByIdAndUpdate(
94 | req.params.id,
95 | { published: req.body.published },
96 | { new: true }
97 | );
98 | res.status(200).json(updatedComment);
99 | });
100 |
101 | // @desc Delete comments
102 | // @route DELETE /api/comments/:id
103 | // @access Private
104 | const deleteComment = asyncHandler(async (req, res) => {
105 | const comment = await Comment.findById(req.params.id);
106 | if (!comment) {
107 | res.status(400);
108 | throw new Error("Comment not found");
109 | }
110 |
111 | //const user = await User.findById(req.user.id)
112 |
113 | if (!req.user) {
114 | res.status(401);
115 | throw new Error("User not founded");
116 | }
117 |
118 | if (comment.adminUser.toString() !== req.user.id) {
119 | res.status(401);
120 | throw new Error("User not authorized");
121 | }
122 |
123 | await comment.remove();
124 |
125 | res.status(200).json({ id: req.params.id });
126 | });
127 |
128 | module.exports = {
129 | getComments,
130 | getAllComments,
131 | setComments,
132 | updateComment,
133 | deleteComment,
134 | };
135 |
--------------------------------------------------------------------------------
/backend/controllers/imageController.js:
--------------------------------------------------------------------------------
1 | const asyncHandler = require("express-async-handler");
2 | const fs = require("fs");
3 |
4 | const Image = require("../models/imageModel");
5 |
6 | // @desc Get images 获取所有图片
7 | // @route GET /api/images
8 | // @access Private
9 | const getImages = asyncHandler(async (req, res) => {
10 | const images = await Image.find({ user: req.user.id });
11 | res.status(200).json(images);
12 | });
13 |
14 | // @desc Set images 上传一张图片
15 | // @route POST /api/images
16 | // @access Private
17 | const setImages = asyncHandler(async (req, res) => {
18 | if (!req.body.title) {
19 | res.status(400);
20 | throw new Error("Please add a title to the image");
21 | }
22 | const imageExists = await Image.findOne({ title: req.body.title });
23 | if (imageExists) {
24 | res.status(400);
25 | throw new Error("图片标题已存在");
26 | }
27 |
28 | let imageObj = {
29 | title: req.body.title,
30 | user: req.user.id,
31 | };
32 | if (req.body.description) {
33 | imageObj.description = req.body.description;
34 | }
35 | if (req.file) {
36 | if (!req.file.mimetype.startsWith("image")) {
37 | res.status(400);
38 | throw new Error("Type error");
39 | }
40 | imageObj.imageUrl = "/api/cloud_photo/" + req.file.filename;
41 | }
42 | try {
43 | const image = await Image.create(imageObj);
44 | res.status(200).json(image);
45 | } catch (error) {
46 | res.status(500);
47 | throw new Error(error);
48 | }
49 | });
50 |
51 | // @desc Update images 修改图片信息
52 | // @route PUT /api/images/:id
53 | // @access Private
54 | const updateImage = asyncHandler(async (req, res) => {
55 | const image = await Image.findById(req.params.id);
56 | if (!image) {
57 | res.status(400);
58 | throw new Error("Image not found");
59 | }
60 | //const user = await User.findById(req.user.id)
61 | if (!req.user) {
62 | res.status(401);
63 | throw new Error("User not founded");
64 | }
65 | if (image.user.toString() !== req.user.id) {
66 | res.status(401);
67 | throw new Error("User not authorized");
68 | }
69 | let newImageData = req.body;
70 | try {
71 | const updatedImage = await Image.findByIdAndUpdate(
72 | req.params.id,
73 | newImageData,
74 | {
75 | new: true,
76 | }
77 | );
78 | updatedImage.imageUrl = undefined;
79 | res.status(200).json(updatedImage);
80 | } catch (error) {
81 | res.status(500);
82 | throw new Error(error);
83 | }
84 | });
85 |
86 | // @desc Delete images
87 | // @route DELETE /api/images/:id
88 | // @access Private
89 | const deleteImage = asyncHandler(async (req, res) => {
90 | const image = await Image.findById(req.params.id);
91 | if (!image) {
92 | res.status(400);
93 | throw new Error("Image not found");
94 | }
95 |
96 | if (!req.user) {
97 | res.status(401);
98 | throw new Error("User not founded");
99 | }
100 |
101 | if (image.user.toString() !== req.user.id) {
102 | res.status(401);
103 | throw new Error("User not authorized");
104 | }
105 | try {
106 | const imageUrl = image.imageUrl;
107 | const path =
108 | "./uploads/cloud_photo" + imageUrl.substring(imageUrl.lastIndexOf("/"));
109 | fs.unlinkSync(path);
110 | await image.remove();
111 | res.status(200).json({ id: req.params.id });
112 | } catch (error) {
113 | res.status(500);
114 | throw new Error(error);
115 | }
116 | });
117 |
118 | module.exports = {
119 | getImages,
120 | setImages,
121 | updateImage,
122 | deleteImage,
123 | };
124 |
--------------------------------------------------------------------------------
/backend/controllers/profileController.js:
--------------------------------------------------------------------------------
1 | const asyncHandler = require("express-async-handler");
2 | const fetch = (...args) =>
3 | import("node-fetch").then(({ default: fetch }) => fetch(...args));
4 |
5 | const Profile = require("../models/profileModel");
6 | const Page = require("../models/pageModel");
7 | const Post = require("../models/postModel");
8 |
9 | // @desc Get profile
10 | // @route GET /api/profile
11 | // @access Private
12 | const getProfile = asyncHandler(async (req, res) => {
13 | const profile = await Profile.find({ user: req.user.id });
14 | res.status(200).json(profile);
15 | });
16 |
17 | // @desc Update profile
18 | // @route PUT /api/profile/:id
19 | // @access Private
20 | const updateProfile = asyncHandler(async (req, res) => {
21 | const profile = await Profile.findById(req.params.id);
22 | if (!profile) {
23 | res.status(400);
24 | throw new Error("Profile not found");
25 | }
26 | //const user = await User.findById(req.user.id)
27 | if (!req.user) {
28 | res.status(401);
29 | throw new Error("User not founded");
30 | }
31 | if (profile.user.toString() !== req.user.id) {
32 | res.status(401);
33 | throw new Error("User not authorized");
34 | }
35 | let newProfileData = req.body;
36 | //console.log('req.files: ',req.files);
37 |
38 | if (req?.files?.logo && req?.files?.logo[0]) {
39 | newProfileData.logo = "/api/image/" + req.files["logo"][0].filename;
40 | newProfileData.logoType = req.files["logo"][0].mimetype;
41 | //console.log("req.files.logo[0]: ",req.files["logo"][0]);
42 | }
43 | if (req?.files?.avatar && req?.files?.avatar[0]) {
44 | newProfileData.avatar = "/api/image/" + req.files["avatar"][0].filename;
45 | newProfileData.avatarType = req.files["avatar"][0].mimetype;
46 | }
47 | if (req?.files?.socialBanner && req?.files?.socialBanner[0]) {
48 | newProfileData.socialBanner =
49 | "/api/image/" + req.files["socialBanner"][0].filename;
50 | //newProfileData.socialBanner = req.files["socialBanner"][0].buffer;
51 | newProfileData.socialBannerType = req.files["socialBanner"][0].mimetype;
52 | }
53 |
54 | const revalidateUrl = `http://localhost:${process.env.NEXTJS_PORT}/api/revalidate?secret=${process.env.MY_SECRET_TOKEN}&change=post`;
55 |
56 | try {
57 | const updatedProfile = await Profile.findByIdAndUpdate(
58 | req.params.id,
59 | newProfileData,
60 | {
61 | new: true,
62 | }
63 | );
64 |
65 | const pages = ["/", "/tags", "/timeline", "/categories"];
66 | let categories = [];
67 | let tags = [];
68 | const posts = await Post.find({ user: req.user.id, draft: false });
69 |
70 | posts.forEach((doc) => {
71 | pages.push("/posts/" + doc.title);
72 | });
73 | posts.forEach((doc) => {
74 | if (!categories.includes(doc.category)) {
75 | pages.push(`/categories/${doc.category}`);
76 | categories.push(doc.category);
77 | }
78 | doc.tags.forEach((tag) => {
79 | if (!tags.includes(tag)) {
80 | pages.push(`/tags/${tag}`);
81 | tags.push(tag);
82 | }
83 | });
84 | pages.push(`/posts/${doc.title}`);
85 | });
86 |
87 | const pageDoc = await Page.findOne({ user: req.user.id });
88 | const mergePages = [...pages, ...pageDoc.pages];
89 | const deDupPages = [...new Set(mergePages)];
90 | await Page.findOneAndUpdate({ user: req.user.id }, { pages: deDupPages });
91 | //console.log("pages: ", deDupPages);
92 |
93 | res.status(200).json(updatedProfile);
94 | } catch (error) {
95 | res.status(500);
96 | throw new Error(error);
97 | } finally {
98 | await fetch(revalidateUrl);
99 | }
100 | });
101 |
102 | module.exports = {
103 | getProfile,
104 | updateProfile,
105 | };
106 |
--------------------------------------------------------------------------------
/backend/controllers/uploadController.js:
--------------------------------------------------------------------------------
1 | const asyncHandler = require("express-async-handler");
2 |
3 | // @desc Upload files
4 | // @route POST /api/upload
5 | // @access Public
6 | const uploadFile = asyncHandler(async (req, res) => {
7 | // console.log("upload req body: ",req.body);
8 | // console.log("upload req files: ",req.files);
9 | res.status(200).json("ok");
10 |
11 | });
12 |
13 | module.exports = { uploadFile }
14 |
--------------------------------------------------------------------------------
/backend/controllers/userController.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 | const bcrypt = require("bcryptjs");
3 | const asyncHandler = require("express-async-handler");
4 | const User = require("../models/userModel");
5 |
6 | // @desc Register new user
7 | // @route POST /api/users
8 | // @access Public
9 | const registerUser = asyncHandler(async (req, res) => {
10 | const { name, password } = req.body;
11 |
12 | if (!name || !password) {
13 | res.status(500);
14 | throw new Error("请填写完整信息");
15 | }
16 |
17 | const userExists = await User.findOne({ name });
18 |
19 | if (userExists) {
20 | res.status(400);
21 | throw new Error("用户已存在");
22 | }
23 |
24 | //console.log(`申请注册新用户:用户名:(${name})`);
25 |
26 | const salt = await bcrypt.genSalt(10);
27 | const hashedPassword = await bcrypt.hash(password, salt);
28 | //console.log(`申请注册新用户:用户名:(${name}),密码哈希成功`);
29 |
30 | const user = await User.create({
31 | name,
32 | password: hashedPassword,
33 | });
34 |
35 | //console.log(`用户名:(${name}),user:${user}`);
36 | if (user) {
37 | res.status(201).json({
38 | _id: user.id,
39 | name: user.name,
40 | token: generateToken(user._id),
41 | });
42 | //console.log(`新用户注册成功,用户名为${user.name}`);
43 | } else {
44 | res.status(400);
45 | throw new Error("用户注册失败");
46 | }
47 | });
48 |
49 | // @desc Authenticate a user
50 | // @route POST /api/users/login
51 | // @access Public
52 | const loginUser = asyncHandler(async (req, res) => {
53 | //console.log("login from: ",req.ip);
54 | if(!req.body.password) {
55 | res.status(500);
56 | throw new Error("请提供密码");
57 | }
58 | if(typeof req.body.password !== 'string') {
59 | res.status(500);
60 | throw new Error("密码类型错误");
61 | }
62 | const { password } = req.body;
63 | const name = process.env.USER_NAME;
64 | const user = await User.findOne({ name });
65 | if (user && (await bcrypt.compare(password, user.password))) {
66 | res.json({
67 | _id: user.id,
68 | name: user.name,
69 | token: generateToken(user._id),
70 | });
71 | } else {
72 | res.status(400);
73 | throw new Error("密码错误");
74 | }
75 | });
76 |
77 | // @desc Get user data
78 | // @route GET /api/users/me
79 | // @access Private
80 | const getMe = asyncHandler(async (req, res) => {
81 | res.status(200).json(req.user);
82 | });
83 |
84 | // @desc Update user password
85 | // @route PUT /api/users/update-password
86 | // @access Private
87 | const updatePassword = asyncHandler(async (req, res) => {
88 | const { origin, password } = req.body;
89 |
90 | if (!origin || !password) {
91 | res.status(400);
92 | throw new Error("请填写完整信息");
93 | }
94 |
95 | if (!req.user) {
96 | res.status(401);
97 | throw new Error("User not founded");
98 | }
99 |
100 | const user = await User.findOne({ name: req.user.name });
101 | if (!user) {
102 | res.status(400);
103 | throw new Error("User not founded");
104 | }
105 | if (!(await bcrypt.compare(origin, user.password))) {
106 | res.status(400);
107 | throw new Error("密码错误");
108 | }
109 | const salt = await bcrypt.genSalt(10);
110 | const hashedPassword = await bcrypt.hash(password, salt);
111 | const updatedUser = await User.findByIdAndUpdate(
112 | req.user.id,
113 | { password: hashedPassword },
114 | {
115 | new: true,
116 | }
117 | );
118 |
119 | if (updatedUser) {
120 | res.status(200).json({
121 | _id: updatedUser.id,
122 | name: updatedUser.name,
123 | token: generateToken(updatedUser._id),
124 | });
125 | } else {
126 | res.status(500);
127 | throw new Error("密码更改失败");
128 | }
129 | });
130 |
131 | // Generate JWT
132 | const generateToken = (id) => {
133 | return jwt.sign({ id }, process.env.JWT_SECRET, {
134 | expiresIn: "5d",
135 | });
136 | };
137 |
138 | module.exports = { loginUser, getMe, updatePassword };
139 |
--------------------------------------------------------------------------------
/backend/middleware/authMiddleware.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const asyncHandler = require('express-async-handler');
3 | const User = require('../models/userModel')
4 |
5 | const protect = asyncHandler(async (req,res,next)=>{
6 | let token;
7 |
8 | if(req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
9 | try {
10 | token = req.headers.authorization.split(' ')[1];
11 | const decoded = jwt.verify(token, process.env.JWT_SECRET)
12 |
13 | req.user = await User.findById(decoded.id).select('-password');
14 | if(!req.user) {
15 | res.status(401)
16 | throw new Error('not authorized')
17 | }
18 | const userUpdatedAt = parseInt(req.user.updatedAt.valueOf()/1000);
19 | const jwtIssueAt = decoded.iat;
20 | if (userUpdatedAt > jwtIssueAt) {
21 | res.status(401)
22 | throw new Error('not authorized')
23 | }
24 |
25 |
26 | next()
27 | } catch (error) {
28 | console.log(error)
29 | res.status(401)
30 | throw new Error('not authorized')
31 | }
32 | }
33 |
34 | if(!token) {
35 | res.status(401)
36 | throw new Error('not authorized, no token')
37 | }
38 |
39 | })
40 |
41 | module.exports = {protect}
--------------------------------------------------------------------------------
/backend/middleware/demoMiddleware.js:
--------------------------------------------------------------------------------
1 | const asyncHandler = require("express-async-handler");
2 |
3 | const handleDemoReq = asyncHandler(async (req, res, next) => {
4 | if (parseInt(process.env.DEMO,10) === 0) {
5 | next();
6 | } else {
7 | res.status(403);
8 | throw new Error("演示环境无法修改数据");
9 | }
10 | });
11 |
12 | module.exports = { handleDemoReq };
13 |
--------------------------------------------------------------------------------
/backend/middleware/errorMiddleware.js:
--------------------------------------------------------------------------------
1 | const errorHandler = (err, req, res, next) => {
2 | const statusCode = res.statusCode ? res.statusCode : 500;
3 |
4 | res.status(statusCode);
5 |
6 | res.json({
7 | message: err.message,
8 | stack: process.env.NODE_ENV === "production" ? null : err.stack,
9 | });
10 | };
11 |
12 |
13 | module.exports = {errorHandler}
--------------------------------------------------------------------------------
/backend/models/commentModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const commentSchema = mongoose.Schema({
4 | adminUser:{
5 | type: mongoose.Schema.Types.ObjectId,
6 | required:true,
7 | ref: 'User'
8 | },
9 | post:{
10 | type: mongoose.Schema.Types.ObjectId,
11 | required:true,
12 | ref: 'Post'
13 | },
14 | source:{
15 | type:String,
16 | required:true,
17 | },
18 | username: {
19 | type: String,
20 | required: [true, 'Please add a username to the comment']
21 | },
22 | email: {
23 | type: String,
24 | required: [true, 'Please add an email']
25 | },
26 | comment: {
27 | type: String,
28 | required: true
29 | },
30 | published: {
31 | type: Boolean,
32 | required: true,
33 | default: false,
34 | }
35 |
36 |
37 | },{
38 | timestamps: true,
39 | })
40 |
41 | module.exports = mongoose.model('Comment', commentSchema);
--------------------------------------------------------------------------------
/backend/models/imageModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const imageSchema = mongoose.Schema({
4 | user:{
5 | type: mongoose.Schema.Types.ObjectId,
6 | required:true,
7 | ref: 'User'
8 | },
9 | title:{
10 | type: String,
11 | required: [true, 'Please add a title to the image']
12 | },
13 | description:{
14 | type: String,
15 | },
16 | imageUrl:{
17 | type: String,
18 | required:true
19 | },
20 | // hotlinkProtection: {
21 | // required:true,
22 | // type: Boolean,
23 | // },
24 | },{
25 | timestamps: true,
26 | })
27 |
28 | module.exports = mongoose.model('Image', imageSchema)
--------------------------------------------------------------------------------
/backend/models/linkModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const linkSchema = mongoose.Schema({
4 | user:{
5 | type: mongoose.Schema.Types.ObjectId,
6 | required:true,
7 | ref: 'User'
8 | },
9 | name:{
10 | type: String,
11 | required: [true, 'Please add a name to the link']
12 | },
13 | website:{
14 | type: String,
15 | required: [true, 'Please add a url']
16 | },
17 | description:{
18 | type: String,
19 | },
20 | picture:{
21 | type: String,
22 | }
23 | })
24 |
25 | module.exports = mongoose.model('Link', linkSchema)
--------------------------------------------------------------------------------
/backend/models/pageModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const pageSchema = mongoose.Schema({
4 | user: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | required: true,
7 | ref: "User",
8 | },
9 | pages: {
10 | type: [String],
11 | }
12 | });
13 |
14 | module.exports = mongoose.model("Page", pageSchema);
--------------------------------------------------------------------------------
/backend/models/postModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const postSchema = mongoose.Schema({
4 | user:{
5 | type: mongoose.Schema.Types.ObjectId,
6 | required:true,
7 | ref: 'User'
8 | },
9 | title: {
10 | type: String,
11 | required: [true, 'Please add a title to the post']
12 | },
13 | authors: {
14 | type: [String],
15 | required: true
16 | },
17 | tags: {
18 | type: [String],
19 | },
20 | category: {
21 | type: String,
22 | required: [true, 'Please add a category to the post']
23 | },
24 | draft: {
25 | required:true,
26 | type: Boolean,
27 | },
28 | summary: {
29 | type: String,
30 | },
31 | canonicalUrl: {
32 | type: String,
33 | },
34 | image: {
35 | type: Buffer,
36 | },
37 | content: {
38 | type: String,
39 | }
40 |
41 | },{
42 | timestamps: true,
43 | })
44 |
45 | module.exports = mongoose.model('Post', postSchema);
--------------------------------------------------------------------------------
/backend/models/profileModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const profileSchema = mongoose.Schema({
4 | user: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | required: true,
7 | ref: "User",
8 | },
9 | name: {
10 | type: String,
11 | required: [true, "Please add a title to the blog logo"],
12 | },
13 | title: {
14 | type: String,
15 | required: [true, "Please add a html title"],
16 | },
17 | author: {
18 | type: String,
19 | required: [true, "Please add a site author"],
20 | },
21 | language: {
22 | type: String,
23 | required: true,
24 | },
25 | siteUrl: {
26 | type: String,
27 | required: true,
28 | },
29 | siteRepo: {
30 | type: String,
31 | required: true,
32 | },
33 | locale: {
34 | type: String,
35 | required: true,
36 | },
37 | email: {
38 | type: String,
39 | required: true,
40 | },
41 | github: {
42 | type: String,
43 | },
44 | zhihu: {
45 | type: String,
46 | },
47 | juejin: {
48 | type: String,
49 | },
50 | wx: {
51 | type: String,
52 | },
53 | description: {
54 | type: String,
55 | required: true,
56 | },
57 | logo: {
58 | type: String,
59 | required: true,
60 | },
61 | logoType: {
62 | type: String,
63 | },
64 | avatar: {
65 | type: String,
66 | required: true,
67 | },
68 | avatarType: {
69 | type: String,
70 | },
71 | socialBanner: {
72 | type: String,
73 | required: true,
74 | },
75 | socialBannerType: {
76 | type: String,
77 | },
78 | });
79 |
80 | module.exports = mongoose.model("Profile", profileSchema);
81 |
--------------------------------------------------------------------------------
/backend/models/userModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const userSchema = mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: [true, 'Please add a name'],
7 | unique: true,
8 | },
9 |
10 | password: {
11 | type: String,
12 | required: [true, 'Please add a password']
13 | },
14 |
15 | }, {
16 | timestamps: true,
17 | })
18 |
19 | module.exports = mongoose.model('User', userSchema)
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "1.0.0",
4 | "description": "blog system backend",
5 | "main": "server.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "start": "node server.js",
9 | "server": "nodemon server.js"
10 | },
11 | "author": "manfred",
12 | "license": "MIT",
13 | "dependencies": {
14 | "bcryptjs": "^2.4.3",
15 | "cors": "^2.8.5",
16 | "dotenv": "^16.0.1",
17 | "express": "^4.18.1",
18 | "express-async-handler": "^1.2.0",
19 | "express-rate-limit": "^6.4.0",
20 | "express-static-gzip": "^2.1.7",
21 | "jsonwebtoken": "^8.5.1",
22 | "mongoose": "^6.3.3",
23 | "multer": "^1.4.5-lts.1",
24 | "node-fetch": "^3.2.10"
25 | },
26 | "devDependencies": {
27 | "nodemon": "^2.0.16"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/backend/routes/commentRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const {
4 | getComments,
5 | getAllComments,
6 | setComments,
7 | updateComment,
8 | deleteComment,
9 | } = require("../controllers/commentController");
10 | const { protect } = require("../middleware/authMiddleware");
11 |
12 | router.route("/all").get(protect, getAllComments);
13 | router.route("/").post(setComments);
14 | router.route("/:postId").get(getComments);
15 | router.route("/:id").put(protect, updateComment).delete(protect, deleteComment);
16 |
17 | module.exports = router;
18 |
--------------------------------------------------------------------------------
/backend/routes/imageRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const path = require("path");
4 | const {
5 | getImages,
6 | setImages,
7 | updateImage,
8 | deleteImage,
9 | } = require("../controllers/imageController");
10 |
11 | const { protect } = require("../middleware/authMiddleware");
12 | const { handleDemoReq } = require("../middleware/demoMiddleware");
13 | const multer = require("multer");
14 | const storage = multer.diskStorage({
15 | destination: "./uploads/cloud_photo",
16 | filename: function (req, file, cb) {
17 | cb(null, Date.now() + path.extname(file.originalname));
18 | },
19 | });
20 | function fileFilter(req, file, cb) {
21 | //console.log("fileFilter file: ", file);
22 | if (file.mimetype.startsWith("image")) {
23 | cb(null, true);
24 | } else {
25 | cb(new Error("Type error"));
26 | }
27 | }
28 |
29 | const upload = multer({
30 | storage,
31 | fileFilter,
32 | });
33 |
34 | router
35 | .route("/")
36 | .get(protect, getImages)
37 | .post(protect, upload.single("image"), setImages);
38 | router
39 | .route("/:id")
40 | .put(protect, updateImage)
41 | .delete(protect, deleteImage);
42 |
43 | module.exports = router;
44 |
--------------------------------------------------------------------------------
/backend/routes/linkRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const path = require("path");
4 | const {
5 | getLinks,
6 | setLinks,
7 | updateLink,
8 | deleteLink,
9 | } = require("../controllers/linkController");
10 | const { protect } = require("../middleware/authMiddleware");
11 | const { handleDemoReq } = require("../middleware/demoMiddleware");
12 | const multer = require("multer");
13 | const storage = multer.diskStorage({
14 | destination: function (req, file, cb) {
15 | cb(null, "./uploads/image");
16 | },
17 | filename: function (req, file, cb) {
18 | cb(null, Date.now() + path.extname(file.originalname));
19 | },
20 | });
21 | function fileFilter(req, file, cb) {
22 | //console.log("fileFilter file: ", file);
23 | if (file.mimetype === "image/png" || file.mimetype === "image/jpeg") {
24 | cb(null, true);
25 | } else {
26 | cb(new Error("Image type error"));
27 | }
28 | }
29 | const upload = multer({
30 | storage: storage,
31 | fileFilter,
32 | limits: { fileSize: 1024 * 1024 },
33 | });
34 |
35 | router
36 | .route("/")
37 | .get(protect, getLinks)
38 | .post(protect, handleDemoReq, upload.single("picture"), setLinks);
39 | router
40 | .route("/:id")
41 | .put(protect, handleDemoReq, upload.single("picture"), updateLink)
42 | .delete(protect, handleDemoReq, deleteLink);
43 |
44 | module.exports = router;
45 |
--------------------------------------------------------------------------------
/backend/routes/postRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const {
4 | getPosts,
5 | setPosts,
6 | updatePost,
7 | deletePost,
8 | updateCategory,
9 | updateTag,
10 | deleteTag,
11 | getPostById,
12 | } = require("../controllers/postController");
13 | const { protect } = require("../middleware/authMiddleware");
14 | const multer = require("multer");
15 | const upload = multer();
16 | const { handleDemoReq } = require("../middleware/demoMiddleware");
17 |
18 | router
19 | .route("/")
20 | .get(protect, getPosts)
21 | .post(protect, handleDemoReq, upload.single("image"), setPosts);
22 | router
23 | .route("/category")
24 | .put(protect, handleDemoReq, upload.none(), updateCategory);
25 | router
26 | .route("/tag")
27 | .put(protect, handleDemoReq, upload.none(), updateTag)
28 | .delete(protect, handleDemoReq, upload.none(), deleteTag);
29 | router
30 | .route("/:id")
31 | .put(protect, handleDemoReq, upload.single("image"), updatePost)
32 | .delete(protect, handleDemoReq, deletePost)
33 | .get(protect, getPostById);
34 |
35 | module.exports = router;
36 |
--------------------------------------------------------------------------------
/backend/routes/profileRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const path = require("path");
4 | const {
5 | getProfile,
6 | updateProfile,
7 | } = require("../controllers/profileController");
8 | const { protect } = require("../middleware/authMiddleware");
9 | const { handleDemoReq } = require("../middleware/demoMiddleware");
10 | const multer = require("multer");
11 | const storage = multer.diskStorage({
12 | destination: function (req, file, cb) {
13 | cb(null, "./uploads/image");
14 | },
15 | filename: function (req, file, cb) {
16 | cb(null, Date.now() + path.extname(file.originalname));
17 | },
18 | });
19 | function fileFilter(req, file, cb) {
20 | //console.log("fileFilter file: ", file);
21 | if (
22 | file.mimetype === "image/png" ||
23 | file.mimetype === "image/svg+xml" ||
24 | file.mimetype === "image/x-icon" ||
25 | file.mimetype === "image/vnd.microsoft.icon"
26 | ) {
27 | cb(null, true);
28 | } else {
29 | cb(new Error("Image type error"));
30 | }
31 | }
32 | const upload = multer({
33 | storage: storage,
34 | fileFilter,
35 | limits: { fileSize: 300 * 1024 },
36 | });
37 |
38 | router.route("/").get(protect, getProfile);
39 | router.route("/:id").put(
40 | protect,
41 | handleDemoReq,
42 | upload.fields([
43 | { name: "avatar", maxCount: 1 },
44 | { name: "logo", maxCount: 1 },
45 | { name: "socialBanner", maxCount: 1 },
46 | ]),
47 | updateProfile
48 | );
49 |
50 | module.exports = router;
51 |
--------------------------------------------------------------------------------
/backend/routes/uploadRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const {uploadFile} = require("../controllers/uploadController")
4 |
5 | const multer = require("multer");
6 | const upload = multer({ dest: "uploads/" });
7 |
8 | router.route('/').post(upload.single('avatar'),uploadFile)
9 |
10 | module.exports = router;
--------------------------------------------------------------------------------
/backend/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const {
4 | loginUser,
5 | getMe,
6 | updatePassword,
7 | } = require("../controllers/userController");
8 | const { protect } = require("../middleware/authMiddleware");
9 | const rateLimit = require("express-rate-limit");
10 | const { handleDemoReq } = require("../middleware/demoMiddleware");
11 |
12 | const limiterLogin = rateLimit({
13 | windowMs: 2 * 60 * 1000, // ms
14 | max: 3, // Limit each IP to xx requests per `window`
15 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
16 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers
17 | });
18 |
19 | //router.post("/", registerUser)
20 | router.post("/login", limiterLogin, loginUser);
21 | router.get("/me", protect, getMe);
22 | router.put("/update-password", protect, handleDemoReq, updatePassword);
23 |
24 | module.exports = router;
25 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const http = require("http");
3 | const dotenv = require("dotenv").config();
4 | const port = process.env.PORT || 5000;
5 | const {
6 | connectDB,
7 | setDefaultPasswd,
8 | setDefaultProfile,
9 | setDefaultPage,
10 | } = require("./config/db");
11 | const { errorHandler } = require("./middleware/errorMiddleware");
12 | const cors = require("cors");
13 | const rateLimit = require("express-rate-limit");
14 |
15 | connectDB();
16 |
17 | //设置管理员默认账号和其他初始化数据
18 | setDefaultPasswd().then(() => {
19 | setDefaultProfile();
20 | setDefaultPage();
21 | }
22 | );
23 |
24 | const app = express();
25 | if (process.env.NODE_ENV === "production") {
26 | app.set("trust proxy", true);
27 | }
28 |
29 | const httpServer = http.createServer(app);
30 |
31 | app.use(
32 | cors(
33 | //设置origin为false取消所有cors.
34 | //{origin: false}
35 | )
36 | );
37 |
38 | const limiter = rateLimit({
39 | windowMs: 10 * 60 * 1000,
40 | max: 500,
41 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
42 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers
43 | });
44 |
45 | const limiterComment = rateLimit({
46 | windowMs: 20 * 60 * 1000, // ms
47 | max: 500, // Limit each IP to xx requests per `window`
48 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
49 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers
50 | });
51 |
52 | app.use(express.json());
53 | app.use(express.urlencoded({ extended: false }));
54 |
55 | // const multer = require('multer')
56 | // const upload = multer()
57 |
58 | // app.post('/api/upload', upload.any(), (req, res, next) => {
59 | // req.body contains the text fields
60 | // console.log("upload req file type: ",typeof req.files[0]);
61 | // console.log("upload req files: ",req.files);
62 | // console.log("upload req body: ",req.body);
63 | // res.status(200).json("ok");
64 | // })
65 |
66 | app.use("/api/users", limiter, require("./routes/userRoutes"));
67 | app.use("/api/links", limiter, require("./routes/linkRoutes"));
68 | app.use("/api/posts", limiter, require("./routes/postRoutes"));
69 | app.use("/api/profile", limiter, require("./routes/profileRoutes"));
70 | app.use("/api/comments", limiterComment, require("./routes/commentRoutes"));
71 | app.use("/api/images", limiter, require("./routes/imageRoutes"));
72 |
73 | app.use(errorHandler);
74 |
75 | //Serving static image files
76 | const limiterStatic = rateLimit({
77 | windowMs: 1 * 60 * 1000, // 1 minutes
78 | max: 200, // Limit each IP to 150 requests per `window`
79 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
80 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers
81 | });
82 | const path = require("path");
83 | app.use(
84 | "/api/image",
85 | limiterStatic,
86 | express.static(path.join(__dirname, "uploads/image"))
87 | );
88 | app.use(
89 | "/api/cloud_photo",
90 | limiterStatic,
91 | express.static(path.join(__dirname, "uploads/cloud_photo"))
92 | );
93 |
94 | //博客管理页面的静态服务器
95 | function setCustomCacheControl (res, path) {
96 | if(path.includes("static") || path.endsWith(".woff")) {
97 | res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
98 | }
99 | if(path.endsWith(".html")) {
100 | res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
101 | }
102 | }
103 |
104 | if (process.env.NODE_ENV === "production") {
105 | const expressStaticGzip = require("express-static-gzip");
106 | app.use(
107 | limiterStatic,
108 | expressStaticGzip(path.join(__dirname, "../admin-blog/build"),{
109 | enableBrotli: true,
110 | orderPreference: ["br"],
111 | serveStatic:{
112 | setHeaders: setCustomCacheControl
113 | }
114 | }),
115 | );
116 | app.get("/*", limiterStatic, function (req, res) {
117 | res.sendFile(path.join(__dirname, "../admin-blog/build", "index.html"));
118 | });
119 | }
120 |
121 | httpServer.listen(port, () => {
122 | console.log(`HTTP服务器启动,端口为: ${port}`);
123 | });
124 |
--------------------------------------------------------------------------------
/backend/uploads/image/avatar.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/backend/uploads/image/avatar.ico
--------------------------------------------------------------------------------
/backend/uploads/image/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/backend/uploads/image/og-image.png
--------------------------------------------------------------------------------
/docs/images/blog-system-small.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/docs/images/blog-system-small.PNG
--------------------------------------------------------------------------------
/docs/images/blog-system.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/docs/images/blog-system.PNG
--------------------------------------------------------------------------------
/nextjs-blog/.envexample:
--------------------------------------------------------------------------------
1 | USER_NAME = admin
2 | MONGODB_URI = mongodb://localhost:27017
3 | DB_NAME = blog
4 | MY_SECRET_TOKEN=replaceWithADifferentValue
5 | PORT = 5000
6 | ANALYZE=false
--------------------------------------------------------------------------------
/nextjs-blog/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/nextjs-blog/.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 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # rss
35 | /public/*.xml
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/nextjs-blog/README.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/nextjs-blog/components/CommentForm.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import styles from "../styles/CommentForm.module.css";
3 |
4 | export default function CommentForm({ postId, setIsCommentChange }) {
5 | const [comment, setComment] = useState("");
6 | const [username, setUsername] = useState("");
7 | const [email, setEmail] = useState("");
8 | const [message, setMessage] = useState("");
9 | const [showNotice, setShowNotice] = useState(false);
10 | const form = {
11 | postId,
12 | username,
13 | email,
14 | comment,
15 | };
16 | const onSubmit = (e) => {
17 | e.preventDefault();
18 | const postData = async () => {
19 | const response = await fetch("/api/comments", {
20 | method: "POST",
21 | headers: {
22 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
23 | },
24 | body: new URLSearchParams(form),
25 | });
26 | if (response.status >= 400 && response.status < 600) {
27 | throw new Error("Bad response from server");
28 | }
29 | const json = await response.json();
30 | return json;
31 | };
32 | postData()
33 | .then((data) => {
34 | //console.log("response data: ", data);
35 | setMessage("评论已发送");
36 | setShowNotice(true);
37 | setIsCommentChange(true);
38 | })
39 | .catch((error) => {
40 | console.error("error: ", error.message);
41 | setMessage("发送失败");
42 | setShowNotice(true);
43 | })
44 | .finally(() => {
45 | setComment("");
46 | setUsername("");
47 | setEmail("");
48 | });
49 |
50 | };
51 | useEffect(() => {
52 | if (showNotice) {
53 | setTimeout(() => {
54 | setShowNotice(false);
55 | }, 3000);
56 | }
57 | }, [showNotice]);
58 |
59 | return (
60 |
61 |
99 | {showNotice ? (
100 |
{message}
101 | ) : null}
102 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/nextjs-blog/components/CommentSection.js:
--------------------------------------------------------------------------------
1 | import CommentForm from "./CommentForm";
2 | import Comments from "./Comments";
3 | import { useState } from "react";
4 |
5 | export default function CommentSection({postId}) {
6 | const [isCommentChange, setIsCommentChange] = useState(false);
7 | return (
8 | <>
9 |
10 |
15 | >
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/nextjs-blog/components/Comments.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useCallback } from "react";
2 | import styles from "../styles/Comments.module.css";
3 | import styleAni from "../styles/AnimatePublic.module.css";
4 |
5 | export default function Comments({
6 | postId,
7 | isCommentChange,
8 | setIsCommentChange,
9 | }) {
10 | const [comments, setComments] = useState(null);
11 |
12 | const fetchData = useCallback(async (signal) => {
13 | const response = await fetch("/api/comments/" + postId, {
14 | method: "GET",
15 | signal,
16 | });
17 | const json = await response.json();
18 | setComments(json);
19 | }, [postId]);
20 |
21 | useEffect(() => {
22 | const controller = new AbortController();
23 | const signal = controller.signal;
24 | fetchData(signal).catch((err)=>{
25 | if(err.name !== "AbortError") {
26 | console.error(err);
27 | }else {
28 | //console.log("取消请求");
29 | };
30 | });
31 | return () => {
32 | controller.abort();
33 | setIsCommentChange(false);
34 | };
35 | }, [postId, isCommentChange, setIsCommentChange, fetchData]);
36 |
37 | return comments !== null ? (
38 |
39 |
{`评论 : `}
40 | {comments
41 | .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
42 | .map((c) => {
43 | const commentCreatedDate = new Date(c.createdAt);
44 | const formatCreated = `${commentCreatedDate.getFullYear()}年${
45 | commentCreatedDate.getMonth() + 1
46 | }月${commentCreatedDate.getDate()}日`;
47 | return (
48 |
52 |
{c.username}
53 |
54 |
{c.comment}
55 |
56 | );
57 | })}
58 |
59 | ) : (
60 | loading
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/nextjs-blog/components/CopyButton.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { CopyToClipboard } from "react-copy-to-clipboard";
3 | import styles from "../styles/CopyButton.module.css";
4 |
5 | function CopyButton({ text }) {
6 | const [copied, setCopied] = useState(false);
7 | useEffect(() => {
8 | if (copied) {
9 | setTimeout(() => {
10 | setCopied(false);
11 | }, 2000);
12 | }
13 | }, [copied]);
14 | return (
15 | {
18 | if (result) {
19 | setCopied(true);
20 | }
21 | }}
22 | >
23 | {copied ? (
24 |
29 | ) : (
30 |
35 | )}
36 |
37 | );
38 | }
39 |
40 | export default CopyButton;
41 |
--------------------------------------------------------------------------------
/nextjs-blog/components/LoadMoreBtn.js:
--------------------------------------------------------------------------------
1 | import styles from "../styles/LoadMoreBtn.module.css";
2 |
3 | export default function LoadMoreBtn({onClick, text}) {
4 | return (
5 |
6 |
7 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/nextjs-blog/components/PostList.js:
--------------------------------------------------------------------------------
1 | import styles from "../styles/PostList.module.css";
2 | import Link from "next/link";
3 | import { useState,useMemo, useEffect } from "react";
4 | import LoadMoreBtn from "./LoadMoreBtn";
5 | import styleAni from "../styles/AnimatePublic.module.css";
6 |
7 | export default function PostList({ posts, displayN }) {
8 | const postNumber = useMemo(()=>posts.length,[posts]);
9 | const [isCompleted, setIsCompleted] = useState(postNumber <= displayN);
10 | const [index, setIndex] = useState(
11 | postNumber > displayN ? displayN : postNumber
12 | );
13 | const handleLoadMore = () => {
14 | const newIndex =
15 | postNumber > index + displayN ? index + displayN : postNumber;
16 | setTimeout(()=>setIndex(newIndex), 300);
17 | setTimeout(()=>setIsCompleted(postNumber <= index + displayN),300);
18 | };
19 | useEffect(()=>{
20 | setIsCompleted(postNumber <= displayN)
21 | },[postNumber, displayN]);
22 |
23 | return (
24 | <>
25 |
26 | {posts.slice(0, index).map((post) => {
27 | const postDate = new Date(post.createdAt);
28 | const formatDate = `${postDate.getFullYear()}年${
29 | postDate.getMonth() + 1
30 | }月${postDate.getDate()}日`;
31 | return (
32 | -
33 |
34 |
52 |
53 |
54 | );
55 | })}
56 |
57 | {isCompleted ? null : (
58 |
61 | )}
62 | >
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/nextjs-blog/components/PostLoading.js:
--------------------------------------------------------------------------------
1 | import styles from "../styles/PostLoading.module.css";
2 |
3 | export default function PostLoading() {
4 | return (
5 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/nextjs-blog/components/PostTimeline.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import styles from "../styles/PostTimeline.module.css";
3 |
4 | export default function PostTimeline({ postArchiveData }) {
5 | return (
6 |
7 |
8 | {postArchiveData.map((archive) => {
9 | return (
10 | -
11 |
12 |
13 |
14 | {archive.date}
15 |
16 | {archive.posts.map((post) => {
17 | return (
18 |
19 | {post.title}
20 |
21 | );
22 | })}
23 |
24 |
25 | );
26 | })}
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/nextjs-blog/components/SearchBar.js:
--------------------------------------------------------------------------------
1 | import styles from "../styles/SearchBar.module.css";
2 |
3 | export default function SearchBar({ value,onChange, onClickSearch,className }) {
4 | return (
5 |
6 |
16 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/nextjs-blog/components/ShrinkHeader.js:
--------------------------------------------------------------------------------
1 | import Header from "./Header";
2 | import { useEffect, useState } from "react";
3 |
4 | export default function ShrinkHeader({ siteMetadata, nav, postTitle }) {
5 | const [shrink, setShrink] = useState(false);
6 |
7 | useEffect(() => {
8 | function handleScroll() {
9 | setShrink(window.pageYOffset > 80);
10 | }
11 | if (typeof window !== "undefined") {
12 | window.addEventListener("scroll", handleScroll);
13 | }
14 | return ()=>{
15 | if(typeof window !== "undefined") {
16 | window.removeEventListener("scroll", handleScroll);
17 | }
18 | }
19 | }, []);
20 |
21 | return (
22 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/nextjs-blog/components/ThemeChanger.js:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes";
2 | import { useEffect, useState } from "react";
3 | import styles from "../styles/ThemeChanger.module.css";
4 |
5 | const ThemeChanger = () => {
6 | const [mounted, setMounted] = useState(false);
7 | const { theme, setTheme } = useTheme();
8 |
9 | useEffect(() => setMounted(true), []);
10 |
11 | if (!mounted) return null;
12 |
13 | return (
14 |
67 | );
68 | };
69 |
70 | export default ThemeChanger;
71 |
--------------------------------------------------------------------------------
/nextjs-blog/components/TocAndMD.js:
--------------------------------------------------------------------------------
1 | import * as tocbot from "tocbot";
2 | import { useEffect } from "react";
3 | import MarkDown from "./MarkDown";
4 | import styles from "../styles/TocAndMD.module.css";
5 |
6 | export default function TocAndMD({mdChildren}) {
7 | useEffect(() => {
8 | tocbot.init({
9 | // Where to render the table of contents.
10 | tocSelector: ".toc",
11 | // Where to grab the headings to build the table of contents.
12 | contentSelector: ".toc-content",
13 | // Which headings to grab inside of the contentSelector element.
14 | headingSelector: "h1, h2, h3",
15 | // For headings inside relative or absolute positioned containers within content.
16 | hasInnerContainers: true,
17 | headingsOffset: 60,
18 | scrollSmoothOffset: -60,
19 | collapseDepth:6 //目录不折叠
20 | });
21 | return () => {
22 | tocbot.destroy();
23 | };
24 | }, [mdChildren]);
25 |
26 | return (
27 |
28 |
32 |
33 | {/* */}
34 |
35 | {/* */}
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/nextjs-blog/lib/generate-rss.js:
--------------------------------------------------------------------------------
1 | // 参考 https://github.com/timlrx/tailwind-nextjs-starter-blog
2 | import { escape } from './utils/htmlEscaper';
3 |
4 | const generateRssItem = (siteMetadata, post, isCategory) => `
5 | -
6 | ${siteMetadata.siteUrl}/posts/${post.title}
7 | ${escape(post.title)}
8 | ${siteMetadata.siteUrl}/posts/${post.title}
9 | ${post.summary && `${escape(post.summary)}`}
10 | ${new Date(post.date).toUTCString()}
11 | ${siteMetadata.email} (${siteMetadata.author})
12 | ${isCategory?(`${post.category}`)
13 | :
14 | (post.tags && post.tags.map((t) => `${t}`).join(''))}
15 |
16 | `;
17 |
18 |
19 | const generateRss = (posts, siteMetadata, page = 'feed.xml') => `
20 |
21 |
22 | ${escape(siteMetadata.title)}
23 | ${siteMetadata.siteUrl}
24 | ${escape(siteMetadata.description)}
25 | ${siteMetadata.language}
26 | ${siteMetadata.email} (${siteMetadata.author})
27 | ${siteMetadata.email} (${siteMetadata.author})
28 | ${new Date(posts[0].date).toUTCString()}
29 |
30 | ${posts.map((post)=>generateRssItem(siteMetadata, post, page.includes('categories'))).join('')}
31 |
32 |
33 | `
34 |
35 |
36 |
37 |
38 |
39 | export default generateRss;
40 |
--------------------------------------------------------------------------------
/nextjs-blog/lib/linksData.js:
--------------------------------------------------------------------------------
1 | import dbConnect from "./utils/dbConnect";
2 | import Link from "./utils/linkModel";
3 | import User from "./utils/userModel";
4 |
5 | export async function getAllLinksData() {
6 | await dbConnect();
7 | const name = process.env.USER_NAME;
8 | const user = await User.findOne({ name });
9 | if (!user) {
10 | console.error("用户不存在");
11 | return [];
12 | } else {
13 | const linksResult = await Link.find({ user: user.id });
14 | const allLinks = [];
15 | if (linksResult) {
16 | linksResult.forEach((doc) => {
17 | const linkObj = {id:doc._id.toString(),
18 | name:doc.name,
19 | website:doc.website,};
20 | if(doc.description) {
21 | linkObj.description = doc.description;
22 | }
23 | if(doc.picture) {
24 | linkObj.picture = doc.picture;
25 | }
26 |
27 | allLinks.push(linkObj);
28 | });
29 | return allLinks;
30 | } else {
31 | return [];
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/nextjs-blog/lib/siteData.js:
--------------------------------------------------------------------------------
1 | import dbConnect from "./utils/dbConnect";
2 | import User from "./utils/userModel";
3 | import Profile from "./utils/profileModel";
4 |
5 | export async function getSiteMetadata() {
6 | await dbConnect();
7 | const name = process.env.USER_NAME;
8 | const user = await User.findOne({ name });
9 | if (!user) {
10 | console.error("用户不存在");
11 | return [];
12 | } else {
13 | const profileResult = await Profile.find({ user: user.id });
14 | let siteMetadata = {};
15 | if (profileResult.length === 1) {
16 | profileResult.forEach((doc) => {
17 | siteMetadata.id = doc._id.toString();
18 | siteMetadata.name = doc.name;
19 | siteMetadata.title = doc.title;
20 | siteMetadata.author = doc.author;
21 | siteMetadata.language = doc.language;
22 | siteMetadata.siteUrl = doc.siteUrl;
23 | siteMetadata.siteRepo = doc.siteRepo;
24 | siteMetadata.locale = doc.locale;
25 | siteMetadata.description = doc.description;
26 | siteMetadata.email = doc.email;
27 | siteMetadata.logo = doc.logo;
28 | siteMetadata.avatar = doc.avatar;
29 | siteMetadata.socialBanner = doc.socialBanner;
30 | if (doc.keywords) {
31 | siteMetadata.keywords = doc.keywords;
32 | }
33 | if (doc.github) {
34 | siteMetadata.github = doc.github;
35 | }
36 | if (doc.zhihu) {
37 | siteMetadata.zhihu = doc.zhihu;
38 | }
39 | if (doc.juejin) {
40 | siteMetadata.juejin = doc.juejin;
41 | }
42 | if (doc.wx) {
43 | siteMetadata.wx = doc.wx;
44 | }
45 | });
46 | }
47 |
48 | return siteMetadata;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/nextjs-blog/lib/utils/db.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const dbName = "blog";
3 |
4 | const connectDB = async () => {
5 | try {
6 | const conn = await mongoose.connect(process.env.MONGODB_URI, {
7 | dbName: dbName,
8 | });
9 | console.log(`MongoDB Connected: ${conn.connection.host}`);
10 | } catch (error) {
11 | console.log(error);
12 | process.exit(1);
13 | }
14 | };
15 |
16 | module.exports = { connectDB };
--------------------------------------------------------------------------------
/nextjs-blog/lib/utils/dbConnect.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const MONGODB_URI = process.env.MONGODB_URI;
4 |
5 | const DB_NAME = process.env.DB_NAME;
6 |
7 | if (!MONGODB_URI) {
8 | throw new Error(
9 | '请在.env.local文件里设置MONGODB_URI环境变量'
10 | );
11 | }
12 |
13 | let cached = global.mongoose
14 |
15 | if (!cached) {
16 | cached = global.mongoose = { conn: null, promise: null }
17 | }
18 |
19 | async function dbConnect() {
20 | if (cached.conn) {
21 | return cached.conn
22 | }
23 |
24 | if (!cached.promise) {
25 | const opts = {
26 | bufferCommands: false,
27 | dbName: DB_NAME
28 | }
29 |
30 | cached.promise = mongoose.connect(MONGODB_URI, opts).then((mongoose) => {
31 | return mongoose
32 | })
33 | }
34 | cached.conn = await cached.promise
35 | return cached.conn
36 | }
37 |
38 | export default dbConnect
--------------------------------------------------------------------------------
/nextjs-blog/lib/utils/htmlEscaper.js:
--------------------------------------------------------------------------------
1 | // 参考 https://github.com/timlrx/tailwind-nextjs-starter-blog
2 | const { replace } = ''
3 |
4 | // escape
5 | const es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g
6 | const ca = /[&<>'"]/g
7 |
8 | const esca = {
9 | '&': '&',
10 | '<': '<',
11 | '>': '>',
12 | "'": ''',
13 | '"': '"',
14 | }
15 | const pe = (m) => esca[m]
16 |
17 | /**
18 | * Safely escape HTML entities such as `&`, `<`, `>`, `"`, and `'`.
19 | * @param {string} es the input to safely escape
20 | * @returns {string} the escaped input, and it **throws** an error if
21 | * the input type is unexpected, except for boolean and numbers,
22 | * converted as string.
23 | */
24 | export const escape = (es) => replace.call(es, ca, pe)
25 |
--------------------------------------------------------------------------------
/nextjs-blog/lib/utils/linkModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const linkSchema = mongoose.Schema({
4 | user:{
5 | type: mongoose.Schema.Types.ObjectId,
6 | required:true,
7 | ref: 'User'
8 | },
9 | name:{
10 | type: String,
11 | required: [true, 'Please add a name to the link']
12 | },
13 | website:{
14 | type: String,
15 | required: [true, 'Please add a url']
16 | },
17 | description:{
18 | type: String,
19 | },
20 | picture:{
21 | type: String,
22 | }
23 | })
24 |
25 | module.exports = mongoose.models.Link || mongoose.model('Link', linkSchema)
--------------------------------------------------------------------------------
/nextjs-blog/lib/utils/pageModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const pageSchema = mongoose.Schema({
4 | user: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | required: true,
7 | ref: "User",
8 | },
9 | pages: {
10 | type: [String],
11 | }
12 | });
13 |
14 | module.exports = mongoose.models.Page || mongoose.model("Page", pageSchema);
--------------------------------------------------------------------------------
/nextjs-blog/lib/utils/postModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const postSchema = mongoose.Schema({
4 | user:{
5 | type: mongoose.Schema.Types.ObjectId,
6 | required:true,
7 | ref: 'User'
8 | },
9 | title: {
10 | type: String,
11 | required: [true, 'Please add a title to the post']
12 | },
13 | authors: {
14 | type: [String],
15 | required: true
16 | },
17 | tags: {
18 | type: [String],
19 | },
20 | category: {
21 | type: String,
22 | required: [true, 'Please add a category to the post']
23 | },
24 | draft: {
25 | required:true,
26 | type: Boolean,
27 | },
28 | summary: {
29 | type: String,
30 | },
31 | canonicalUrl: {
32 | type: String,
33 | },
34 | image: {
35 | type: Buffer,
36 | },
37 | content: {
38 | type: String,
39 | }
40 |
41 | },{
42 | timestamps: true,
43 | })
44 |
45 | module.exports = mongoose.models.Post || mongoose.model('Post',postSchema);
--------------------------------------------------------------------------------
/nextjs-blog/lib/utils/profileModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const profileSchema = mongoose.Schema({
4 | user: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | required: true,
7 | ref: "User",
8 | },
9 | name: {
10 | type: String,
11 | required: [true, "Please add a title to the blog logo"],
12 | },
13 | title: {
14 | type: String,
15 | required: [true, "Please add a html title"],
16 | },
17 | author: {
18 | type: String,
19 | required: [true, "Please add a site author"],
20 | },
21 | language: {
22 | type: String,
23 | required: true,
24 | },
25 | siteUrl: {
26 | type: String,
27 | required: true,
28 | },
29 | siteRepo: {
30 | type: String,
31 | required: true,
32 | },
33 | locale: {
34 | type: String,
35 | required: true,
36 | },
37 | email: {
38 | type: String,
39 | required: true,
40 | },
41 | github: {
42 | type: String,
43 | },
44 | zhihu: {
45 | type: String,
46 | },
47 | juejin: {
48 | type: String,
49 | },
50 | wx: {
51 | type: String,
52 | },
53 | description: {
54 | type: String,
55 | required: true,
56 | },
57 | logo: {
58 | type: String,
59 | required: true,
60 | },
61 | logoType: {
62 | type: String,
63 | },
64 | avatar: {
65 | type: String,
66 | required: true,
67 | },
68 | avatarType: {
69 | type: String,
70 | },
71 | socialBanner: {
72 | type: String,
73 | required: true,
74 | },
75 | socialBannerType: {
76 | type: String,
77 | },
78 | });
79 |
80 | module.exports =
81 | mongoose.models.Profile || mongoose.model("Profile", profileSchema);
82 |
--------------------------------------------------------------------------------
/nextjs-blog/lib/utils/userModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const userSchema = mongoose.Schema({
4 | name: {
5 | type: String,
6 | required: [true, 'Please add a name'],
7 | unique: true,
8 | },
9 |
10 | password: {
11 | type: String,
12 | required: [true, 'Please add a password']
13 | },
14 |
15 | }, {
16 | timestamps: true,
17 | })
18 |
19 | module.exports = mongoose.models.User || mongoose.model('User',userSchema)
--------------------------------------------------------------------------------
/nextjs-blog/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const withBundleAnalyzer = require('@next/bundle-analyzer')({
3 | enabled: process.env.ANALYZE === 'true',
4 | })
5 | const path = require('path');
6 |
7 | // 设置内容安全策略
8 | const ContentSecurityPolicy = `
9 | default-src 'self';
10 | script-src 'self' 'unsafe-eval' 'unsafe-inline';
11 | child-src 'none';
12 | style-src 'self' 'unsafe-inline';
13 | font-src 'self';
14 | media-src 'none';
15 | object-src 'none';
16 | img-src * data:;
17 | `
18 |
19 | const nextConfig = {
20 | i18n: {
21 | locales: ["zh"],
22 | defaultLocale: "zh",
23 | },
24 | reactStrictMode: true,
25 | swcMinify: true,
26 | images: {
27 | domains: ['localhost']
28 | },
29 | sassOptions: {
30 | includePaths: [path.join(__dirname, 'node_modules')],
31 | },
32 | async rewrites() {
33 | return [
34 | {
35 | source: '/api/image/:slug*',
36 | destination: `http://localhost:${process.env.PORT}/api/image/:slug*`
37 | },
38 | {
39 | source: '/api/comments/:slug*',
40 | destination: `http://localhost:${process.env.PORT}/api/comments/:slug*`
41 | },
42 | ]
43 | },
44 | async headers() {
45 | return [
46 | {
47 | source: '/(.*)',
48 | headers: [
49 | {
50 | key: 'Referrer-Policy',
51 | value: 'strict-origin-when-cross-origin',
52 | },
53 | {
54 | key: 'X-Frame-Options',
55 | value: 'DENY',
56 | },
57 | {
58 | key: 'X-Content-Type-Options',
59 | value: 'nosniff',
60 | },
61 | {
62 | key: 'X-DNS-Prefetch-Control',
63 | value: 'on',
64 | },
65 | {
66 | key: 'Strict-Transport-Security',
67 | value: 'max-age=31536000; includeSubDomains',
68 | },
69 | {
70 | key: 'X-XSS-Protection',
71 | value: '1; mode=block'
72 | },
73 | {
74 | key: 'Content-Security-Policy',
75 | value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
76 | }
77 | ],
78 | },
79 | ]
80 | },
81 |
82 | }
83 |
84 | module.exports = withBundleAnalyzer(nextConfig)
85 |
--------------------------------------------------------------------------------
/nextjs-blog/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-blog",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev -p 3001",
7 | "build": "next build",
8 | "start": "next start -p 3001",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@next/bundle-analyzer": "^12.2.5",
13 | "github-markdown-css": "^5.1.0",
14 | "mongoose": "^6.4.4",
15 | "next": "12.2.2",
16 | "next-themes": "^0.2.0",
17 | "parse-numeric-range": "^1.3.0",
18 | "react": "18.2.0",
19 | "react-copy-to-clipboard": "^5.1.0",
20 | "react-dom": "18.2.0",
21 | "react-markdown": "^8.0.3",
22 | "react-syntax-highlighter": "^15.5.0",
23 | "rehype-katex": "^6.0.2",
24 | "rehype-slug": "^5.1.0",
25 | "remark-footnotes": "^4.0.1",
26 | "remark-gfm": "^3.0.1",
27 | "remark-math": "^5.1.1",
28 | "sass": "^1.60.0",
29 | "tocbot": "^4.20.1"
30 | },
31 | "devDependencies": {
32 | "eslint": "8.19.0",
33 | "eslint-config-next": "12.2.2"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/_app.js:
--------------------------------------------------------------------------------
1 | import "../styles/globals.scss";
2 | import "../styles/globals.css";
3 | import "../styles/icomoonStyle.css";
4 |
5 | import { ThemeProvider } from "next-themes";
6 |
7 | function MyApp({ Component, pageProps }) {
8 | return (
9 |
10 |
11 |
12 | );
13 | }
14 |
15 | export default MyApp;
16 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/api/revalidate.js:
--------------------------------------------------------------------------------
1 | import { getPostRelatedPath } from "../../lib/posts";
2 |
3 |
4 | export default async function handler(req, res) {
5 | // Check for secret to confirm this is a valid request
6 | if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
7 | return res.status(401).json({ message: "Invalid token" });
8 | }
9 |
10 | if (req.query.change === "post") {
11 | const revalidatePaths = await getPostRelatedPath();
12 | try {
13 | await Promise.all(revalidatePaths.map((path)=>res.revalidate(path)));
14 | return res.json({ revalidated: true });
15 | } catch (err) {
16 | return res.status(500).send("Error revalidating");
17 | }
18 | } else if (req.query.change === "link") {
19 | try {
20 | await res.revalidate("/links");
21 | return res.json({ revalidated: true });
22 | } catch (err) {
23 | // If there was an error, Next.js will continue
24 | // to show the last successfully generated page
25 | return res.status(500).send("Error revalidating");
26 | }
27 | } else {
28 | return res.status(400).json({ message: "Invalid change value" });
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/categories.js:
--------------------------------------------------------------------------------
1 | import { getAllCategories } from "../lib/posts";
2 | import Link from "next/link";
3 | import { getSiteMetadata } from "../lib/siteData";
4 | import { PageSEO } from "../components/SEO";
5 | import Header from "../components/Header";
6 | import Footer from "../components/Footer";
7 | import styles from "../styles/Categories.module.css";
8 | import styleAni from "../styles/AnimatePublic.module.css";
9 |
10 | export default function Categories({ allCategories, siteMetadata }) {
11 | return (
12 | <>
13 |
20 |
21 |
22 |
23 | {/* */}
26 |
27 | {allCategories.map((category) => {
28 | return (
29 |
30 |
31 | {`\u00A0\u00A0${category === "default" ? "未分类" : category}\u00A0\u00A0`}
32 |
33 |
34 | );
35 | })}
36 |
37 |
38 |
39 |
40 | >
41 | );
42 | }
43 |
44 | export async function getStaticProps() {
45 | const allCategories = await getAllCategories();
46 | const siteMetadata = await getSiteMetadata();
47 | return {
48 | props: {
49 | allCategories,
50 | siteMetadata,
51 | },
52 | };
53 | }
54 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/categories/[category].js:
--------------------------------------------------------------------------------
1 | import { getAllCategories, getPostsByCategory } from "../../lib/posts";
2 | import { getSiteMetadata } from "../../lib/siteData";
3 | import { CategorySEO } from "../../components/SEO";
4 | import generateRss from "../../lib/generate-rss";
5 | import fs from "fs";
6 | import path from "path";
7 | import PostList from "../../components/PostList";
8 | import Header from "../../components/Header";
9 | import Footer from "../../components/Footer";
10 | import styles from "../../styles/CategoryPage.module.css";
11 |
12 | const root = process.cwd();
13 | const DISPLAY_POST_NUMBER = 5;
14 |
15 | export default function PostByCategory({
16 | postsByCategory,
17 | siteMetadata,
18 | category,
19 | }) {
20 | //console.log("postsByCategory: ", postsByCategory);
21 | return (
22 | <>
23 |
30 |
31 |
32 | {`分类 : ${
33 | category !== "default" ? category : "未分类"
34 | }`}
35 |
38 |
39 |
40 | >
41 | );
42 | }
43 |
44 | export async function getStaticPaths() {
45 | const allCategories = await getAllCategories();
46 | const paths = allCategories.map((category) => {
47 | return {
48 | params: {
49 | category,
50 | },
51 | };
52 | });
53 | return {
54 | paths,
55 | fallback: 'blocking',
56 | };
57 | }
58 |
59 | export async function getStaticProps({ params }) {
60 | const postsByCategory = await getPostsByCategory(params.category);
61 | const siteMetadata = await getSiteMetadata();
62 | const sortedPosts = postsByCategory.sort(
63 | (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
64 | );
65 | const filteredPosts = sortedPosts.map((post) => {
66 | return {
67 | date: post.createdAt,
68 | title: post.title,
69 | summary: post.summary ?? "",
70 | tags: post.tags,
71 | category: post.category,
72 | };
73 | });
74 |
75 | // rss
76 | if (filteredPosts.length > 0) {
77 | const rss = generateRss(
78 | filteredPosts,
79 | siteMetadata,
80 | `categories/${params.category}/feed.xml`
81 | );
82 | //console.log("rss: ", rss);
83 | const rssPath = path.join(root, "public", "categories", params.category);
84 | //console.log("rssPath: ", rssPath);
85 | fs.mkdirSync(rssPath, { recursive: true });
86 | fs.writeFileSync(path.join(rssPath, "feed.xml"), rss);
87 | }
88 |
89 | return {
90 | props: {
91 | postsByCategory,
92 | siteMetadata,
93 | category: params.category,
94 | },
95 | };
96 | }
97 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/index.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { useState } from "react";
3 | import styles from "../styles/Home.module.css";
4 | import { getAllPostsData } from "../lib/posts";
5 | import { getSiteMetadata } from "../lib/siteData";
6 | import { PageSEO } from "../components/SEO";
7 | import generateRss from "../lib/generate-rss";
8 | import fs from "fs";
9 | import path from "path";
10 | import Header from "../components/Header";
11 | import SearchBar from "../components/SearchBar";
12 | import PostList from "../components/PostList";
13 | import Footer from "../components/Footer";
14 |
15 | const root = process.cwd();
16 |
17 | const DISPLAY_POST_NUMBER = 10;
18 |
19 | export default function Home({ allPostsData, siteMetadata }) {
20 | const [searchValue, setSearchValue] = useState("");
21 | const [filteredPosts, setFilteredPosts] = useState(allPostsData);
22 | const onChange = (e) => {
23 | setSearchValue(e.target.value);
24 | };
25 | const onClickSearch = () => {
26 | setFilteredPosts(
27 | allPostsData.filter((post) => post.title.includes(searchValue))
28 | );
29 | };
30 | return (
31 | <>
32 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
62 | >
63 | );
64 | }
65 |
66 | export async function getStaticProps() {
67 | const allPostsData = await getAllPostsData();
68 | const siteMetadata = await getSiteMetadata();
69 |
70 | const sortedPosts = allPostsData.sort(
71 | (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
72 | );
73 | const allPosts = sortedPosts.map((post) => {
74 | return {
75 | date: post.createdAt,
76 | title: post.title,
77 | summary: post.summary ?? "",
78 | tags: post.tags,
79 | category: post.category,
80 | };
81 | });
82 |
83 | // rss
84 | if (allPosts.length > 0) {
85 | const rss = generateRss(allPosts, siteMetadata);
86 | const rssPath = path.join(root, "public", "feed.xml");
87 | fs.writeFileSync(rssPath, rss);
88 | }
89 |
90 | //debug
91 | // const postRelatedPath = await getPostRelatedPath();
92 | //console.log("postRelatedPath: ", postRelatedPath);
93 |
94 | return {
95 | props: {
96 | allPostsData,
97 | siteMetadata,
98 | },
99 | };
100 | }
101 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/posts/[slug].js:
--------------------------------------------------------------------------------
1 | import { getPostByTitle, getAllPostTitles } from "../../lib/posts";
2 | import { getSiteMetadata } from "../../lib/siteData";
3 | import { BlogSEO } from "../../components/SEO";
4 | import PostLayout from "../../components/PostLayout";
5 | import ShrinkHeader from "../../components/ShrinkHeader";
6 | import Footer from "../../components/Footer";
7 | import CommentSection from "../../components/CommentSection";
8 |
9 | export default function Post({ post, siteMetadata }) {
10 | const authorDetails = post.authors
11 | ? post.authors.map((author) => {
12 | const name = author === "default" ? siteMetadata.author : author;
13 | return { name };
14 | })
15 | : null;
16 | const postTitle =
17 | post.title.length > 22 ? post.title.substr(0, 22) + "..." : post.title;
18 | return (
19 | <>
20 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | >
43 | );
44 | }
45 |
46 | export async function getStaticPaths() {
47 | const paths = await getAllPostTitles();
48 | return {
49 | paths,
50 | fallback: "blocking",
51 | };
52 | }
53 |
54 | export async function getStaticProps({ params }) {
55 | const posts = await getPostByTitle(params.slug);
56 | const siteMetadata = await getSiteMetadata();
57 | // const postsData = await getAllPostsData();
58 | // const sortedPosts = postsData.sort(
59 | // (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
60 | // );
61 | // const allPosts = sortedPosts.map((post)=>{
62 | // return {
63 | // date: post.createdAt,
64 | // title: post.title,
65 | // summary: post.summary??'',
66 | // tags:post.tags,
67 | // category:post.category
68 | // }
69 | // })
70 |
71 | // rss
72 | // if (allPosts.length > 0) {
73 | // const rss = generateRss(allPosts, siteMetadata);
74 | // fs.writeFileSync("./public/feed.xml", rss);
75 | // }
76 | if(!posts.length) {
77 | return {
78 | notFound: true
79 | }
80 | }
81 |
82 | return {
83 | props: {
84 | post: posts[0],
85 | siteMetadata,
86 | },
87 | };
88 | }
89 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/sitemap.xml.js:
--------------------------------------------------------------------------------
1 | //参考 https://nextjs.org/learn/seo/crawling-and-indexing/xml-sitemaps
2 |
3 | import {getAllTags, getAllCategories, getAllPostTitles} from "../lib/posts";
4 | import {getSiteMetadata} from "../lib/siteData";
5 |
6 | function generateSiteMap(siteMetadata, tags, categories, postSlugArray) {
7 | return `
8 |
9 |
10 | ${siteMetadata.siteUrl}
11 |
12 |
13 | ${siteMetadata.siteUrl}/categories
14 |
15 |
16 | ${siteMetadata.siteUrl}/tags
17 |
18 |
19 | ${siteMetadata.siteUrl}/timeline
20 |
21 |
22 | ${siteMetadata.siteUrl}/links
23 |
24 | ${categories
25 | .map((category) => {
26 | return `
27 |
28 | ${siteMetadata.siteUrl}/categories/${category}
29 |
30 | `;
31 | })
32 | .join('')}
33 | ${tags
34 | .map((tag) => {
35 | return `
36 |
37 | ${siteMetadata.siteUrl}/tags/${tag}
38 |
39 | `;
40 | })
41 | .join('')}
42 | ${postSlugArray
43 | .map((slug) => {
44 | return `
45 |
46 | ${siteMetadata.siteUrl}/posts/${slug}
47 |
48 | `;
49 | })
50 | .join('')}
51 |
52 | `;
53 | }
54 |
55 | function SiteMap() {
56 | // getServerSideProps will do the heavy lifting
57 | }
58 |
59 | export async function getServerSideProps({ res }) {
60 | // We make an API call to gather the URLs for our site
61 | const allTags = await getAllTags();
62 | const allCategories = await getAllCategories();
63 | const allPostTitles = await getAllPostTitles();
64 | const siteMetadata = await getSiteMetadata();
65 |
66 | const tags = allTags.map((t)=>t.tagName);
67 | const postSlugArray = allPostTitles.map((p)=>p.params.slug);
68 |
69 | const sitemap = generateSiteMap(siteMetadata, tags, allCategories, postSlugArray);
70 |
71 | res.setHeader('Content-Type', 'text/xml');
72 | // we send the XML to the browser
73 | res.write(sitemap);
74 | res.end();
75 |
76 | return {
77 | props: {},
78 | };
79 | }
80 |
81 | export default SiteMap;
--------------------------------------------------------------------------------
/nextjs-blog/pages/tags.js:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import { getAllTags } from "../lib/posts";
3 | import { getSiteMetadata } from "../lib/siteData";
4 | import { PageSEO } from "../components/SEO";
5 | import Header from "../components/Header";
6 | import Footer from "../components/Footer";
7 | import styles from "../styles/Tags.module.css";
8 | import styleAni from "../styles/AnimatePublic.module.css";
9 |
10 | export default function Tags({ allTags, siteMetadata }) {
11 | //console.log("allTags: ", allTags);
12 | return (
13 | <>
14 |
21 |
22 |
23 | {/* */}
24 |
25 | {allTags.map((tag) => {
26 | return (
27 |
28 |
29 | {tag.tagName}{tag.value}
30 |
31 |
32 | );
33 | })}
34 |
35 |
36 | >
37 | );
38 | }
39 |
40 | export async function getStaticProps() {
41 | const allTags = await getAllTags();
42 | const siteMetadata = await getSiteMetadata();
43 | return {
44 | props: {
45 | allTags,
46 | siteMetadata,
47 | },
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/tags/[tag].js:
--------------------------------------------------------------------------------
1 | import { getAllTags, getPostsByTag } from "../../lib/posts";
2 | import { getSiteMetadata } from "../../lib/siteData";
3 | import { TagSEO } from "../../components/SEO";
4 | import generateRss from "../../lib/generate-rss";
5 | import fs from "fs";
6 | import path from "path";
7 | import styles from "../../styles/TagPage.module.css";
8 | import PostList from "../../components/PostList";
9 | import Header from "../../components/Header";
10 | import Footer from "../../components/Footer";
11 |
12 | const root = process.cwd();
13 | const DISPLAY_POST_NUMBER = 5;
14 |
15 | export default function PostByTag({ postsData, siteMetadata, tag }) {
16 | return (
17 | <>
18 |
25 |
26 |
27 | {`标签 : ${tag}`}
28 |
31 |
32 |
33 |
34 | >
35 | );
36 | }
37 |
38 | export async function getStaticPaths() {
39 | const allTags = await getAllTags();
40 | const paths = allTags.map((t) => {
41 | return {
42 | params: {
43 | tag: t.tagName,
44 | },
45 | };
46 | });
47 | return {
48 | paths,
49 | fallback: 'blocking',
50 | };
51 | }
52 |
53 | export async function getStaticProps({ params }) {
54 | const postsData = await getPostsByTag(params.tag);
55 | const siteMetadata = await getSiteMetadata();
56 | const sortedPosts = postsData.sort(
57 | (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
58 | );
59 | const filteredPosts = sortedPosts.map((post)=>{
60 | return {
61 | date: post.createdAt,
62 | title: post.title,
63 | summary: post.summary??'',
64 | tags:post.tags,
65 | category:post.category
66 | }
67 | })
68 |
69 | // rss
70 | if (filteredPosts.length > 0) {
71 | const rss = generateRss(
72 | filteredPosts,
73 | siteMetadata,
74 | `tags/${params.tag}/feed.xml`
75 | );
76 | const rssPath = path.join(root, "public", "tags", params.tag);
77 | fs.mkdirSync(rssPath, { recursive: true });
78 | fs.writeFileSync(path.join(rssPath, "feed.xml"), rss);
79 | }
80 |
81 | return {
82 | props: {
83 | postsData,
84 | siteMetadata,
85 | tag: params.tag,
86 | },
87 | };
88 | }
89 |
--------------------------------------------------------------------------------
/nextjs-blog/pages/timeline.js:
--------------------------------------------------------------------------------
1 | import { getMonthArchive } from "../lib/posts";
2 | import { getSiteMetadata } from "../lib/siteData";
3 | import { PageSEO } from "../components/SEO";
4 | import Header from "../components/Header";
5 | import Footer from "../components/Footer";
6 | import PostTimeline from "../components/PostTimeline";
7 | import styleAni from "../styles/AnimatePublic.module.css";
8 |
9 | export default function Timeline({ postArchiveData, siteMetadata }) {
10 | return (
11 | <>
12 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | );
26 | }
27 |
28 | export async function getStaticProps() {
29 | const postArchiveData = await getMonthArchive();
30 | const siteMetadata = await getSiteMetadata();
31 | return {
32 | props: {
33 | postArchiveData,
34 | siteMetadata,
35 | },
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/nextjs-blog/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/nextjs-blog/public/favicon.ico
--------------------------------------------------------------------------------
/nextjs-blog/public/robots.txt:
--------------------------------------------------------------------------------
1 | # Allow all crawlers
2 | User-agent: *
3 | Allow: /
--------------------------------------------------------------------------------
/nextjs-blog/styles/AnimatePublic.module.css:
--------------------------------------------------------------------------------
1 | .fade-in-top {
2 | -webkit-animation: fade-in-top 0.35s cubic-bezier(0.390, 0.575, 0.565, 1.000) both;
3 | animation: fade-in-top 0.35s cubic-bezier(0.390, 0.575, 0.565, 1.000) both;
4 | }
5 |
6 | @-webkit-keyframes fade-in-top {
7 | 0% {
8 | -webkit-transform: translateY(-50px);
9 | transform: translateY(-50px);
10 | opacity: 0;
11 | }
12 |
13 | 100% {
14 | -webkit-transform: translateY(0);
15 | transform: translateY(0);
16 | opacity: 1;
17 | }
18 | }
19 |
20 | @keyframes fade-in-top {
21 | 0% {
22 | -webkit-transform: translateY(-50px);
23 | transform: translateY(-50px);
24 | opacity: 0;
25 | }
26 |
27 | 100% {
28 | -webkit-transform: translateY(0);
29 | transform: translateY(0);
30 | opacity: 1;
31 | }
32 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/Categories.module.css:
--------------------------------------------------------------------------------
1 | .total-num-label {
2 | display: block;
3 | text-align: center;
4 | font-size: 1.6rem;
5 | }
6 |
7 | .all-category-box {
8 | display: flex;
9 | flex-direction: column;
10 | align-items: center;
11 | margin-top: 2rem;
12 | }
13 |
14 | .category {
15 | font-size: 1.25rem;
16 | text-decoration: underline;
17 | text-underline-offset: 0.2rem;
18 | margin: 0.5rem 0;
19 | /* color:rgb(0, 109, 153); */
20 | }
21 |
22 | .category:hover {
23 | color: purple;
24 | }
25 |
26 | @media (max-width: 1224px) {
27 | .category {
28 | text-align: center;
29 | text-decoration: none;
30 | font-size: 1.4rem;
31 | background-color: rgb(239, 239, 239);
32 | border-radius: 8px;
33 | margin: 6px 12px;
34 | padding: 8px 0;
35 | }
36 | .category:hover {
37 | color: initial;
38 | }
39 | .all-category-box {
40 | align-items: stretch;
41 | margin-top: 4px;
42 | }
43 | [data-theme="dark"] .category {
44 | background-color: rgb(79, 79, 79);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/nextjs-blog/styles/CategoryPage.module.css:
--------------------------------------------------------------------------------
1 | .post-list-wrap {
2 | width: max-content;
3 | margin: 0 auto;
4 | }
5 |
6 | .category-page-title {
7 | text-align: center;
8 | border-bottom: 1px solid lightgray;
9 | padding-bottom: 2rem;
10 | width: 70vw;
11 | margin: 0 auto 3rem auto;
12 | font-size: 2rem;
13 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/CommentForm.module.css:
--------------------------------------------------------------------------------
1 | .comment-form-box {
2 | /* border: 1px solid green; */
3 | width: 50rem;
4 | margin: 0 auto;
5 | padding: 2rem;
6 | border-radius: 1rem;
7 | background-color: #4777e612;
8 | }
9 |
10 | [data-theme='dark'] .comment-form-box {
11 | background-color: #3e434e;
12 | }
13 |
14 | .comment-textarea {
15 | display: block;
16 | width: 100%;
17 | height: 8rem;
18 | border-radius: 1rem;
19 | padding: 1rem;
20 | resize: none;
21 | margin: 0;
22 | font-size: 1.1rem;
23 | border-color: lightgray;
24 |
25 | }
26 |
27 | .comment-textarea:focus {
28 | outline: 1px solid #8E54E9;
29 | }
30 |
31 | .comment-textarea::placeholder {
32 | font-size: 1.1rem;
33 | color: gray;
34 | }
35 |
36 | .user-info-wrap {
37 | display: flex;
38 | justify-content: space-between;
39 | }
40 |
41 | .username-input,
42 | .email-input {
43 | margin-top: 1.5rem;
44 | font-size: 1.1rem;
45 | border-radius: 0.8rem;
46 | padding: 0.5rem 1rem;
47 | width: 100%;
48 | border: 1px solid lightgray;
49 | }
50 |
51 | .username-input {
52 | margin-right: 2rem;
53 |
54 | }
55 |
56 | .username-input:focus,
57 | .email-input:focus {
58 | outline: 1px solid #8E54E9;
59 | }
60 |
61 | .username-input::placeholder,
62 | .email-input::placeholder {
63 | font-size: 1rem;
64 | font-weight: 400;
65 | color: lightslategray;
66 | }
67 |
68 |
69 | .submit-button {
70 | display: block;
71 | margin: 2rem auto 0 auto;
72 | background-image: linear-gradient(to right, #4776E6 0%, #8E54E9 51%, #4776E6 100%);
73 | padding: 15px 45px;
74 | text-align: center;
75 | transition: 0.5s;
76 | background-size: 200% auto;
77 | color: white;
78 | box-shadow: 0 0 20px #eee;
79 | border-radius: 10px;
80 | border: none;
81 | cursor: pointer;
82 | }
83 |
84 | [data-theme='dark'] .submit-button {
85 | box-shadow: 0 0 20px rgb(91, 91, 91);
86 | }
87 |
88 | .submit-button:hover {
89 | background-position: right center;
90 | color: #fff;
91 | text-decoration: none;
92 | }
93 |
94 | .notification {
95 | padding: 0.5rem 1.5rem;
96 | border-radius: 1rem;
97 | position: fixed;
98 | top: 15%;
99 | left: 50%;
100 | transform: translate(-50%, 0);
101 | background-color: rgba(1, 1, 108, 0.5);
102 | color: white;
103 | font-size: 1.3rem;
104 | font-weight: 600;
105 | transition: all 1s;
106 | }
107 |
108 | @media (max-width: 1224px) {
109 | .comment-form-box {
110 | width: 95vw;
111 | }
112 |
113 | }
114 |
115 |
--------------------------------------------------------------------------------
/nextjs-blog/styles/Comments.module.css:
--------------------------------------------------------------------------------
1 | .comments-box {
2 | width: 50rem;
3 | margin: 2rem auto 0 auto;
4 | }
5 | .comment-user {
6 | display: inline-block;
7 | margin-right: 2rem;
8 | }
9 | .comment-wrap {
10 | padding: 0.5rem;
11 | border-top: 1px solid lightgray;
12 | }
13 | .comment-user {
14 | font-weight: 600;
15 | font-size: large;
16 |
17 | }
18 | .comment-date {
19 | font-size: 0.9rem;
20 | color:gray;
21 | }
22 | .comment {
23 | padding: 1.5rem;
24 | font-weight: 400;
25 | color:rgb(7, 0, 53);
26 | overflow-x: auto;
27 | white-space: pre-wrap;
28 | }
29 |
30 | [data-theme='dark'] .comment {
31 | color:rgb(226, 222, 255);
32 | }
33 | .comment-label {
34 | font-size: 1.5rem;
35 | position: relative;
36 | }
37 |
38 | .comment-label::before {
39 | content: "";
40 | position: absolute;
41 | left: 5%;
42 | bottom: -0.1rem;
43 | width: 4rem;
44 | height: 0.6rem;
45 | transform: skew(12deg) translateX(-50%);
46 | background: rgba(155, 87, 238, 0.3);
47 |
48 | }
49 |
50 | @media (max-width: 1224px) {
51 | .comments-box {
52 | width: 90vw;
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/CopyButton.module.css:
--------------------------------------------------------------------------------
1 | .copy-button {
2 | font-size: 1.5rem;
3 | color:lightgray;
4 | cursor: pointer;
5 | position: relative;
6 | z-index: 101;
7 | }
8 |
9 | .copy-button:hover {
10 | color: dodgerblue;
11 | }
12 |
13 | .copied {
14 | font-size: 1.5rem;
15 | color:lawngreen;
16 | }
17 |
18 | @media (max-width: 1224px) {
19 | .copied,.copy-button {
20 | font-size: 1rem;
21 | }
22 | .copy-button {
23 | color:rgba(255, 255, 255, 0.545);
24 | }
25 | .copy-button:hover {
26 | color:rgba(255, 255, 255, 0.545);
27 | }
28 |
29 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/Footer.module.css:
--------------------------------------------------------------------------------
1 | .footer {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | margin-top: 10rem;
7 | margin-bottom: 3rem;
8 | }
9 |
10 | .copyright-box {
11 | font-size: 1rem;
12 | line-height: 2rem;
13 | margin-bottom: 1rem;
14 | color: #323236;
15 | font-weight: 500;
16 | }
17 |
18 | .copyright-support {
19 | color: rgb(59, 59, 59);
20 | }
21 |
22 | [data-theme='dark'] .copyright-box {
23 | color: rgb(205, 205, 205);
24 | }
25 | [data-theme='dark'] .copyright-support {
26 | color: rgb(205, 205, 205);
27 | }
28 | .social-info-box>*+* {
29 | margin-left: 1.5rem;
30 | }
31 |
32 | .social-info-box {
33 | margin-bottom: 2rem;
34 | }
35 |
36 | .svg-logo {
37 | height: 2rem;
38 | width: 2rem;
39 | padding: 0.2rem;
40 | background-color: grey;
41 | fill: white;
42 | border-radius: 0.5rem;
43 |
44 | }
45 |
46 | .svg-logo:hover {
47 | background-color: rgb(200, 135, 235);
48 | }
49 |
50 | .wx-icon-box {
51 | position: relative;
52 | }
53 |
54 | .wx-tooltip {}
55 |
56 | .icon-box {
57 | position: relative;
58 | }
59 |
60 | .tooltip {
61 | visibility: hidden;
62 | opacity:0;
63 | background-color: rgba(0, 0, 0, 0.856);
64 | color: white;
65 | border-radius: 0.5rem;
66 | padding: 0.25rem 0.75rem;
67 | position: absolute;
68 |
69 | /* 移动到上面的中间 */
70 | bottom: 2.5rem;
71 | left: 50%;
72 | transform:translateX(-50%);
73 |
74 | transition:visibility 0.8s, opacity 0.8s;
75 |
76 | }
77 |
78 | [data-theme='dark'] .tooltip {
79 | background-color: rgba(138, 138, 138, 0.856);
80 | }
81 |
82 | .icon-box:hover .tooltip {
83 | visibility: visible;
84 | opacity:1;
85 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/Home.module.css:
--------------------------------------------------------------------------------
1 |
2 | .search-post-hr {
3 | border: 0;
4 | height: 1px;
5 | background-color: #eaeaea;
6 | width: 90vw;
7 | margin: 1.5rem auto;
8 | }
9 |
10 | .search-bar-wrap {
11 | margin-left: 10vw;
12 | margin-top: 1rem;
13 | }
14 |
15 | .post-list-wrap {
16 | margin: 0 auto;
17 | width: max-content;
18 | }
19 |
20 | @media (max-width: 1224px) {
21 | .search-post-hr {
22 | height: 1px;
23 | background-color: #a7a7a7;
24 | width: 90vw;
25 | }
26 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/Links.module.css:
--------------------------------------------------------------------------------
1 | .link-icon {
2 | width: 80px;
3 | height: 80px;
4 | padding: 0.5rem;
5 | border-radius: 1rem;
6 | background-color: darkmagenta;
7 | fill: white;
8 | }
9 |
10 | .link-picture {
11 | border-radius: 1rem;
12 |
13 | }
14 |
15 | .grid-wrap {
16 | display: grid;
17 | grid-template-columns: repeat(auto-fit, 26rem);
18 | justify-content: center;
19 | grid-row-gap: 2rem;
20 | grid-column-gap: 3rem;
21 | }
22 |
23 | .link-card {
24 | width: 26rem;
25 | height: 8rem;
26 | box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 5px 0px, rgba(0, 0, 0, 0.1) 0px 0px 1px 0px;
27 | border-radius: 1rem;
28 | display: flex;
29 | align-items: center;
30 | cursor: auto;
31 | padding-left: 1rem;
32 | }
33 |
34 | [data-theme='dark'] .link-card {
35 | background-color: rgb(61, 61, 61);
36 | }
37 | .link-name {
38 | border-bottom: 1px solid lightgray;
39 | padding: 0.5rem 0;
40 | margin:0;
41 | }
42 | .link-info-box {
43 | width: 18rem;
44 | text-align: center;
45 | align-self:flex-start;
46 | padding-left: 0.5rem;
47 | padding-right: 0.5rem;
48 | margin-left: 1rem;
49 | }
50 |
51 | .link-description,.link-url {
52 | padding: 0.5rem 0;
53 | margin:0;
54 |
55 | }
56 |
57 | .link-url {
58 | padding-top: 1rem;
59 | }
60 |
61 | .link-card:hover {
62 | box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px;
63 | transition: all 0.2s;
64 | }
65 |
66 | [data-theme='dark'] .link-card:hover {
67 | box-shadow:none;
68 | outline: 2px solid rgb(201, 90, 201);
69 | }
70 |
71 | @media (max-width: 1224px) {
72 | .grid-wrap {
73 | grid-template-columns: repeat(auto-fit, 25rem);
74 | }
75 | .link-card {
76 | width: 25rem;
77 | }
78 | .link-info-box {
79 | width: 15rem;
80 | }
81 | .link-name {
82 | color:rgb(155, 84, 221);
83 | text-decoration: underline;
84 | }
85 |
86 | }
87 |
88 |
--------------------------------------------------------------------------------
/nextjs-blog/styles/LoadMoreBtn.module.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .container {
4 | display: flex;
5 | justify-content: center;
6 | align-items: center;
7 | margin-top: 3rem;
8 | }
9 |
10 | .center {
11 | position: relative;
12 | }
13 |
14 | .btn {
15 | width: 180px;
16 | height: 60px;
17 | cursor: pointer;
18 | background: #fff;
19 | border: 1px solid lightgray;
20 | outline: none;
21 | transition: 1s ease-in-out;
22 |
23 | }
24 |
25 | [data-theme='dark'] .btn {
26 | background: black;
27 | border: 1px solid lightgray;
28 |
29 | }
30 |
31 | .btn svg {
32 | position: absolute;
33 | left: 0;
34 | top: 0;
35 | fill: none;
36 | stroke: black;
37 | stroke-dasharray: 150 480;
38 | stroke-dashoffset: 150;
39 | transition: 1s ease-in-out;
40 | }
41 |
42 | [data-theme='dark'] .btn svg {
43 | stroke: #fff;
44 | }
45 |
46 | .btn:hover {
47 | transition: 1s ease-in-out;
48 | background: white;
49 |
50 | }
51 | [data-theme='dark'] .btn:hover {
52 | background: black;
53 | }
54 | .btn:active {
55 | transform: scale(0.95);
56 | transition: 0.1s ease-in-out;
57 | }
58 |
59 | .btn:hover svg {
60 | stroke-dashoffset: -480;
61 | }
62 |
63 | .btn span {
64 | color: black;
65 | color: gray;
66 | font-size: 1.3rem;
67 | font-weight: 300;
68 | }
69 |
70 | .btn:hover span {
71 | color: black;
72 | }
73 |
74 | [data-theme='dark'] .btn span {
75 | color:rgb(216, 216, 216);
76 | }
77 |
78 | [data-theme='dark'] .btn:hover span {
79 | color:white;
80 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/MarkDown.module.css:
--------------------------------------------------------------------------------
1 | .preview-post-body pre {
2 | background-color: #ffffff;
3 | }
4 |
5 | .code-header {
6 | display: block;
7 | width: 2rem;
8 | margin-right: 1rem;
9 | margin-left: auto;
10 | position: relative;
11 | height: 2rem;
12 | top: 2.8rem;
13 | visibility: hidden;
14 | margin-top: -3rem;
15 | }
16 |
17 | .code-box:hover .code-header {
18 | visibility: visible;
19 | }
20 |
21 | [data-theme='dark'] .preview-post-body {
22 | background: black;
23 | color: white;
24 | }
25 |
26 | [data-theme='dark'] .preview-post-body pre {
27 | background-color: black;
28 | }
29 |
30 | [data-theme='dark'] .preview-post-body table * {
31 | background: black;
32 | }
33 |
34 | @media (max-width: 1224px) {
35 | .preview-post-body pre {
36 | font-size: 0.6rem;
37 | }
38 |
39 | .code-header {
40 | visibility: visible;
41 | margin-right: 0rem;
42 | top: 2.8rem;
43 | }
44 |
45 | .preview-post-body {
46 | overflow-x: auto;
47 | overflow-y: hidden;
48 | }
49 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/PostList.module.css:
--------------------------------------------------------------------------------
1 | .post-list-li+.post-list-li {
2 | margin-top: 4rem;
3 | }
4 |
5 | .post-list-li {
6 | max-width: 760px;
7 | }
8 |
9 | .post-list-ul {
10 | list-style: none;
11 | }
12 |
13 | .post-title {
14 | position: relative;
15 | }
16 |
17 | .post-title::before {
18 | position: absolute;
19 | top: 0.65rem;
20 | left: -35px;
21 | width: 6px;
22 | height: 6px;
23 | content: '';
24 | transition: border 0.2s ease-out, background 0.2s ease-out;
25 | border: 6px solid #f2f6fa;
26 | border-radius: 12px;
27 | background-color: #d2dbe5;
28 | }
29 |
30 | .post-title:hover::before {
31 | border-color: #f3daff;
32 | background-color: #8000af;
33 | }
34 |
35 | [data-theme='dark'] .post-title::before {
36 | border: 6px solid #6a6c6e;
37 | background-color: #3c4146;
38 | }
39 |
40 | [data-theme='dark'] .post-title:hover::before {
41 | border-color: #eabcff;
42 | background-color: #a600e3;
43 | }
44 |
45 | .post-title {
46 | font-size: 1.6rem;
47 | font-weight: 500;
48 | max-width: 760px;
49 | margin-top: 8px;
50 | margin-bottom: 8px;
51 | }
52 |
53 | .post-date {
54 | color: #596177;
55 | font-weight: 500;
56 | font-size: 1rem;
57 | margin-left: 0.5rem;
58 | margin-top: 0;
59 |
60 | }
61 |
62 | .post-summary {
63 | margin-top: 0.3rem;
64 | font-size: 1.1rem;
65 | color:#596177;
66 | }
67 | [data-theme='dark'] .post-summary {
68 | color: #b1b7c9;
69 | }
70 |
71 | .post-title:hover>*,
72 | .post-title:active>* {
73 | text-decoration: underline;
74 | text-decoration-color: lightgray;
75 | text-underline-offset: 0.5rem;
76 | }
77 |
78 | .post-tag {
79 | background-color: whitesmoke;
80 | padding: 0.25rem 1rem;
81 | border-radius: 1rem;
82 | text-align: center;
83 | margin: 0.5rem;
84 | box-shadow: rgba(0, 0, 0, 0.1) 0px 1px 2px 0px;
85 | color: #8000af;
86 | }
87 |
88 | [data-theme='dark'] .post-tag {
89 | background-color: rgb(141, 141, 141);
90 | box-shadow: rgba(255, 255, 255, 0.3) 0px 1px 2px 0px;
91 | color:#aa00e8;
92 | }
93 |
94 | .tags-box {
95 | display: flex;
96 | flex-wrap: wrap;
97 | width: 360px;
98 | }
99 |
100 | .btn {
101 | width: 180px;
102 | height: 60px;
103 | cursor: pointer;
104 | background: transparent;
105 | border: 1px solid #91C9FF;
106 | outline: none;
107 | transition: 1s ease-in-out;
108 | }
109 |
110 | .btn-svg {
111 | position: absolute;
112 | left: 0;
113 | top: 0;
114 | fill: none;
115 | stroke: #fff;
116 | stroke-dasharray: 150 480;
117 | stroke-dashoffset: 150;
118 | transition: 1s ease-in-out;
119 | }
120 |
121 | .btn:hover {
122 | transition: 1s ease-in-out;
123 | background: #4F95DA;
124 | }
125 |
126 | .btn:hover .btn-svg {
127 | stroke-dashoffset: -480;
128 | }
129 |
130 | .btn span {
131 | color: white;
132 | font-size: 18px;
133 | font-weight: 100;
134 | }
135 |
136 | @media (max-width: 1224px) {
137 | .post-list-li {
138 | max-width: 70vw;
139 | }
140 |
141 | .post-title {
142 | max-width: 70vw;
143 | }
144 |
145 | .tags-box {
146 | width: 60vw;
147 | }
148 |
149 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/PostLoading.module.css:
--------------------------------------------------------------------------------
1 | .loading-box {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | }
6 |
7 | .loading-text-p {
8 | font-size: 22px;
9 | margin-right: 4px;
10 | font-weight: 200;
11 | }
12 |
13 | .lds-ellipsis {
14 | display: inline-block;
15 | position: relative;
16 | width: 80px;
17 | height: 80px;
18 | }
19 | .lds-ellipsis div {
20 | position: absolute;
21 | top: 33px;
22 | width: 13px;
23 | height: 13px;
24 | border-radius: 50%;
25 | background: #c381d9;
26 | animation-timing-function: cubic-bezier(0, 1, 1, 0);
27 | }
28 | .lds-ellipsis div:nth-child(1) {
29 | left: 8px;
30 | animation: lds-ellipsis1 0.6s infinite;
31 | }
32 | .lds-ellipsis div:nth-child(2) {
33 | left: 8px;
34 | animation: lds-ellipsis2 0.6s infinite;
35 | }
36 | .lds-ellipsis div:nth-child(3) {
37 | left: 32px;
38 | animation: lds-ellipsis2 0.6s infinite;
39 | }
40 | .lds-ellipsis div:nth-child(4) {
41 | left: 56px;
42 | animation: lds-ellipsis3 0.6s infinite;
43 | }
44 | @keyframes lds-ellipsis1 {
45 | 0% {
46 | transform: scale(0);
47 | }
48 | 100% {
49 | transform: scale(1);
50 | }
51 | }
52 | @keyframes lds-ellipsis3 {
53 | 0% {
54 | transform: scale(1);
55 | }
56 | 100% {
57 | transform: scale(0);
58 | }
59 | }
60 | @keyframes lds-ellipsis2 {
61 | 0% {
62 | transform: translate(0, 0);
63 | }
64 | 100% {
65 | transform: translate(24px, 0);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/nextjs-blog/styles/PostTimeline.module.css:
--------------------------------------------------------------------------------
1 |
2 |
3 | .post-timeline-wrap {
4 | font-size: 16px;
5 | font-weight: 300;
6 | line-height: 1.5;
7 | letter-spacing: 0.05em;
8 |
9 | }
10 | .timeline {
11 | list-style: none;
12 | margin: 4em auto;
13 | position: relative;
14 | max-width: 50em;
15 | }
16 | .timeline:before {
17 | background-color: rgb(0, 0, 0);
18 | content: '';
19 | margin-left: -1px;
20 | position: absolute;
21 | top: 0;
22 | left: 4.5em;
23 | width: 3px;
24 | height: 100%;
25 | }
26 |
27 | [data-theme='dark'] .timeline:before {
28 | background-color: white;
29 |
30 | }
31 |
32 | .timeline-event {
33 | position: relative;
34 | }
35 | .timeline-event:hover .timeline-event-icon {
36 | transform: rotate(-45deg);
37 | background-color: #5c33a9;
38 |
39 | }
40 | .timeline-event:hover .timeline-event-thumbnail {
41 | box-shadow: inset 20em 0 0 0 #5c33a9;
42 |
43 |
44 | }
45 |
46 | .timeline-event-copy {
47 | padding: 2em;
48 | position: relative;
49 | top: -3.5em;
50 | left: 48px;
51 | width: 80%;
52 |
53 | }
54 | .timeline-event-copy h3 {
55 | font-size: 1.75em;
56 | }
57 | .timeline-event-copy h4 {
58 | font-size: 1.2em;
59 | margin-bottom: 1.2em;
60 | }
61 | .timeline-event-copy strong {
62 | font-weight: 700;
63 | }
64 | .timeline-event-copy p:not(.timeline-event-thumbnail) {
65 | padding-bottom: 1.2em;
66 | }
67 |
68 | .timeline-event-icon {
69 | transition: transform 0.2s ease-in;
70 | transform: rotate(45deg);
71 | background-color: black;
72 | outline: 10px solid white;
73 | display: block;
74 | margin: 0.5em 0.5em 0.5em -0.5em;
75 | position: absolute;
76 | top: 0;
77 | left: 32px;
78 | width: 1em;
79 | height: 1em;
80 | }
81 | [data-theme='dark'] .timeline-event-icon {
82 | outline: 10px solid black;
83 | background-color: white;
84 | }
85 |
86 | .timeline-event-thumbnail {
87 | -moz-transition: box-shadow 0.5s ease-in 0.1s;
88 | -o-transition: box-shadow 0.5s ease-in 0.1s;
89 | -webkit-transition: box-shadow 0.5s ease-in;
90 | -webkit-transition-delay: 0.1s;
91 | transition: box-shadow 0.4s ease-in 0.1s;
92 | color: white;
93 | font-size: 0.75em;
94 | font-size: 1.25em;
95 | background-color: black;
96 | box-shadow: inset 0 0 0 0em #5a62ef;
97 | display: inline-block;
98 | margin-bottom: 1.2em;
99 | padding: 0.25em 1em 0.2em 1em;
100 | }
101 |
102 | [data-theme='dark'] .timeline-event-thumbnail {
103 | background-color: rgb(97, 97, 97);
104 | }
105 |
106 | .post-title a {
107 | text-decoration: underline;
108 | text-decoration-color: gray;
109 | text-underline-offset: 0.5rem;
110 | }
111 |
112 | .post-title:hover {
113 | color:#5c33a9;
114 | }
115 |
116 |
--------------------------------------------------------------------------------
/nextjs-blog/styles/SearchBar.module.css:
--------------------------------------------------------------------------------
1 | .search-box-wrap {
2 | border: 1px solid rgb(222, 222, 222);
3 | width: max-content;
4 | border-radius: 0.5rem;
5 | padding: 0.2rem 1rem;
6 | display: flex;
7 | align-items: center;
8 | }
9 |
10 | [data-theme='dark'] .search-box-wrap {
11 | border: 1px solid rgb(78, 78, 78);
12 | }
13 |
14 | .search-box-wrap:hover {
15 | border: 1px solid rgb(108, 108, 108);
16 | }
17 | .search-box-wrap:focus-within {
18 | border: 1px solid black;
19 | }
20 |
21 | [data-theme='dark'] .search-box-wrap:focus-within {
22 | border: 1px solid white;
23 | }
24 | .search-input {
25 | border: none;
26 | outline: none;
27 | width: 20rem;
28 | font-size: 1.1rem;
29 | }
30 |
31 | .search-button {
32 | border: none;
33 | background-color: inherit;
34 | cursor: pointer;
35 | }
36 |
37 | .search-button > svg {
38 | fill: rgb(222, 222, 222);
39 | }
40 |
41 | .search-button:hover > svg {
42 | fill: gray;
43 | }
44 |
45 | .search-button:active > svg {
46 | transform: scale(0.95);
47 | }
48 |
49 | .search-input::placeholder {
50 | color:rgb(222, 222, 222);
51 | }
52 |
53 | [data-theme='dark'] .search-input::placeholder {
54 | color:rgb(129, 129, 129);
55 | }
56 | [data-theme='dark'] .search-input {
57 | background-color: black;
58 | }
59 |
60 | @media (max-width: 1224px) {
61 | .search-input {
62 | width: 10rem;
63 | }
64 |
65 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/TagPage.module.css:
--------------------------------------------------------------------------------
1 | .tag-page-title {
2 | text-align: center;
3 | border-bottom: 1px solid lightgray;
4 | padding-bottom: 2rem;
5 | width: 70vw;
6 | margin: 0 auto 3rem auto;
7 | font-size: 2rem;
8 | }
9 |
10 | .post-list-wrap {
11 | width: max-content;
12 | margin: 0 auto;
13 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/Tags.module.css:
--------------------------------------------------------------------------------
1 | .tags-box {
2 | text-align: center;
3 | max-width: 65vw;
4 | margin: 0 auto;
5 | display: flex;
6 | flex-wrap: wrap;
7 | justify-content: center;
8 | }
9 | .tag-label {
10 | font-size: 1.5rem;
11 | display: block;
12 | text-align: center;
13 | }
14 | .tag {
15 | margin: 1rem;
16 | background-color: rgb(244, 244, 244);
17 | border-radius: 1.5rem;
18 | padding: 0.5rem 1.5rem;
19 | cursor: pointer;
20 | /* white-space: nowrap; */
21 | position: relative;
22 | }
23 | [data-theme="dark"] .tag {
24 | background-color: rgb(100, 100, 100);
25 | }
26 |
27 | .tag:hover {
28 | background-color: rgb(138, 55, 168);
29 | color: white;
30 | }
31 |
32 | .post-number {
33 | position: absolute;
34 | right: -0.5rem;
35 | top: -0.5rem;
36 | line-height: 1;
37 | display: flex;
38 | justify-content: center;
39 | align-items: center;
40 | height: 1.3rem;
41 | width: 1.3rem;
42 | border-radius: 0.7rem;
43 | background-color: rgba(221, 160, 221, 0.516);
44 | color: gray;
45 | }
46 |
47 | [data-theme="dark"] .post-number {
48 | background-color: rgba(221, 160, 221, 0.93);
49 | color: rgb(230, 230, 230);
50 | }
51 |
52 | @media (max-width: 1224px) {
53 | .tags-box {
54 | max-width: 90vw;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/nextjs-blog/styles/ThemeChanger.module.css:
--------------------------------------------------------------------------------
1 | .sun-svg,.moon-svg {
2 | height: 1.6rem;
3 | width: 1.6rem;
4 | }
5 |
6 | .moon-svg {
7 | fill:rgb(217, 217, 0);
8 | }
9 |
10 |
11 | .theme-button {
12 | background-color: transparent;
13 | border: none;
14 | cursor: pointer;
15 | }
16 |
17 | @media (max-width: 1224px) {
18 | .sun-svg {
19 | fill:black;
20 | }
21 | .theme-button {
22 | display: flex;
23 | align-items: center;
24 | }
25 | .theme-button::before {
26 | content: "主题 : ";
27 | color:black;
28 | font-size: 1.6rem;
29 | margin-right: 0.5rem;
30 | }
31 | [data-theme='dark'] .sun-svg {
32 | fill: white;
33 | }
34 | [data-theme='dark'] .theme-button::before {
35 | color: white;
36 | }
37 |
38 | }
--------------------------------------------------------------------------------
/nextjs-blog/styles/TocAndMD.module.css:
--------------------------------------------------------------------------------
1 | .toc-and-md {
2 | display: flex;
3 | }
4 |
5 | .md-toc {
6 | padding: 1rem;
7 | flex: 0 0 360px;
8 | order: 1;
9 | margin-left: 32px;
10 | position: sticky;
11 | top: 60px;
12 | align-self: flex-start;
13 | font-size: 16px;
14 | }
15 |
16 | .menu-head {
17 | font-size: large;
18 | margin: 4px;
19 | }
20 |
21 | .md-box {
22 | padding: 1rem;
23 | flex: 1;
24 | }
25 |
26 | @media (max-width: 1224px) {
27 | .md-toc {
28 | display: none;
29 | }
30 |
31 | .toc-and-md {
32 | display: initial;
33 | }
34 |
35 | .md-box {
36 | padding: 0;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/nextjs-blog/styles/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/nextjs-blog/styles/fonts/icomoon.eot
--------------------------------------------------------------------------------
/nextjs-blog/styles/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/nextjs-blog/styles/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/nextjs-blog/styles/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ManfredMT/nextjs-blog-react-admin/7ff31f568f03655a5c730038e001870fd7b84dac/nextjs-blog/styles/fonts/icomoon.woff
--------------------------------------------------------------------------------
/nextjs-blog/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
18 | :root {
19 | font-size: calc(0.8em + 0.2vw);
20 | }
21 |
22 | body::-webkit-scrollbar {
23 | width: 9px;
24 | height: 9px;
25 | background-color: transparent;
26 | }
27 |
28 | body::-webkit-scrollbar-track {
29 | border-radius: 16px;
30 | background-color: #e7e7e7;
31 | }
32 |
33 | body::-webkit-scrollbar-thumb {
34 | border-radius: 16px;
35 | background-color: rgb(175, 175, 175);
36 | }
37 |
38 | body::-webkit-scrollbar-thumb:hover {
39 | background-color: rgb(145, 145, 145);
40 | }
41 |
42 | [data="highlight"] {
43 | background-color: #282c34;
44 | display: block;
45 | position: relative;
46 | left: -3px;
47 | border-left: 3px solid rgb(197, 135, 235);
48 | }
49 |
50 | /* 改变Tocbot默认颜色 */
51 | .is-active-link::before {
52 | background-color: #894bbc;
53 | }
54 |
55 | /* 改变Tocbot目录行间距 */
56 | .toc-link {
57 | line-height: 1.3;
58 | }
59 | .toc-list-item {
60 | margin-top: 8px;
61 | margin-bottom: 8px;
62 | }
63 |
64 | /* 使目录可以滚动,需要取消目录折叠 */
65 | .toc {
66 | max-height: 70vh;
67 | overscroll-behavior: contain; /* 阻止滚动链 */
68 | }
69 |
70 | /* 修改目录滚动条 */
71 | .toc::-webkit-scrollbar {
72 | width: 9px;
73 | height: 9px;
74 | background-color: transparent;
75 | }
76 |
77 | .toc::-webkit-scrollbar-track {
78 | border-radius: 16px;
79 | background-color: #e7e7e7;
80 | }
81 |
82 | .toc::-webkit-scrollbar-thumb {
83 | border-radius: 16px;
84 | background-color: rgb(175, 175, 175);
85 | }
86 |
87 | .toc::-webkit-scrollbar-thumb:hover {
88 | background-color: rgb(145, 145, 145);
89 | }
90 |
91 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/nextjs-blog/styles/globals.scss:
--------------------------------------------------------------------------------
1 | @import 'tocbot/src/scss/tocbot';
--------------------------------------------------------------------------------
/nextjs-blog/styles/icomoonStyle.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'icomoon';
3 | src: url('fonts/icomoon.eot?9m6d4w');
4 | src: url('fonts/icomoon.eot?9m6d4w#iefix') format('embedded-opentype'),
5 | url('fonts/icomoon.ttf?9m6d4w') format('truetype'),
6 | url('fonts/icomoon.woff?9m6d4w') format('woff'),
7 | url('fonts/icomoon.svg?9m6d4w#icomoon') format('svg');
8 | font-weight: normal;
9 | font-style: normal;
10 | font-display: block;
11 | }
12 |
13 | [class^="icon-"], [class*=" icon-"] {
14 | /* use !important to prevent issues with browser extensions that change fonts */
15 | font-family: 'icomoon' !important;
16 | speak: never;
17 | font-style: normal;
18 | font-weight: normal;
19 | font-variant: normal;
20 | text-transform: none;
21 | line-height: 1;
22 |
23 | /* Better Font Rendering =========== */
24 | -webkit-font-smoothing: antialiased;
25 | -moz-osx-font-smoothing: grayscale;
26 | }
27 |
28 | .icon-home:before {
29 | content: "\e900";
30 | }
31 | .icon-paste:before {
32 | content: "\e92d";
33 | }
34 | .icon-folder-open:before {
35 | content: "\e930";
36 | }
37 | .icon-price-tag:before {
38 | content: "\e935";
39 | }
40 | .icon-envelop:before {
41 | content: "\e945";
42 | }
43 | .icon-compass:before {
44 | content: "\e949";
45 | }
46 | .icon-clock:before {
47 | content: "\e94e";
48 | }
49 | .icon-calendar:before {
50 | content: "\e953";
51 | }
52 | .icon-drawer:before {
53 | content: "\e95c";
54 | }
55 | .icon-user:before {
56 | content: "\e971";
57 | }
58 | .icon-clipboard:before {
59 | content: "\e9b8";
60 | }
61 | .icon-list2:before {
62 | content: "\e9bb";
63 | }
64 | .icon-menu:before {
65 | content: "\e9bd";
66 | }
67 | .icon-link:before {
68 | content: "\e9cb";
69 | }
70 | .icon-arrow-up2:before {
71 | content: "\ea3a";
72 | }
73 |
--------------------------------------------------------------------------------