├── .gitignore
├── .DS_Store
├── favicon.ico
├── src
├── index.css
├── .DS_Store
├── resources
│ ├── .DS_Store
│ ├── Screenshot.png
│ └── Screenshot2.png
├── page
│ ├── Activity.js
│ ├── 404.js
│ ├── PostList.js
│ ├── Chat.js
│ ├── About.js
│ └── Post.js
├── components
│ ├── Footer.js
│ ├── Loading.js
│ ├── BlogPost.js
│ ├── CalendarHeatmap.js
│ ├── Nav.js
│ └── Container.js
├── config.js
├── index.js
├── tailwind.config.js
├── hooks
│ └── useHash
│ │ └── index.js
├── App.js
├── style.css
├── constantFunction.js
└── output.css
├── public
├── .DS_Store
├── logo.png
├── favicon.ico
├── robots.txt
├── apple-touch-icon.png
├── manifest.json
└── index.html
├── .vercel
├── project.json
└── README.txt
├── .runtime.dockerfile
├── .deploy
├── go-heptabase
│ ├── Makefile
│ ├── local.values.yaml
│ ├── templates
│ │ ├── serviceaccount.yaml
│ │ ├── service.yaml
│ │ ├── tests
│ │ │ └── test-connection.yaml
│ │ ├── hpa.yaml
│ │ ├── NOTES.txt
│ │ ├── _helpers.tpl
│ │ ├── deployment.yaml
│ │ └── ingress.yaml
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── values.yaml
│ └── template.yaml
└── blog.conf
├── tailwind.config.js
├── .github
└── workflows
│ └── main.yml
├── LICENSE
├── .env.development.local
├── package.json
├── .dockerignore
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /.idea
3 | .run
4 | build
5 | .vscode
--------------------------------------------------------------------------------
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/.DS_Store
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/favicon.ico
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/src/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/src/.DS_Store
--------------------------------------------------------------------------------
/public/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/public/.DS_Store
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/public/logo.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/.vercel/project.json:
--------------------------------------------------------------------------------
1 | {"projectId":"prj_CsWgYEwHW4U8Tck8Mt98s8YgWQo9","orgId":"Ss3cmk6hTZAp5wWowtGvJVTu"}
--------------------------------------------------------------------------------
/src/resources/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/src/resources/.DS_Store
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/resources/Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/src/resources/Screenshot.png
--------------------------------------------------------------------------------
/src/resources/Screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/draJiang/Heptabase-Blog/HEAD/src/resources/Screenshot2.png
--------------------------------------------------------------------------------
/.runtime.dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:stable-bullseye
2 | ADD ./.deploy/blog.conf /etc/nginx/nginx.conf
3 | ADD ./build /var/www/
--------------------------------------------------------------------------------
/.deploy/go-heptabase/Makefile:
--------------------------------------------------------------------------------
1 | install:
2 | helm -n blog upgrade frontend . -f local.values.yaml --create-namespace --install --kube-context kiila
3 |
4 | uninstall:
5 | helm -n blog uninstall frontend --kube-context kiila
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [{
5 | "src": "favicon.ico",
6 | "sizes": "64x64 32x32 24x24 16x16",
7 | "type": "image/x-icon"
8 | }],
9 | "start_url": ".",
10 | "display": "standalone",
11 | "theme_color": "#000000",
12 | "background_color": "#ffffff"
13 | }
--------------------------------------------------------------------------------
/.deploy/go-heptabase/local.values.yaml:
--------------------------------------------------------------------------------
1 | ingress:
2 | enabled: true
3 | className: nginx
4 | annotations:
5 | kubernetes.io/tls-acme: "true"
6 | cert-manager.io/cluster-issuer: "letsencrypt-prod"
7 | hosts:
8 | - host: blog.kii.la
9 | paths:
10 | - path: /
11 | pathType: ImplementationSpecific
12 | tls:
13 | - secretName: blog.kii.la
14 | hosts:
15 | - blog.kii.la
--------------------------------------------------------------------------------
/.deploy/go-heptabase/templates/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.serviceAccount.create -}}
2 | apiVersion: v1
3 | kind: ServiceAccount
4 | metadata:
5 | name: {{ include "go-heptabase.serviceAccountName" . }}
6 | labels:
7 | {{- include "go-heptabase.labels" . | nindent 4 }}
8 | {{- with .Values.serviceAccount.annotations }}
9 | annotations:
10 | {{- toYaml . | nindent 4 }}
11 | {{- end }}
12 | {{- end }}
13 |
--------------------------------------------------------------------------------
/.deploy/go-heptabase/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/.deploy/go-heptabase/templates/service.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: {{ include "go-heptabase.fullname" . }}
5 | labels:
6 | {{- include "go-heptabase.labels" . | nindent 4 }}
7 | spec:
8 | type: {{ .Values.service.type }}
9 | ports:
10 | - port: {{ .Values.service.port }}
11 | targetPort: http
12 | protocol: TCP
13 | name: http
14 | selector:
15 | {{- include "go-heptabase.selectorLabels" . | nindent 4 }}
16 |
--------------------------------------------------------------------------------
/src/page/Activity.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Nav from '../components/Nav';
4 | import Footer from '../components/Footer'
5 | import CalendarHeatmap from '../components/CalendarHeatmap'
6 |
7 |
8 | function Activity(props) {
9 |
10 | return
11 |
12 |
13 |
14 |
15 |
16 |
17 | }
18 |
19 | export default Activity;
--------------------------------------------------------------------------------
/.deploy/go-heptabase/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Pod
3 | metadata:
4 | name: "{{ include "go-heptabase.fullname" . }}-test-connection"
5 | labels:
6 | {{- include "go-heptabase.labels" . | nindent 4 }}
7 | annotations:
8 | "helm.sh/hook": test
9 | spec:
10 | containers:
11 | - name: wget
12 | image: busybox
13 | command: ['wget']
14 | args: ['{{ include "go-heptabase.fullname" . }}:{{ .Values.service.port }}']
15 | restartPolicy: Never
16 |
--------------------------------------------------------------------------------
/src/page/404.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Nav from '../components/Nav';
4 | import Footer from '../components/Footer'
5 |
6 |
7 | function Empty(props) {
8 |
9 | document.title = '404'
10 |
11 | return
12 |
13 |
14 |
404 Not Found
15 | Back to Home page
16 |
17 |
18 |
19 |
20 | }
21 |
22 | export default Empty;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // tailwind.config.js
2 | const {nextui} = require("@nextui-org/theme");
3 |
4 | /** @type {import('tailwindcss').Config} */
5 | module.exports = {
6 | content: [
7 | // single component styles
8 | "./node_modules/@nextui-org/theme/dist/components/navbar.js",
9 | // or you can use a glob pattern (multiple component styles)
10 | // './node_modules/@nextui-org/theme/dist/components/(button|snippet|code|input).js'
11 | ],
12 | theme: {
13 | extend: {},
14 | },
15 | darkMode: "class",
16 | plugins: [nextui()],
17 | };
--------------------------------------------------------------------------------
/.vercel/README.txt:
--------------------------------------------------------------------------------
1 | > Why do I have a folder named ".vercel" in my project?
2 | The ".vercel" folder is created when you link a directory to a Vercel project.
3 |
4 | > What does the "project.json" file contain?
5 | The "project.json" file contains:
6 | - The ID of the Vercel project that you linked ("projectId")
7 | - The ID of the user or team your Vercel project is owned by ("orgId")
8 |
9 | > Should I commit the ".vercel" folder?
10 | No, you should not share the ".vercel" folder with anyone.
11 | Upon creation, it will be automatically added to your ".gitignore" file.
12 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { BrowserRouter as Router, Routes, Route, Link, useParams } from 'react-router-dom';
3 |
4 | import '../style.css'
5 |
6 |
7 | function Footer(props) {
8 | let { slug } = useParams();
9 |
10 | useEffect(() => {
11 |
12 |
13 | })
14 |
15 |
16 | return
17 |
18 |
19 | }
20 |
21 | export default Footer;
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | const CONFIG = {
2 | 'ga': 'G-6C4Z9NHW8J',
3 | 'whiteboard_id': 'd4cc3728297609add1a00aab108e90c4e57a1c378cfc2307c251745bf7d2a884',
4 | 'title': '数字花园🌱', // 站点标题
5 | 'pages': {
6 | 'Articles': '9f31ea21-90b9-4523-b8d5-cb33b7a01bda',
7 | 'Projects': '3dd9a603-a7f3-44e9-a6d7-cd2ebda08952',
8 | // 'Activity':'activity', // 花园活跃状态页面
9 | 'About': '3a433c0b-e2e1-4722-8a88-a17e9aa2b927'
10 | },
11 | 'server': '1214087861653995540', // Discord 服务器 ID,非必填,填写后将在网站中显示聊天入口
12 | 'channel': '1214087861653995545' // Discord 频道 ID,非必填,填写后将在网站中显示聊天入口
13 | }
14 |
15 | export default CONFIG
16 |
17 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import { NextUIProvider } from "@nextui-org/system";
5 | import { ThemeProvider as NextThemesProvider } from "next-themes";
6 |
7 | import { createRoot } from 'react-dom/client';
8 | const container = document.getElementById('root');
9 | const root = createRoot(container); // createRoot(container!) if you use TypeScript
10 |
11 | root.render(
12 |
13 | {/* */}
14 |
15 | {/* */}
16 |
17 | )
18 |
19 | // ReactDOM.render( , document.getElementById('root'));
--------------------------------------------------------------------------------
/src/page/PostList.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useUrlState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import Nav from '../components/Nav';
4 | import BlogPost from '../components/BlogPost';
5 | import Container from '../components/Container';
6 | import Footer from '../components/Footer'
7 |
8 | // 笔记列表
9 | function PostList(props) {
10 |
11 | document.title = props.title
12 |
13 | useEffect(()=>{
14 | // console.log('scrollTo(0, 0)');
15 | window.scrollTo(0, 0);
16 |
17 | })
18 |
19 |
20 | return
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | }
30 |
31 | export default PostList;
--------------------------------------------------------------------------------
/src/tailwind.config.js:
--------------------------------------------------------------------------------
1 | // tailwind.config.js
2 | const { nextui } = require("@nextui-org/theme");
3 |
4 | /** @type {import('tailwindcss').Config} */
5 | module.exports = {
6 | content: [
7 | // single component styles
8 | "./src/**/*.{js,jsx,ts,tsx}",
9 | "./node_modules/@nextui-org/theme/dist/components/navbar.js",
10 | ],
11 | theme: {
12 | extend: {},
13 | },
14 | darkMode: "media",
15 | plugins: [nextui()],
16 | };
17 |
18 | // /** @type {import('tailwindcss').Config} */
19 | // module.exports = {
20 | // content: [
21 | // "./src/**/*.{js,jsx,ts,tsx}",
22 | // ],
23 | // theme: {
24 | // extend: {},
25 | // },
26 | // plugins: [],
27 | // }
--------------------------------------------------------------------------------
/src/page/Chat.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import WidgetBot, { API } from '@widgetbot/react-embed'
3 | import Nav from '../components/Nav';
4 |
5 | export default function Chat(props) {
6 |
7 | return (
8 |
9 |
10 |
11 |
12 | 欢迎交流
13 |
14 |
23 |
24 |
25 | )
26 |
27 | }
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Fetch JSON Data
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | fetch-json:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout repository
11 | uses: actions/checkout@v2
12 | with:
13 | ref: 'jiang'
14 |
15 | - name: Fetch JSON data
16 | run: |
17 | curl https://api.dabing.one > src/resources/data.json
18 |
19 | - name: Save JSON data to repository
20 | run: |
21 | git config --global user.email "github-actions[bot]@users.noreply.github.com"
22 | git config --global user.name "GitHub Actions"
23 | git checkout -B jiang
24 | git pull
25 | git add src/resources/data.json # Change data.`json to data.json
26 | git commit -m "Update data.json"
27 | git push origin jiang
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.ACTION_TOKEN }}
30 |
--------------------------------------------------------------------------------
/src/components/Loading.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { BrowserRouter as Router, Routes, Route, Link, useParams } from 'react-router-dom';
3 |
4 | import '../index.css'
5 |
6 | import { Skeleton, Spin, ConfigProvider, Button } from 'antd';
7 | import 'antd/dist/reset.css';
8 |
9 |
10 |
11 |
12 | function Loading(props) {
13 | let { slug } = useParams();
14 | let [rows, setRows] = useState(3)
15 |
16 | useEffect(() => {
17 |
18 | // 设置动态加载样式
19 | let timer
20 | timer = setInterval(() => {
21 | setRows(rows += 2)
22 |
23 | if (rows > 10) {
24 | clearInterval(timer);
25 | }
26 |
27 |
28 | }, 1400);
29 | }, [slug])
30 |
31 |
32 |
33 | // return
34 | return
35 |
36 |
37 | }
38 |
39 | export default Loading;
--------------------------------------------------------------------------------
/src/hooks/useHash/index.js:
--------------------------------------------------------------------------------
1 | import { useState, useCallback, useEffect } from "react";
2 |
3 | const useHash = () => {
4 | const [hash, setHash] = useState(() => window.location.hash);
5 |
6 | // 当hash发生变化时
7 | const handleChangeEvent = useCallback(() => {
8 | setHash(window.location.hash);
9 | }, []);
10 |
11 | useEffect(() => {
12 | // 监听 hashchange ,当 hash 发生变化时执行
13 | window.addEventListener("hashchange", handleChangeEvent);
14 | // 组件卸载时将监听事件进行移除
15 | return () => {
16 | window.removeEventListener("hashchange", handleChangeEvent);
17 | };
18 | }, []);
19 |
20 | // 更新 hash
21 | const updateHash = useCallback(
22 | (newHash) => {
23 | // 只有当新输入的 hash 跟原有的 hanh 不一致时才进行更新
24 | if (newHash !== hash) {
25 | window.location.hash = newHash;
26 | }
27 | }, [hash]
28 | );
29 |
30 | // 将当前的 hash 和 updateHash 暴露出去给外面进行使用
31 | return [hash, updateHash];
32 | };
33 |
34 | export default useHash;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 江子龙
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 |
--------------------------------------------------------------------------------
/.deploy/go-heptabase/templates/hpa.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.autoscaling.enabled }}
2 | apiVersion: autoscaling/v2
3 | kind: HorizontalPodAutoscaler
4 | metadata:
5 | name: {{ include "go-heptabase.fullname" . }}
6 | labels:
7 | {{- include "go-heptabase.labels" . | nindent 4 }}
8 | spec:
9 | scaleTargetRef:
10 | apiVersion: apps/v1
11 | kind: Deployment
12 | name: {{ include "go-heptabase.fullname" . }}
13 | minReplicas: {{ .Values.autoscaling.minReplicas }}
14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }}
15 | metrics:
16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
17 | - type: Resource
18 | resource:
19 | name: cpu
20 | target:
21 | type: Utilization
22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
23 | {{- end }}
24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
25 | - type: Resource
26 | resource:
27 | name: memory
28 | target:
29 | type: Utilization
30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
31 | {{- end }}
32 | {{- end }}
33 |
--------------------------------------------------------------------------------
/.deploy/blog.conf:
--------------------------------------------------------------------------------
1 | events {
2 | worker_connections 1024;
3 | }
4 |
5 | http {
6 | include /etc/nginx/mime.types;
7 |
8 | underscores_in_headers on;
9 |
10 | server {
11 | listen 80 default_server;
12 | root /var/www/;
13 | gzip on;
14 | gzip_min_length 1000;
15 | gzip_types text/plain application/x-javascript application/javascript text/css application/xml application/json;
16 | charset utf-8;
17 | charset_types text/plain application/javascript application/x-javascript application/json text/css;
18 |
19 | location / {
20 | try_files $uri /index.html;
21 | }
22 |
23 | location ~* \.html$ {
24 | add_header Last-Modified $date_gmt;
25 | add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
26 | if_modified_since off;
27 | expires off;
28 | etag off;
29 | }
30 |
31 | location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|mp3|js|css|otf|eot|svg|ttf|woff)$ {
32 | add_header Cache-Control max-age=2592000;
33 | }
34 |
35 | location = /ping {
36 | return 200 'pong';
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.deploy/go-heptabase/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: go-heptabase
3 | description: A Helm chart for Kubernetes
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 0.1.0
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | # It is recommended to use it with quotes.
24 | appVersion: "1.16.0"
25 |
--------------------------------------------------------------------------------
/.env.development.local:
--------------------------------------------------------------------------------
1 | # Created by Vercel CLI
2 | VERCEL="1"
3 | VERCEL_ENV="development"
4 | TURBO_REMOTE_ONLY="true"
5 | NX_DAEMON="false"
6 | VERCEL_URL=""
7 | VERCEL_GIT_PROVIDER=""
8 | VERCEL_GIT_PREVIOUS_SHA=""
9 | VERCEL_GIT_REPO_SLUG=""
10 | VERCEL_GIT_REPO_OWNER=""
11 | VERCEL_GIT_REPO_ID=""
12 | VERCEL_GIT_COMMIT_REF=""
13 | VERCEL_GIT_COMMIT_SHA=""
14 | VERCEL_GIT_COMMIT_MESSAGE=""
15 | VERCEL_GIT_COMMIT_AUTHOR_LOGIN=""
16 | VERCEL_GIT_COMMIT_AUTHOR_NAME=""
17 | VERCEL_GIT_PULL_REQUEST_ID=""
18 | VERCEL_ANALYTICS_ID="SkfRIostrRsfZ8XqPMEH28eD3Os"
19 | POSTGRES_URL="postgres://default:QG8qFwIn4hTY@ep-ancient-tree-596298-pooler.us-east-1.postgres.vercel-storage.com/verceldb"
20 | POSTGRES_URL_NON_POOLING="postgres://default:QG8qFwIn4hTY@ep-ancient-tree-596298.us-east-1.postgres.vercel-storage.com/verceldb"
21 | POSTGRES_PRISMA_URL="postgres://default:QG8qFwIn4hTY@ep-ancient-tree-596298-pooler.us-east-1.postgres.vercel-storage.com/verceldb?pgbouncer=true&connect_timeout=15"
22 | POSTGRES_USER="default"
23 | POSTGRES_HOST="ep-ancient-tree-596298-pooler.us-east-1.postgres.vercel-storage.com"
24 | POSTGRES_PASSWORD="QG8qFwIn4hTY"
25 | POSTGRES_DATABASE="verceldb"
26 | EDGE_CONFIG="https://edge-config.vercel.com/ecfg_4rhkf78f3vf5u9h1xuzjvhdrmyw4?token=c6fc1635-b35e-44db-928a-b53a0103c51b"
27 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
3 | import PostList from './page/PostList';
4 | import Post from './page/Post';
5 | import Chat from './page/Chat';
6 | import Empty from './page/404';
7 | import Activity from './page/Activity';
8 |
9 | import CONFIG from "./config";
10 | // import { NextUIProvider } from "@nextui-org/system";
11 |
12 | // import generateSitemap from './sitemap';
13 |
14 |
15 | // 设置路由
16 | function App() {
17 |
18 | return (
19 | //
20 | //
21 |
22 |
23 | } />
24 | } />
25 | } />
26 | } />
27 | } />
28 |
29 | } />
30 | } />
31 |
32 | {/* */}
33 |
34 |
35 |
36 | //
37 | //
38 | )
39 | }
40 | export default App;
--------------------------------------------------------------------------------
/.deploy/go-heptabase/templates/NOTES.txt:
--------------------------------------------------------------------------------
1 | 1. Get the application URL by running these commands:
2 | {{- if .Values.ingress.enabled }}
3 | {{- range $host := .Values.ingress.hosts }}
4 | {{- range .paths }}
5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
6 | {{- end }}
7 | {{- end }}
8 | {{- else if contains "NodePort" .Values.service.type }}
9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "go-heptabase.fullname" . }})
10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
11 | echo http://$NODE_IP:$NODE_PORT
12 | {{- else if contains "LoadBalancer" .Values.service.type }}
13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available.
14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "go-heptabase.fullname" . }}'
15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "go-heptabase.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
16 | echo http://$SERVICE_IP:{{ .Values.service.port }}
17 | {{- else if contains "ClusterIP" .Values.service.type }}
18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "go-heptabase.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
20 | echo "Visit http://127.0.0.1:8080 to use your application"
21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT
22 | {{- end }}
23 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
28 |
29 |
30 |
31 |
32 |
33 | You need to enable JavaScript to run this app.
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/.deploy/go-heptabase/values.yaml:
--------------------------------------------------------------------------------
1 | # Default values for go-heptabase.
2 | # This is a YAML-formatted file.
3 | # Declare variables to be passed into your templates.
4 |
5 | replicaCount: 1
6 |
7 | image:
8 | repository: ccr.ccs.tencentyun.com/farfalle/blog
9 | pullPolicy: Always
10 | # Overrides the image tag whose default is the chart appVersion.
11 | tag: "latest"
12 |
13 | imagePullSecrets: [ ]
14 | nameOverride: ""
15 | fullnameOverride: ""
16 |
17 | serviceAccount:
18 | # Specifies whether a service account should be created
19 | create: true
20 | # Annotations to add to the service account
21 | annotations: { }
22 | # The name of the service account to use.
23 | # If not set and create is true, a name is generated using the fullname template
24 | name: ""
25 |
26 | podAnnotations: { }
27 |
28 | podSecurityContext: { }
29 | # fsGroup: 2000
30 |
31 | securityContext: { }
32 | # capabilities:
33 | # drop:
34 | # - ALL
35 | # readOnlyRootFilesystem: true
36 | # runAsNonRoot: true
37 | # runAsUser: 1000
38 |
39 | service:
40 | type: ClusterIP
41 | port: 80
42 |
43 | ingress:
44 | enabled: false
45 | className: nginx
46 | annotations:
47 | kubernetes.io/tls-acme: "true"
48 | cert-manager.io/cluster-issuer: "letsencrypt-prod"
49 | hosts: [ ]
50 | tls: [ ]
51 |
52 | resources: { }
53 | # We usually recommend not to specify default resources and to leave this as a conscious
54 | # choice for the user. This also increases chances charts run on environments with little
55 | # resources, such as Minikube. If you do want to specify resources, uncomment the following
56 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
57 | # limits:
58 | # cpu: 100m
59 | # memory: 128Mi
60 | # requests:
61 | # cpu: 100m
62 | # memory: 128Mi
63 |
64 | autoscaling:
65 | enabled: false
66 | minReplicas: 1
67 | maxReplicas: 100
68 | targetCPUUtilizationPercentage: 80
69 | # targetMemoryUtilizationPercentage: 80
70 |
71 | nodeSelector: { }
72 |
73 | tolerations: [ ]
74 |
75 | affinity: { }
76 |
--------------------------------------------------------------------------------
/.deploy/go-heptabase/templates/_helpers.tpl:
--------------------------------------------------------------------------------
1 | {{/*
2 | Expand the name of the chart.
3 | */}}
4 | {{- define "go-heptabase.name" -}}
5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
6 | {{- end }}
7 |
8 | {{/*
9 | Create a default fully qualified app name.
10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
11 | If release name contains chart name it will be used as a full name.
12 | */}}
13 | {{- define "go-heptabase.fullname" -}}
14 | {{- if .Values.fullnameOverride }}
15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
16 | {{- else }}
17 | {{- $name := default .Chart.Name .Values.nameOverride }}
18 | {{- if contains $name .Release.Name }}
19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }}
20 | {{- else }}
21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
22 | {{- end }}
23 | {{- end }}
24 | {{- end }}
25 |
26 | {{/*
27 | Create chart name and version as used by the chart label.
28 | */}}
29 | {{- define "go-heptabase.chart" -}}
30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
31 | {{- end }}
32 |
33 | {{/*
34 | Common labels
35 | */}}
36 | {{- define "go-heptabase.labels" -}}
37 | helm.sh/chart: {{ include "go-heptabase.chart" . }}
38 | {{ include "go-heptabase.selectorLabels" . }}
39 | {{- if .Chart.AppVersion }}
40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
41 | {{- end }}
42 | app.kubernetes.io/managed-by: {{ .Release.Service }}
43 | {{- end }}
44 |
45 | {{/*
46 | Selector labels
47 | */}}
48 | {{- define "go-heptabase.selectorLabels" -}}
49 | app.kubernetes.io/name: {{ include "go-heptabase.name" . }}
50 | app.kubernetes.io/instance: {{ .Release.Name }}
51 | {{- end }}
52 |
53 | {{/*
54 | Create the name of the service account to use
55 | */}}
56 | {{- define "go-heptabase.serviceAccountName" -}}
57 | {{- if .Values.serviceAccount.create }}
58 | {{- default (include "go-heptabase.fullname" .) .Values.serviceAccount.name }}
59 | {{- else }}
60 | {{- default "default" .Values.serviceAccount.name }}
61 | {{- end }}
62 | {{- end }}
63 |
--------------------------------------------------------------------------------
/src/page/About.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { BrowserRouter as Router, Routes, Route, Link, useParams } from 'react-router-dom';
3 |
4 | import Container from '../components/Container'
5 | import Nav from '../components/Nav';
6 | import Footer from '../components/Footer'
7 | import '../style.css'
8 | import Loading from '../components/Loading'
9 |
10 | import { getHeptabaseData, getClearCard, getClearImag } from '../constantFunction'
11 |
12 |
13 |
14 |
15 | function About(props) {
16 | let { slug } = useParams();
17 | let [isLoading, setLoadingState] = useState(true)
18 | let [page_id, setPageID] = useState('');
19 |
20 | document.title = props.title
21 |
22 | useEffect(() => {
23 | // console.log('scrollTo(0, 0)');
24 | window.scrollTo(0, 0);
25 | window.history.scrollRestoration = 'auto';
26 |
27 | })
28 |
29 | // 获取 About 数据的 ID
30 | let heptabase_blog_data
31 |
32 |
33 | getHeptabaseData.then((res) => {
34 | heptabase_blog_data = res.data
35 |
36 | if (res['pages']['about'] != undefined) {
37 | setLoadingState(false)
38 | setPageID(res['pages']['about']['id'])
39 | } else {
40 | // 404
41 | window.location = '/404'
42 | }
43 |
44 | })
45 |
46 | let content =
47 | if (page_id != '' || isLoading !== true) {
48 | content =
49 | } else {
50 | // content =
51 | }
52 |
53 |
54 | // return
55 |
56 | if (page_id != '' || isLoading !== true) {
57 | return
58 |
59 |
60 |
61 |
62 |
63 |
;
64 | } else {
65 | return
66 |
67 |
68 |
69 |
70 |
71 |
;
72 | }
73 |
74 |
75 | }
76 |
77 | export default About;
--------------------------------------------------------------------------------
/.deploy/go-heptabase/templates/deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ include "go-heptabase.fullname" . }}
5 | labels:
6 | {{- include "go-heptabase.labels" . | nindent 4 }}
7 | spec:
8 | {{- if not .Values.autoscaling.enabled }}
9 | replicas: {{ .Values.replicaCount }}
10 | {{- end }}
11 | selector:
12 | matchLabels:
13 | {{- include "go-heptabase.selectorLabels" . | nindent 6 }}
14 | template:
15 | metadata:
16 | {{- with .Values.podAnnotations }}
17 | annotations:
18 | {{- toYaml . | nindent 8 }}
19 | {{- end }}
20 | labels:
21 | {{- include "go-heptabase.selectorLabels" . | nindent 8 }}
22 | spec:
23 | {{- with .Values.imagePullSecrets }}
24 | imagePullSecrets:
25 | {{- toYaml . | nindent 8 }}
26 | {{- end }}
27 | serviceAccountName: {{ include "go-heptabase.serviceAccountName" . }}
28 | securityContext:
29 | {{- toYaml .Values.podSecurityContext | nindent 8 }}
30 | containers:
31 | - name: {{ .Chart.Name }}
32 | securityContext:
33 | {{- toYaml .Values.securityContext | nindent 12 }}
34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
35 | imagePullPolicy: {{ .Values.image.pullPolicy }}
36 | ports:
37 | - name: http
38 | containerPort: {{ .Values.service.port }}
39 | protocol: TCP
40 | livenessProbe:
41 | httpGet:
42 | path: /
43 | port: http
44 | readinessProbe:
45 | httpGet:
46 | path: /
47 | port: http
48 | resources:
49 | {{- toYaml .Values.resources | nindent 12 }}
50 | {{- with .Values.nodeSelector }}
51 | nodeSelector:
52 | {{- toYaml . | nindent 8 }}
53 | {{- end }}
54 | {{- with .Values.affinity }}
55 | affinity:
56 | {{- toYaml . | nindent 8 }}
57 | {{- end }}
58 | {{- with .Values.tolerations }}
59 | tolerations:
60 | {{- toYaml . | nindent 8 }}
61 | {{- end }}
62 |
--------------------------------------------------------------------------------
/.deploy/go-heptabase/templates/ingress.yaml:
--------------------------------------------------------------------------------
1 | {{- if .Values.ingress.enabled -}}
2 | {{- $fullName := include "go-heptabase.fullname" . -}}
3 | {{- $svcPort := .Values.service.port -}}
4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
7 | {{- end }}
8 | {{- end }}
9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
10 | apiVersion: networking.k8s.io/v1
11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
12 | apiVersion: networking.k8s.io/v1beta1
13 | {{- else -}}
14 | apiVersion: extensions/v1beta1
15 | {{- end }}
16 | kind: Ingress
17 | metadata:
18 | name: {{ $fullName }}
19 | labels:
20 | {{- include "go-heptabase.labels" . | nindent 4 }}
21 | {{- with .Values.ingress.annotations }}
22 | annotations:
23 | {{- toYaml . | nindent 4 }}
24 | {{- end }}
25 | spec:
26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
27 | ingressClassName: {{ .Values.ingress.className }}
28 | {{- end }}
29 | {{- if .Values.ingress.tls }}
30 | tls:
31 | {{- range .Values.ingress.tls }}
32 | - hosts:
33 | {{- range .hosts }}
34 | - {{ . | quote }}
35 | {{- end }}
36 | secretName: {{ .secretName }}
37 | {{- end }}
38 | {{- end }}
39 | rules:
40 | {{- range .Values.ingress.hosts }}
41 | - host: {{ .host | quote }}
42 | http:
43 | paths:
44 | {{- range .paths }}
45 | - path: {{ .path }}
46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
47 | pathType: {{ .pathType }}
48 | {{- end }}
49 | backend:
50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
51 | service:
52 | name: {{ $fullName }}
53 | port:
54 | number: {{ $svcPort }}
55 | {{- else }}
56 | serviceName: {{ $fullName }}
57 | servicePort: {{ $svcPort }}
58 | {{- end }}
59 | {{- end }}
60 | {{- end }}
61 | {{- end }}
62 |
--------------------------------------------------------------------------------
/src/components/BlogPost.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import localStorage from 'localStorage';
4 | import { format } from 'date-fns'
5 | import { getHeptabaseData } from '../constantFunction'
6 |
7 | import Loading from '../components/Loading'
8 |
9 | // 文章列表
10 | class BlogPost extends React.Component {
11 |
12 | constructor(props) {
13 | super(props);
14 | this.state = { posts: [], isLoading: true };
15 | }
16 |
17 | componentDidMount() {
18 | window.history.scrollRestoration = 'auto';
19 |
20 | // 设置网页标题
21 | document.title = 'Notes'
22 |
23 | // 获取数据
24 | getHeptabaseData.then((res) => {
25 | this.setState({ posts: res.data, isLoading: false })
26 | })
27 |
28 | console.log('BlogPost componentDidMount end');
29 |
30 | }
31 |
32 | componentDidUpdate() {
33 | console.log('BlogPost componentDidUpdate');
34 | console.log(this.state.posts);
35 | }
36 |
37 | handlePostClick(){
38 | // 记录跳转类型
39 | sessionStorage.setItem('nav_type', 3)
40 | // 记录当前滚动的位置
41 | // sessionStorage.setItem('scrollY', window.scrollY)
42 | }
43 |
44 | render() {
45 |
46 | if (this.state.isLoading == false && this.state.posts != undefined) {
47 | // 加载完毕
48 | console.log(this.state.posts);
49 | let posts = this.state.posts.cards
50 |
51 | let postList
52 | if (posts != undefined && posts != null) {
53 |
54 | postList = posts.map((post) =>
55 |
56 |
57 | {/* */}
58 |
59 |
62 |
63 | {post.lastEditedTimeDiff}
64 |
65 |
66 | )
67 | }
68 |
69 | return (
70 |
73 | );
74 | }
75 |
76 | // 加载中
77 | return
78 |
79 | }
80 | }
81 |
82 | export default BlogPost;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "heptabase_blog",
3 | "version": "1.0.0",
4 | "private": true,
5 | "proxy": "http://0.0.0.0:3000",
6 | "dependencies": {
7 | "@ant-design/icons": "^4.8.0",
8 | "@nextui-org/navbar": "^2.0.27",
9 | "@nextui-org/system": "^2.0.15",
10 | "@nextui-org/theme": "^2.1.17",
11 | "@testing-library/jest-dom": "^5.16.5",
12 | "@testing-library/react": "^13.4.0",
13 | "@testing-library/user-event": "^13.5.0",
14 | "@uiw/react-heat-map": "^2.0.5",
15 | "@uiw/react-tooltip": "^4.21.23",
16 | "@vercel/postgres": "^0.3.0",
17 | "@widgetbot/react-embed": "^1.9.0",
18 | "antd": "^5.1.1",
19 | "chalk": "^5.3.0",
20 | "clipboard": "^2.0.11",
21 | "copy-to-clipboard": "^3.3.3",
22 | "cors": "^2.8.5",
23 | "date-fns": "^2.29.3",
24 | "express": "^4.18.2",
25 | "fetch-jsonp": "^1.2.3",
26 | "framer-motion": "^10.17.6",
27 | "github-markdown-css": "^5.1.0",
28 | "highlight.js": "^11.7.0",
29 | "jsonp": "^0.2.1",
30 | "localStorage": "^1.0.4",
31 | "next-themes": "^0.2.1",
32 | "react": "^18.2.0",
33 | "react-dom": "^18.2.0",
34 | "react-ga": "^3.3.1",
35 | "react-markdown": "^8.0.3",
36 | "react-router-dom": "^6.21.1",
37 | "react-router-sitemap-maker": "^1.1.0",
38 | "react-scripts": "^5.0.1",
39 | "react-syntax-highlighter": "^15.5.0",
40 | "react-tooltip": "^5.7.5",
41 | "rehype-raw": "^6.1.1",
42 | "remark-gfm": "^3.0.1",
43 | "semver": "^7.5.4",
44 | "showdown": "^2.1.0",
45 | "web-vitals": "^2.1.4"
46 | },
47 | "scripts": {
48 | "start": "react-scripts start",
49 | "build": "react-scripts build",
50 | "test": "react-scripts 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 | "tailwindcss": "^3.4.3"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | ### Node template
2 | # Logs
3 | logs
4 | *.log
5 | npm-debug.log*
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
13 |
14 | # Runtime data
15 | pids
16 | *.pid
17 | *.seed
18 | *.pid.lock
19 |
20 | # Directory for instrumented libs generated by jscoverage/JSCover
21 | lib-cov
22 |
23 | # Coverage directory used by tools like istanbul
24 | coverage
25 | *.lcov
26 |
27 | # nyc test coverage
28 | .nyc_output
29 |
30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
31 | .grunt
32 |
33 | # Bower dependency directory (https://bower.io/)
34 | bower_components
35 |
36 | # node-waf configuration
37 | .lock-wscript
38 |
39 | # Compiled binary addons (https://nodejs.org/api/addons.html)
40 | build/Release
41 |
42 | # Dependency directories
43 | node_modules/
44 | jspm_packages/
45 |
46 | # Snowpack dependency directory (https://snowpack.dev/)
47 | web_modules/
48 |
49 | # TypeScript cache
50 | *.tsbuildinfo
51 |
52 | # Optional npm cache directory
53 | .npm
54 |
55 | # Optional eslint cache
56 | .eslintcache
57 |
58 | # Optional stylelint cache
59 | .stylelintcache
60 |
61 | # Microbundle cache
62 | .rpt2_cache/
63 | .rts2_cache_cjs/
64 | .rts2_cache_es/
65 | .rts2_cache_umd/
66 |
67 | # Optional REPL history
68 | .node_repl_history
69 |
70 | # Output of 'npm pack'
71 | *.tgz
72 |
73 | # Yarn Integrity file
74 | .yarn-integrity
75 |
76 | # dotenv environment variable files
77 | .env
78 | .env.development.local
79 | .env.test.local
80 | .env.production.local
81 | .env.local
82 |
83 | # parcel-bundler cache (https://parceljs.org/)
84 | .cache
85 | .parcel-cache
86 |
87 | # Next.js build output
88 | .next
89 | out
90 |
91 | # Nuxt.js build / generate output
92 | .nuxt
93 | dist
94 |
95 | # Gatsby files
96 | .cache/
97 | # Comment in the public line in if your project uses Gatsby and not Next.js
98 | # https://nextjs.org/blog/next-9-1#public-directory-support
99 | # public
100 |
101 | # vuepress build output
102 | .vuepress/dist
103 |
104 | # vuepress v2.x temp and cache directory
105 | .temp
106 | .cache
107 |
108 | # Docusaurus cache and generated files
109 | .docusaurus
110 |
111 | # Serverless directories
112 | .serverless/
113 |
114 | # FuseBox cache
115 | .fusebox/
116 |
117 | # DynamoDB Local files
118 | .dynamodb/
119 |
120 | # TernJS port file
121 | .tern-port
122 |
123 | # Stores VSCode versions used for testing VSCode extensions
124 | .vscode-test
125 |
126 | # yarn v2
127 | .yarn/cache
128 | .yarn/unplugged
129 | .yarn/build-state.yml
130 | .yarn/install-state.gz
131 | .pnp.*
132 |
133 |
--------------------------------------------------------------------------------
/.deploy/go-heptabase/template.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Source: go-heptabase/templates/serviceaccount.yaml
3 | apiVersion: v1
4 | kind: ServiceAccount
5 | metadata:
6 | name: go-heptabase
7 | labels:
8 | helm.sh/chart: go-heptabase-0.1.0
9 | app.kubernetes.io/name: go-heptabase
10 | app.kubernetes.io/instance: go-heptabase
11 | app.kubernetes.io/version: "1.16.0"
12 | app.kubernetes.io/managed-by: Helm
13 | ---
14 | # Source: go-heptabase/templates/configmap.yaml
15 | apiVersion: v1
16 | kind: ConfigMap
17 | metadata:
18 | name: go-heptabase
19 | labels:
20 | helm.sh/chart: go-heptabase-0.1.0
21 | app.kubernetes.io/name: go-heptabase
22 | app.kubernetes.io/instance: go-heptabase
23 | app.kubernetes.io/version: "1.16.0"
24 | app.kubernetes.io/managed-by: Helm
25 | data:
26 | config.yaml: |-
27 | env: debug
28 | heptabase:
29 | shared_id: 5b32b02b51a359eb2e4d5bbbfe41003a043924131f374c0950f444f79fdf65e0
30 | timeout: 10
31 | ---
32 | # Source: go-heptabase/templates/service.yaml
33 | apiVersion: v1
34 | kind: Service
35 | metadata:
36 | name: go-heptabase
37 | labels:
38 | helm.sh/chart: go-heptabase-0.1.0
39 | app.kubernetes.io/name: go-heptabase
40 | app.kubernetes.io/instance: go-heptabase
41 | app.kubernetes.io/version: "1.16.0"
42 | app.kubernetes.io/managed-by: Helm
43 | spec:
44 | type: ClusterIP
45 | ports:
46 | - port: 80
47 | targetPort: http
48 | protocol: TCP
49 | name: http
50 | selector:
51 | app.kubernetes.io/name: go-heptabase
52 | app.kubernetes.io/instance: go-heptabase
53 | ---
54 | # Source: go-heptabase/templates/deployment.yaml
55 | apiVersion: apps/v1
56 | kind: Deployment
57 | metadata:
58 | name: go-heptabase
59 | labels:
60 | helm.sh/chart: go-heptabase-0.1.0
61 | app.kubernetes.io/name: go-heptabase
62 | app.kubernetes.io/instance: go-heptabase
63 | app.kubernetes.io/version: "1.16.0"
64 | app.kubernetes.io/managed-by: Helm
65 | spec:
66 | replicas: 1
67 | selector:
68 | matchLabels:
69 | app.kubernetes.io/name: go-heptabase
70 | app.kubernetes.io/instance: go-heptabase
71 | template:
72 | metadata:
73 | labels:
74 | app.kubernetes.io/name: go-heptabase
75 | app.kubernetes.io/instance: go-heptabase
76 | spec:
77 | serviceAccountName: go-heptabase
78 | securityContext:
79 | {}
80 | containers:
81 | - name: go-heptabase
82 | volumeMounts:
83 | - mountPath: /usr/share/nginx/html
84 | name: go-heptabase
85 | securityContext:
86 | {}
87 | image: "ccr.ccs.tencentyun.com/farfalle/blog:server-latest"
88 | imagePullPolicy: IfNotPresent
89 | ports:
90 | - name: http
91 | containerPort: 80
92 | protocol: TCP
93 | livenessProbe:
94 | httpGet:
95 | path: /
96 | port: http
97 | readinessProbe:
98 | httpGet:
99 | path: /
100 | port: http
101 | resources:
102 | {}
103 | volumes:
104 | - name: go-heptabase
105 | hostPath:
106 | path: /go-heptabase
107 | ---
108 | # Source: go-heptabase/templates/ingress.yaml
109 | apiVersion: networking.k8s.io/v1
110 | kind: Ingress
111 | metadata:
112 | name: go-heptabase
113 | labels:
114 | helm.sh/chart: go-heptabase-0.1.0
115 | app.kubernetes.io/name: go-heptabase
116 | app.kubernetes.io/instance: go-heptabase
117 | app.kubernetes.io/version: "1.16.0"
118 | app.kubernetes.io/managed-by: Helm
119 | annotations:
120 | cert-manager.io/cluster-issuer: letsencrypt-prod
121 | kubernetes.io/tls-acme: "true"
122 | spec:
123 | ingressClassName: nginx
124 | tls:
125 | - hosts:
126 | - "go-heptabase.vs-game.com"
127 | secretName: go-heptabase.vs-game.com
128 | rules:
129 | - host: "go-heptabase.vs-game.com"
130 | http:
131 | paths:
132 | - path: /
133 | pathType: ImplementationSpecific
134 | backend:
135 | service:
136 | name: go-heptabase
137 | port:
138 | number: 80
139 | ---
140 | # Source: go-heptabase/templates/tests/test-connection.yaml
141 | apiVersion: v1
142 | kind: Pod
143 | metadata:
144 | name: "go-heptabase-test-connection"
145 | labels:
146 | helm.sh/chart: go-heptabase-0.1.0
147 | app.kubernetes.io/name: go-heptabase
148 | app.kubernetes.io/instance: go-heptabase
149 | app.kubernetes.io/version: "1.16.0"
150 | app.kubernetes.io/managed-by: Helm
151 | annotations:
152 | "helm.sh/hook": test
153 | spec:
154 | containers:
155 | - name: wget
156 | image: busybox
157 | command: ['wget']
158 | args: ['go-heptabase:80']
159 | restartPolicy: Never
160 |
--------------------------------------------------------------------------------
/src/components/CalendarHeatmap.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useUrlState } from 'react';
2 |
3 | import Tooltip from '@uiw/react-tooltip';
4 | import HeatMap from '@uiw/react-heat-map';
5 |
6 | import { getHeptabaseData } from '../constantFunction'
7 |
8 | import '../style.css'
9 |
10 | let heptabase_blog_data = undefined
11 |
12 | let cards = undefined
13 | let myValue = []
14 | let dateList = []
15 |
16 |
17 | console.log(heptabase_blog_data);
18 |
19 | const CalendarHeatmap = () => {
20 |
21 | // 开始时间
22 | let sd = new Date() //今天
23 | sd.setFullYear(sd.getFullYear() - 1) //一年前的今天
24 |
25 | const [startDate, setStartDate] = useState(sd);
26 | const [value, setValue] = useState();
27 |
28 |
29 | useEffect(() => {
30 | // 处理 Hepta 数据
31 |
32 | // 获取 Heptabase 数据
33 | if (myValue.length <= 0) {
34 | getHeptabaseData().then((res) => {
35 |
36 | heptabase_blog_data = res.data
37 | console.log('CalendarHeatmap getHeptabaseData');
38 |
39 | cards = heptabase_blog_data['cards']
40 | for (let i = cards.length - 1; i > -1; i--) {
41 |
42 | //将 TZ 时间转为本地时间 yyyy-mm-dd
43 | let date = new Date(cards[i]['lastEditedTime']),
44 | month = '' + (date.getMonth() + 1),
45 | day = '' + date.getDate(),
46 | year = date.getFullYear();
47 |
48 | if (month.length < 2)
49 | month = '0' + month;
50 | if (day.length < 2)
51 | day = '0' + day;
52 |
53 | let dateStr = [year, month, day].join('/');
54 |
55 | // 判断 value 中是否有此时间,有则追加 count
56 | if (dateList.indexOf(dateStr) > -1) {
57 | myValue[dateList.indexOf(dateStr)]['count'] += 1
58 | } else {
59 | myValue.push({ 'date': dateStr, 'count': 1 })
60 | dateList.push(dateStr)
61 | }
62 |
63 | }
64 |
65 | setValue(myValue)
66 | console.log('setValue(myValue)');
67 |
68 | })
69 | } else if (value === undefined) {
70 | setValue(myValue)
71 | }
72 | setMapWidth()
73 |
74 | })
75 |
76 | const setMapWidth = () => {
77 | let windowWidth = window.innerWidth
78 | // console.log(windowWidth);
79 |
80 | let heatmapDiv = document.getElementsByClassName('calendarHeatmap')[0]
81 | // console.log(heatmapDiv);
82 | if (heatmapDiv === undefined) {
83 | return
84 | }
85 |
86 | // 调整 map 的尺寸
87 |
88 | if (windowWidth >= 780 && heatmapDiv.getAttribute('map-width') !== '12') {
89 |
90 | let sd = new Date() // 今天
91 | sd.setMonth(sd.getMonth() - 12) // x 个月前的今天
92 | setStartDate(sd)
93 |
94 | //标记样式
95 | heatmapDiv.setAttribute('map-width', '12')
96 | }
97 |
98 | if (windowWidth >= 680 && windowWidth < 780 && heatmapDiv.getAttribute('map-width') !== '10') {
99 |
100 | let sd = new Date() // 今天
101 | sd.setMonth(sd.getMonth() - 10) // x 个月前的今天
102 | setStartDate(sd)
103 |
104 | //标记样式
105 | heatmapDiv.setAttribute('map-width', '10')
106 | }
107 |
108 | if (windowWidth >= 580 && windowWidth < 680 && heatmapDiv.getAttribute('map-width') !== '9') {
109 |
110 | let sd = new Date() // 今天
111 | sd.setMonth(sd.getMonth() - 9) // x 个月前的今天
112 | setStartDate(sd)
113 |
114 | //标记样式
115 | heatmapDiv.setAttribute('map-width', '9')
116 | }
117 |
118 | if (windowWidth >= 480 && windowWidth < 580 && heatmapDiv.getAttribute('map-width') !== '7') {
119 |
120 | let sd = new Date() // 今天
121 | sd.setMonth(sd.getMonth() - 7) // x 个月前的今天
122 | setStartDate(sd)
123 |
124 | //标记样式
125 | heatmapDiv.setAttribute('map-width', '7')
126 | }
127 |
128 | if (windowWidth >= 375 && windowWidth < 480 && heatmapDiv.getAttribute('map-width') !== '6') {
129 |
130 | let sd = new Date() // 今天
131 | sd.setMonth(sd.getMonth() - 6) // x 个月前的今天
132 | setStartDate(sd)
133 |
134 | //标记样式
135 | heatmapDiv.setAttribute('map-width', '6')
136 | }
137 |
138 | if (windowWidth < 375 && heatmapDiv.getAttribute('map-width') !== 'small') {
139 |
140 | let sd = new Date() // 今天
141 | sd.setMonth(sd.getMonth() - 4) // x 个月前的今天
142 | setStartDate(sd)
143 |
144 | //标记样式
145 | heatmapDiv.setAttribute('map-width', 'small')
146 | }
147 | }
148 |
149 |
150 | window.addEventListener("resize", (event) => {
151 |
152 | setMapWidth()
153 |
154 | });
155 |
156 |
157 | return (
158 |
159 | {
168 | // if (!data.count) return ;
169 | return (
170 |
171 |
172 |
173 | );
174 | }}
175 |
176 | />
177 |
178 | )
179 | };
180 | export default CalendarHeatmap
--------------------------------------------------------------------------------
/src/components/Nav.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import CONFIG from '../config'
4 | import { Navbar, NavbarBrand, NavbarContent, NavbarItem, NavbarMenuToggle, NavbarMenu, NavbarMenuItem } from "@nextui-org/navbar";
5 |
6 |
7 | // 页面头部
8 | function Nav(props) {
9 |
10 | const [showChatWindow, setShowChatWindow] = useState(false);
11 |
12 |
13 | const handleNavBarClick = (e) => {
14 | console.log('handleNavBarClick');
15 | // 记录跳转类型,实现打开新卡片后定位到卡片顶部
16 | sessionStorage.setItem('nav_type', 3)
17 |
18 | }
19 |
20 | const handleShowChatWindow = () => {
21 | props.handleShowChatWindow()
22 | setShowChatWindow(!showChatWindow)
23 | }
24 |
25 | // 加载 Tabs
26 | let tabs = []
27 | Object.keys(CONFIG['pages']).forEach(key => {
28 |
29 | let page
30 | if (key === 'Activity') {
31 | page =
32 | Activity
33 |
34 | } else {
35 | page =
36 | {key}
37 |
38 | }
39 |
40 | tabs.push(page)
41 |
42 | });
43 |
44 | return (
45 |
46 | < Navbar shouldHideOnScroll isBlurred={false} maxWidth={'full'} height={'3rem'} isBordered={true} >
47 |
48 |
49 |
50 |
53 | {tabs}
54 |
55 | {(props.discord && CONFIG.server && CONFIG.channel) &&
56 |
65 |
66 | Chat
67 |
68 | }
69 |
70 |
71 | )
72 | }
73 |
74 | export default Nav;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 使用 Heptabase 管理数字花园
2 |
3 | ## 是什么
4 |
5 | 在 Heptabase 中编写笔记,自动同步到个人的数字花园中。
6 |
7 | - [数字花园地址](https://notes.dabing.one/)
8 |
9 | - [原始白板地址](https://app.heptabase.com/w/d4cc3728297609add1a00aab108e90c4e57a1c378cfc2307c251745bf7d2a884)
10 |
11 | - Heptabase CSS 预览
12 |
13 | 
14 |
15 | 目前支持的功能
16 |
17 | - [x] 展示 Heptabase 的笔记内容
18 |
19 | - [x] Heptabase 编辑笔记可一键更新到网站
20 |
21 | - [x] 双向链接
22 |
23 | - [x] 深浅色模式
24 |
25 | - [x] 面包片导航交互(参考了 [Andyʼs working notes](https://notes.andymatuschak.org/Evergreen_notes))
26 |
27 | - [x] 文字高亮
28 |
29 | - [x] 嵌入网易云音乐歌曲、专辑
30 |
31 | - [x] 嵌入视频
32 |
33 | ## 使用方法
34 |
35 | ### Heptabase
36 |
37 | 1. 在 Heptabase 中创建一个白板,这个白板中的所有卡片都可以在数字花园中查看
38 |
39 | 2. 公开此白板
40 |
41 | 
42 |
43 | 3. 根据你的喜好在上述白板中创建一些卡片,例如 About 介绍自己、Projects 介绍自己参与的项目等等
44 |
45 | ### GitHub
46 |
47 | 1. Fork [项目](https://github.com/draJiang/Heptabase-Blog)
48 |
49 | 
50 |
51 | 2. 在 `src/config.js` 中设置你的网站名称等信息
52 |
53 | ```javascript
54 | const CONFIG = {
55 | 'ga': 'G-XXXXXX', // 填写 Google Analytics 的 ID,不填也没问题
56 | 'whiteboard_id': '', // 填写白板 ID
57 | 'title': '数字花园🌱', // 站点标题
58 | 'pages': { // pages 里的标题和卡片 ID 可自定义
59 | 'Articles': '2e0bbcb8-fdf7-4cdb-8ee2-9f0651b71550',
60 | 'Projects': '2e0bbcb8-fdf7-4cdb-8ee2-9f0651b71550',
61 | 'Activity': 'activity', // 站点活跃状态热力图
62 | 'About': '2e0bbcb8-fdf7-4cdb-8ee2-9f0651b71550',
63 | 'XXXXX':'xxxx-xxxx-xxx(Heptabase 卡片 ID)'
64 | }
65 | 'server': '', // Discord 服务器 ID,非必填,填写后将在网站中显示聊天入口
66 | 'channel': '' // Discord 频道 ID,非必填,填写后将在网站中显示聊天入口
67 | }
68 | ```
69 |
70 | `pages` 里配置 Heptabase 中的卡片 ID, 配置后会显示在网站的右上角,点击会打开对应的卡片:
71 |
72 | 
73 |
74 | `pages` 的配置会影响你的网站首页,目前的规则是:网站会在你的白板中寻找名称为 About 的卡片(不区分大小写),如果不存在,则会将 pages 配置中第 1 个卡片作为**首页**。
75 |
76 | 3. 设置 GitHub Action
77 |
78 | 编辑 `.github/workflows/main.yml` ,将 `https://api.dabing.one` 修改为 `https://api.dabing.one?whiteboard_id=your_whiteboard_id` 例如 `https://api.dabing.one?whiteboard_id=d4cc3728297609add1a00aab108e90c4e57a1c378cfc2307c251745bf7d2a884`
79 |
80 | 编辑完毕后保存,这一步是为了后续更新网站的内容。
81 |
82 | 
83 |
84 | 4. 自定义 LOGO
85 |
86 | 在 `public` 目录下替换 `favicon.ico`、`apple-touch-icon.png` 、`logo.png` 文件
87 |
88 | ## Vercel
89 |
90 | 1. 在 [Vercel](https://vercel.com/) 中新建项目
91 |
92 | 2. 选择 GitHub 中对应的项目名称
93 |
94 | 
95 |
96 | 3. 自定义域名
97 |
98 | 在 Vercel 中打开项目,在 Settings 中设置自己的域名
99 |
100 | 
101 |
102 | ## 更新内容
103 |
104 | Heptabase 的白板更新后不会自动同步到网站中,需要手动进行更新,手动更新方法:
105 |
106 | 1. 在你的 GitHub 仓库中打开 Actions
107 |
108 | 2. 点击 Run workflow
109 |
110 | 3. 几分钟后网站的内容就会与 Heptabase 同步
111 |
112 | 
113 |
114 | ## 常见问题
115 |
116 | Q:为什么上传到 Hepta 中的图片无法在网站上显示
117 |
118 | A:直接在 Heptabase 中上传的图片,目前无法显示,一个解决方案是先将图片传到自己的图床里再放到 Hepta 里。
119 |
120 | ## 一些小技巧
121 |
122 | ### 嵌入 HTML
123 |
124 | Buy me a coffee
125 |
126 | ```html
127 | {HTML}
128 |
129 | ```
130 |
131 | Producthunt
132 |
133 | ```html
134 | {HTML}
135 |
136 | ```
137 |
138 | Bandcamp
139 |
140 | ```html
141 | {HTML}
142 |
143 | ```
144 |
145 | 上述样式在编辑器中是这个样子:
146 |
147 | 
148 |
149 | ## 拉取远程更新
150 |
151 | 当此项目功能更新后你可以通过以下方式同步更新你的网站。
152 |
153 | 首先需要在终端中安装 git 以及安装 [VScode](https://code.visualstudio.com/download) 客户端。
154 |
155 | ### 将自己的项目克隆到本地
156 |
157 | 首先获取你自己的项目 URL
158 |
159 | 
160 |
161 | 接下来在终端中操作
162 |
163 | ```shell
164 | git clone "https://github.com/YOURNAME/Heptabase-Blog.git"
165 | ```
166 |
167 | 通过以下命令进入项目文件夹
168 |
169 | ```shell
170 | cd Heptabase-Blog
171 | ```
172 |
173 | 
174 |
175 | ### 将本地的仓库与上游仓库关联
176 |
177 | 通过在终端中输入以下命令建立关联
178 |
179 | ```shell
180 | git remote add upstream https://github.com/draJiang/Heptabase-Blog.git
181 | ```
182 |
183 | 关联成功后,通过以下命令确认是否添加成功
184 |
185 | ```shell
186 | git remote -v
187 | ```
188 |
189 | 
190 |
191 | 用以下命令从你的上游仓库获取更新
192 |
193 | ```shell
194 | git fetch upstream
195 | ```
196 |
197 | 获取更新成功后,终端可能不会提供任何提示,接着进行下一步:合并上游仓库的更新
198 |
199 | ```shell
200 | git merge upstream/main
201 | ```
202 |
203 | 在合并的过程中可能系统会提示你**上游仓库和你的本地仓库存在冲突**,其实就是两个项目中存在差异,需要你手动选择应该如何处理这些冲突。
204 |
205 | ### 在 VSCode 中处理冲突
206 |
207 | 首先打开 VScode,然后打开本地的 Heptabase-Blog 文件夹
208 |
209 | 
210 |
211 | 在左侧的文件列表中,你可能会看到一些红色的文件名称,意味着这些文件中存在冲突,你需要点击这些文件逐个解决冲突。
212 |
213 | 
214 |
215 | 通常 `config.js` 、LOGO 等你需要自行自定义的内容,你可以选择「Accept Current Change」,其他则可以选择「Accept Incoming Change」
216 |
217 | 
218 |
219 | ### 将本地项目推送到自己的 GitHub 仓库中
220 |
221 | 你已经解决了所有的冲突,现在可以将代码提交到你的 GitHub 仓库中,提交成功后 Vercel 会自动更新网站。
222 |
223 | 在终端中分别输入以下命令提交本次更新
224 |
225 | ```shell
226 | git add .
227 | ```
228 |
229 | ```shell
230 | git commit -m 'Conflicts resolved'
231 | ```
232 |
233 | 推送到 GitHub 仓库中
234 |
235 | ```shell
236 | git push origin main
237 | ```
238 |
239 | ### 检查 Vercel 是否更新成功
240 |
241 | 现在,一切都已完成,不过还是要检查一下 Vercel 是否正常完成网站的更新。
242 |
243 | 打开你的 GitHub 项目主页,点击注释 1 位置的图标,注意,根据部署状态这里会分别显示以下几种情况:
244 |
245 | - 🟡:正在部署
246 |
247 | - ❌:部署失败
248 |
249 | - ✅:部署成功
250 |
251 | 
252 |
253 | 如果正在部署、部署失败,你可以点击注释 2 查看失败原因。
254 |
255 | ---
256 |
257 | ## 为什么不用其他方式
258 |
259 | ### 工具选择
260 |
261 | 尝试过 [HUGO](https://gohugo.io/) 和 [Notion](https://sspai.com/post/66678) 等方式、研究了 [obsidian publish](https://obsidian.md/publish),也实践用 Notion 维护了一年的[博客](https://blog.dabing.one/),但一直没有找到比较理想的方案。
262 |
263 | 一方面,笔记在 Heptabase 中,文章放到其他平台会导致双向链接失效。我在实践卡片笔记法,文章与笔记有高度的关联性,例如下面这篇文章中就存在多个卡片链接,但是这些链接在 Heptabase 以外的地方显示时就无法正常打卡笔记,所以不得不转为普通文本,这不但增加了工作量,也使得原本文章的脉络失效。
264 |
265 | 
266 |
267 | 另一方面,笔记、文章数据在不同平台有多份副本,后续修改起来就要穿梭在不同平台中进行更新,维护成本高。
268 |
269 | Heptabase 本身也支持公开笔记,但移动端的支持不太好,于是决定自己开发了一个。
270 |
271 | ### 博客 vs 数字花园
272 |
273 | 数字花园的理念与我正在使用的卡片笔记法、Heptabase 的设计哲学更加贴近,所以放弃了持续 1 年的博客,改用数字花园的方式维护自己的个人站点,下面会详细介绍一下原因。
274 |
275 | ## 对数字花园的理解
276 |
277 | 聊一下我对数字花园理解,以及如何将这些理解体现到网站的设计上。
278 |
279 | ### 知识的持续性
280 |
281 | 数字花园的内容是持续迭代的,我可以发布尚不成熟的想法(不一定要等到输出完整的文章)并且可以在发布后持续的修订。所以在笔记列表中,不是强调笔记的创建时间而是展示最近编辑时间,并将最近编辑的笔记展示在最前,方便阅读者理解笔记的活跃状态。
282 |
283 | 
284 |
285 | 这和我运用的卡片笔记法理念一致,通过不断的积累、迭代卡片完成文章的输出,而不是一来就面对一张白纸一步到位完成创作。(写作不是从零开始)
286 |
287 | ### 思考的脉络
288 |
289 | 传统博客通常会按创建时间展示文章列表供用户阅读,通过标签筛选某个类型的文章。而数字花园则强调思考的脉络,具体体现在以下几点
290 |
291 | 1. 弱化文章列表,用一个介绍页/索引页作为阅读者漫游的起点
292 |
293 | 2. 支持双向链接,阅读者可以看到与当前文章关联的其他知识、想法
294 |
295 | 3. 支持开头提到的[面包片导航](https://notes.andymatuschak.org/Evergreen_notes)的交互方式,阅读者可以快速地在不同笔记间流转
296 |
297 | 数字花园的首页与双向链接:
298 |
299 | 
300 |
301 | 总的来说,使用持续迭代的、重在体现思考脉络的方式记录想法,最终也用同样的方式分享知识,这是我选择数字花园并开发此网站的原因。
--------------------------------------------------------------------------------
/src/components/Container.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useUrlState } from 'react';
2 | import { BrowserRouter as Router, Routes, Route, Link, useParams } from 'react-router-dom';
3 | import { useLocation } from "react-router";
4 |
5 | import { format } from 'date-fns'
6 |
7 | import '../index.css'
8 | // import 'github-markdown-css'
9 | import Loading from '../components/Loading'
10 |
11 | // import "highlight.js/styles/github.css";
12 | // import 'highlight.js/styles/dark.css';
13 | // import 'highlight.js/styles/hopscotch.css';
14 | // import hljs from "highlight.js";
15 |
16 | import { ShareAltOutlined } from '@ant-design/icons';
17 | import { Button, message, Tooltip } from 'antd';
18 |
19 |
20 | // 文章正文
21 | function Container(props) {
22 |
23 | // 记录文章的 DOM 信息,用来处理 DOM 元素,例如修改图片样式
24 | let post = useRef(null);
25 | const { pathname } = useLocation();
26 | // Tooltips 显示、隐藏
27 | let [TooltipsOpen, setTooltipsOpen] = useState(false)
28 | // 记录当前文章的 ID
29 | let [thisPageId, setPageID] = useState('')
30 |
31 | // 当前路径信息
32 | let path = window.location.pathname
33 |
34 | // 路径中包含的 post id,用以获取文章 md 信息
35 | let path_id
36 | if (path.indexOf('/post/') < 0) {
37 |
38 | // 若路径中不含 post id,则取父组件的 props
39 | path_id = props.post_id
40 |
41 | } else {
42 | path_id = path.replace('/post/', '')
43 | }
44 |
45 |
46 |
47 | // 记录自定义的 Link 数据,用来实现 DOM 链接的间接跳转
48 | // let [my_link, setLink] = useState('');
49 |
50 | // 如果当前页面 ID 为空则获取数据
51 | if (thisPageId == '') {
52 | setPageID(props.post_id)
53 | }
54 |
55 | // 如果是移动端则增加图片的尺寸
56 | let isMobile = navigator.userAgent.match(/Mobile/i)
57 | let mobileSkale = 1
58 | if (isMobile) {
59 | mobileSkale = 2
60 | }
61 |
62 | // 点击反向链接
63 | const handleBackLinkClick = (link_id, current_id) => {
64 | console.log('handleBackLinkClick');
65 | console.log(link_id);
66 | // 记录跳转类型
67 | sessionStorage.setItem('nav_type', 0)
68 | // 记录当前滚动的位置
69 | sessionStorage.setItem('scrollY', window.scrollY)
70 |
71 | props.handleLinkClick(link_id, current_id)
72 |
73 | }
74 |
75 | // 点击文内链接
76 | const handleAarticleLinkClick = (node) => {
77 |
78 | if (node !== undefined) {
79 |
80 | if (node.classList.contains('article_link') !== true || node.getAttribute('path') === undefined || node.getAttribute('path') === null) {
81 | // 如果 DOM 中的元素**不**包含 path 属性,则跳过(有 path 属性的元素才需要处理)
82 |
83 | } else {
84 | let post_id = node.getAttribute('path').replace('/post/', '')
85 | let parent_note_id = node.getAttribute('parent_note_id')
86 |
87 | if (node.getAttribute('addClickHandleFlag') !== '1') {
88 | // 如果未绑定事件,则绑定,否则不绑定
89 | node.setAttribute('addClickHandleFlag', '1')
90 |
91 | node.addEventListener('click', function () {
92 | // 记录跳转类型
93 | sessionStorage.setItem('nav_type', 1)
94 | // 记录当前滚动的位置
95 | sessionStorage.setItem('scrollY', window.scrollY)
96 | props.handleLinkClick(post_id, parent_note_id)
97 | })
98 | }
99 |
100 |
101 | // node.removeEventListener('click', handleAarticleLinkClickInstant.bind(Event, post_id, parent_note_id))
102 | // node.addEventListener('click', handleAarticleLinkClickInstant.bind(Event, post_id, parent_note_id))
103 |
104 | // node.removeEventListener('click', ttest,false)
105 | // node.addEventListener('click', ttest,false)
106 |
107 | // node.onClick = handleAarticleLinkClickInstant.bind(Event,post_id, parent_note_id)
108 | // node.onClick = function () {
109 | // console.log('ok');
110 | // handleAarticleLinkClickInstant(Event, post_id, parent_note_id)
111 | // }
112 |
113 |
114 | }
115 |
116 | }
117 |
118 | }
119 |
120 | const handleCopyBtnClick = () => {
121 | console.log(123);
122 | // 显示 Tooltips
123 | setTooltipsOpen(true)
124 |
125 | setTimeout(() => {
126 | // 隐藏 Tooltips
127 | setTooltipsOpen(false)
128 | }, 1400);
129 | }
130 |
131 | // Tooltips 显示、隐藏状态变化时
132 | const handleTooltipOnOpenChange = () => {
133 | console.log('onOpenChange');
134 | }
135 |
136 | // 组件生命周期,组件载入、更新时将触发此函数
137 | useEffect(() => {
138 |
139 | // console.log('useEffect====================');
140 | props.handleHashChange(window.location.href, props['card']['card']['id'])
141 |
142 | // dom 加载完毕后
143 | if (post.current != null) {
144 |
145 | // 设置网易云音乐播放器的尺寸
146 |
147 | // 设置 img 的尺寸
148 | // let article_img = document.getElementsByTagName('img');
149 |
150 | // for (let i = 0; i < article_img.length; i++) {
151 | // let width_key_index = article_img[i]['alt'].indexOf('{{width ')
152 | // if (width_key_index > -1) {
153 | // let img_width = article_img[i]['alt'].substring(width_key_index, article_img[i]['alt'].length)
154 | // img_width = img_width.replace('{{width ', '')
155 | // img_width = img_width.replace('}}', '')
156 |
157 | // article_img[i].setAttribute('style', 'width:' + (Number(img_width.replace('%', '')) * mobileSkale).toString() + '%')
158 | // article_img[i].style.display = 'block'
159 | // article_img[i].style.margin = '0 auto'
160 | // }
161 | // }
162 |
163 | // 设置 a 链接的点击事件,将 a 按照 Link 的方式进行跳转,避免页面不必要的刷新
164 | let article_link = document.getElementsByClassName('article_link');
165 |
166 | let links = []
167 |
168 | for (let i = 0; i < article_link.length; i++) {
169 |
170 | if (article_link[i].classList.contains('article_link') !== true || article_link[i].getAttribute('path') === undefined || article_link[i].getAttribute('path') === null) {
171 | // 如果 DOM 中的元素**不**包含 path 属性,则跳过(有 path 属性的元素才需要处理)
172 | continue
173 | }
174 |
175 | setTimeout(() => {
176 |
177 | handleAarticleLinkClick(article_link[i])
178 |
179 | }, 10);
180 |
181 | }
182 |
183 | // 设置自定义 Link 并渲染到 DOM 中
184 | // if (my_link == '' && links.length > 0) {
185 | // setLink(links)
186 | // }
187 |
188 | // 滚动到对应卡片的位置
189 | setTimeout(() => {
190 | let last_note = document.getElementsByClassName('container')
191 | // console.log(last_note[last_note.length - 1]);
192 | // document.getElementsByClassName('notes')[0].scrollTo({ left: last_note[last_note.length - 1].offsetLeft, behavior: 'smooth' })
193 | }, 100);
194 |
195 |
196 | }
197 |
198 | // // 代码高亮
199 | // // if (document.querySelectorAll('pre').length > 0) {
200 | // // document.querySelectorAll('pre').forEach(element => {
201 | // // hljs.highlightBlock(element);
202 | // // });
203 | // // }
204 |
205 |
206 |
207 |
208 | });
209 |
210 | // 加载中
211 | if (false) {
212 | console.log('isLoading');
213 |
214 | return
215 |
216 |
217 | } else {
218 |
219 | let links = []
220 |
221 | // 反向链接
222 | let backLinksBox =
228 |
229 | if (props['card']['backLinks'].length > 0) {
230 | let backLinks = props['card']['backLinks'].map((backLink) =>
231 |
232 |
233 | {/* */}
234 |
235 | {backLink.title}
236 |
237 | {/* */}
238 |
239 |
240 | )
241 |
242 | backLinksBox =
248 | }
249 |
250 |
251 |
252 | return
253 |
254 |
255 |
256 |
257 | Created {format(new Date(props['card']['card']['createdTime']), 'yyyy-MM-dd')}
258 | {props['card']['card']['lastEditedTimeDiff']}
259 |
260 |
261 | } />
262 |
263 |
264 | {backLinksBox}
265 |
266 |
267 | }
268 |
269 | }
270 |
271 | export default Container;
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | @media (prefers-color-scheme: light) {
2 | :root {
3 | --notes-bg-color: #fff;
4 | --notes-border-default: #d0d7de;
5 | --notes-text-bg-yellow: #fbf2da;
6 | --notes-text-color-yellow: #ca902e;
7 | }
8 | body {
9 | background-color: var(--notes-bg-color);
10 | }
11 | .notes {
12 | background-color: #FAFAFC;
13 | border-right: 1px solid var(--notes-border-default)
14 | }
15 | .overlay {
16 | box-shadow: inset 0 0 0.5px 1px hsla(0, 0%, 100%, 0.075), /* shadow ring 👇 */
17 | 0 0 0 1px hsla(0, 0%, 0%, 0.05), /* multiple soft shadows 👇 */
18 | 0 0.3px 0.4px hsla(0, 0%, 0%, 0.02), 0 0.9px 1.5px hsla(0, 0%, 0%, 0.045), 0 3.5px 6px hsla(0, 0%, 0%, 0.09);
19 | }
20 | pre {
21 | background-color: #f6f8fa;
22 | color: #1F2328;
23 | }
24 | }
25 |
26 | @media (prefers-color-scheme: dark) {
27 | :root {
28 | --notes-bg-color: #0d1117;
29 | --notes-border-default: #30363d;
30 | --notes-text-bg-yellow: #564328;
31 | --notes-text-color-yellow: #c99849;
32 | }
33 | body {
34 | background-color: var(--notes-bg-color);
35 | }
36 | .notes_box nav,
37 | .notes {
38 | background-color: var(--notes-bg-color);
39 | border-bottom: 1px solid var(--notes-border-default);
40 | }
41 | .notes {
42 | border-right: 1px solid var(--notes-border-default)
43 | }
44 | nav {
45 | color: #c9d1d9;
46 | }
47 | .overlay {
48 | box-shadow: inset 0 0 0.5px 1px hsla(0, 0%, 100%, 0.075), /* shadow ring 👇 */
49 | 0 0 0 1px hsla(0, 0%, 0%, 0.05), /* multiple soft shadows 👇 */
50 | 0 0.3px 0.4px hsla(0, 0%, 0%, 0.02), 0 0.9px 1.5px hsla(0, 0%, 0%, 0.045), 0 3.5px 6px hsla(0, 0%, 0%, 0.09);
51 | }
52 | .container .note_title .note_close_button svg,
53 | .copy-btn svg {
54 | fill: var(--color-fg-default, #c9d1d9);
55 | }
56 | /* background: linear-gradient(90deg, rgba(255, 255, 255, 0.12) 25%, rgba(255, 255, 255, 0.18) 37%, rgba(255, 255, 255, 0.12) 63%);
57 | background: rgba(255, 255, 255, 0.12); */
58 | .ant-skeleton.ant-skeleton-active .ant-skeleton-title::after,
59 | .ant-skeleton.ant-skeleton-active .ant-skeleton-paragraph>li::after,
60 | .ant-skeleton.ant-skeleton-active .ant-skeleton-avatar::after,
61 | .ant-skeleton.ant-skeleton-active .ant-skeleton-button::after,
62 | .ant-skeleton.ant-skeleton-active .ant-skeleton-input::after,
63 | .ant-skeleton.ant-skeleton-active .ant-skeleton-image::after {
64 | background: linear-gradient(90deg, rgba(255, 255, 255, 0.12) 25%, rgba(255, 255, 255, 0.18) 37%, rgba(255, 255, 255, 0.12) 63%) !important;
65 | }
66 | .copy-btn {
67 | background: none;
68 | border-color: var(--color-border-muted);
69 | }
70 | .w-heatmap text {
71 | /* background-color: rgb(0, 0, 0); */
72 | /* color: rgb(255, 255, 255); */
73 | fill: currentColor;
74 | color: rgb(136, 136, 136);
75 | }
76 | pre {
77 | color: #e6edf3;
78 | background-color: #161b22;
79 | }
80 | }
81 |
82 | @media (width <=600px) {
83 | #root .container {
84 | border: none;
85 | width: 100%;
86 | }
87 | }
88 |
89 | @media (width >600px) {
90 | #root .notes_box {
91 | display: flex;
92 | flex-direction: column;
93 | height: 100vh;
94 | }
95 | .container,
96 | .post_list {
97 | width: 650px;
98 | }
99 | }
100 |
101 | body {
102 | padding: 0;
103 | margin: 0;
104 | height: 100vh;
105 | /* background-color: var(--color-canvas-default, red); */
106 | }
107 |
108 | a {
109 | word-wrap: break-word;
110 | }
111 |
112 | article .htmlBox {
113 | margin-bottom: 16px;
114 | }
115 |
116 | article .htmlBox * {
117 | max-width: 100%;
118 | }
119 |
120 | article .imgBox {
121 | text-align: center;
122 | margin: 32px 0 32px 0;
123 | }
124 |
125 | article .imgBox img {
126 | width: 100%;
127 | max-width: 800px;
128 | border-radius: 2px;
129 | }
130 |
131 | article {
132 | margin-bottom: 2rem;
133 | }
134 |
135 | article .external_link::after {
136 | /* text-decoration: underline !important; */
137 | content: "↗";
138 | }
139 |
140 | .markdown-body {
141 | /* margin: 0 auto !important; */
142 | }
143 |
144 | .post_list {
145 | /* margin-top: 1rem; */
146 | padding-top: 2rem;
147 | }
148 |
149 | .post_list ul {
150 | margin: 0 !important;
151 | padding: 0 !important;
152 | }
153 |
154 | .post_list li {
155 | list-style: none;
156 | margin-bottom: 1.2rem;
157 | }
158 |
159 | .post_list h2 {
160 | font-size: 1.2rem !important;
161 | padding-bottom: 0 !important;
162 | margin-bottom: 0.4rem !important;
163 | border-bottom: none !important;
164 | }
165 |
166 | .post_list a {
167 | text-decoration: none;
168 | color: #333;
169 | }
170 |
171 | .post_list time,
172 | .article_bottom {
173 | font-size: 0.9rem;
174 | color: #666;
175 | }
176 |
177 | .container .article_bottom {
178 | margin-bottom: 1rem;
179 | display: flex;
180 | }
181 |
182 | .article_bottom time {
183 | margin-right: 1rem;
184 | }
185 |
186 | .container {
187 | position: sticky;
188 | padding-bottom: 1rem;
189 | overflow-y: auto;
190 | /* flex: 0 0 auto; */
191 | /* border-right: 1px solid var(--color-border-default); */
192 | box-shadow: 0 0 0 1px var(--color-border-default);
193 | /* 用 box-shadow 模拟 1px 黑色边框 */
194 | /* border-left: 1px solid var(--color-border-default); */
195 | }
196 |
197 | .container,
198 | .post_list {
199 | margin: 0 auto;
200 | /* width: 38rem; */
201 | /* width: 650px; */
202 | padding-left: 1.4rem;
203 | padding-right: 1.4rem;
204 | }
205 |
206 | .container .note_title {
207 | display: flex;
208 | position: fixed;
209 | flex-direction: row;
210 | align-items: center;
211 | top: 0;
212 | padding: 60px 0 0 0;
213 | width: 40px;
214 | height: 100%;
215 | writing-mode: vertical-lr;
216 | background-color: var(--color-canvas-default);
217 | z-index: 999;
218 | }
219 |
220 | .container .note_title p {
221 | cursor: pointer;
222 | flex: 0.9;
223 | }
224 |
225 | .container .note_title * {
226 | margin: 0;
227 | padding: 4px;
228 | }
229 |
230 | .container .note_title .note_close_button {
231 | position: absolute;
232 | bottom: 8px;
233 | cursor: pointer;
234 | }
235 |
236 | .container .note_title p:hover {
237 | color: var(--color-accent-fg);
238 | }
239 |
240 | .container .note_title .note_close_button:hover,
241 | .container .note_title p:active,
242 | .my_link:active {
243 | opacity: 0.6;
244 | }
245 |
246 | .notes {
247 | display: flex;
248 | width: auto;
249 | overflow-x: scroll;
250 | overflow-y: hidden;
251 | flex-grow: 1;
252 | /* padding: 1rem; */
253 | }
254 |
255 | .container {
256 | padding-bottom: 1.4rem;
257 | }
258 |
259 |
260 | /* .markdown-body nav header {
261 | padding: 0;
262 | } */
263 |
264 | .markdown-body nav ul,
265 | .markdown-body nav ul li {
266 | margin: 0;
267 | }
268 |
269 | .nav {
270 | /* background-color: #fff; */
271 | /* color: #000; */
272 | display: flex;
273 | height: 3rem;
274 | position: sticky;
275 | top: 0;
276 | /* border-bottom: 1px solid var(--color-border-defaultABC, var(--notes-border-default)); */
277 | /* margin-bottom: 1.4rem !important; */
278 | /* padding: 0 1rem; */
279 | z-index: 999;
280 | }
281 |
282 | .nav ul {
283 | display: flex;
284 | padding: 0;
285 | height: 100%;
286 | margin: 0;
287 | }
288 |
289 | .nav li {
290 | /* position: relative; */
291 | margin-left: 0.4rem;
292 | list-style: none;
293 | margin-top: 0em !important;
294 | }
295 |
296 | .nav a {
297 | text-decoration: none;
298 | padding: 0.4rem;
299 | }
300 |
301 | .nav a:hover {
302 | text-decoration: underline;
303 | }
304 |
305 | .nav header {
306 | position: relative;
307 | flex: 1;
308 | overflow: hidden;
309 | text-overflow: ellipsis;
310 | /* width: 100%; */
311 | /* margin-left: 2rem; */
312 | }
313 |
314 | .nav header,
315 | .nav ul {
316 | line-height: 3rem;
317 | }
318 |
319 | .nav header img {
320 | width: 1.4rem;
321 | }
322 |
323 | .nav header img {
324 | position: absolute;
325 | top: 50%;
326 | transform: translate(0%, -50%);
327 | }
328 |
329 | .backLinks {
330 | border: 1px solid var(--color-border-default, var(--notes-border-default));
331 | padding: 0.6rem;
332 | background-color: var(--color-canvas-subtle);
333 | border-radius: 2px;
334 | }
335 |
336 | .backLinks header {
337 | margin-bottom: 0.4rem;
338 | font-weight: bold;
339 | }
340 |
341 | .nav a,
342 | .post_list a {
343 | color: var(--color-fg-default) !important;
344 | }
345 |
346 | .my_link {
347 | background-color: transparent;
348 | color: var(--color-accent-fg);
349 | text-decoration: none;
350 | cursor: pointer;
351 | }
352 |
353 | .my_link:hover,
354 | .backLinks a:hover,
355 | .post_list a:hover {
356 | text-decoration: underline;
357 | }
358 |
359 | .unknown_card {
360 | opacity: 0.6;
361 | /* 禁止点击 */
362 | pointer-events: none;
363 | }
364 |
365 | .unknown_card::after {
366 | /* text-decoration: underline !important; */
367 | content: "";
368 | }
369 |
370 | .loading {
371 | /* position: relative; */
372 | /* opacity: 0.6; */
373 | width: 650px;
374 | padding: 0 1rem;
375 | }
376 |
377 | .footer {
378 | text-align: center;
379 | color: var(--color-fg-subtle);
380 | }
381 |
382 | .markdown-body #player {
383 | margin: 0;
384 | }
385 |
386 | .music {
387 | text-align: center;
388 | margin: 1rem 0;
389 | }
390 |
391 | .music iframe {
392 | max-width: 28rem;
393 | }
394 |
395 | .highlight_bg {
396 | background-color: var(--notes-text-bg-yellow);
397 | }
398 |
399 | .highlight_color {
400 | color: var(--notes-text-color-yellow);
401 | }
402 |
403 | .listBox {
404 | display: flex;
405 | }
406 |
407 | .listBox * {
408 | /* max-width: 97%; */
409 | }
410 |
411 | .listBullet,
412 | .numberListBullet {
413 | line-height: 1.3em;
414 | margin-left: 0.8em;
415 | }
416 |
417 | .numberListBullet::before {
418 | content: attr(data-before);
419 | margin-right: 0.3em;
420 | /* font-size: 1.57em; */
421 | }
422 |
423 | .numberListBullet {
424 | margin-top: 0.1em;
425 | /* margin-right: 0.3em; */
426 | }
427 |
428 | .listBullet::before {
429 | content: "•";
430 | margin-right: 0.3em;
431 | font-size: 1.7em;
432 | }
433 |
434 |
435 | /* .markdown-body .hljs {
436 | background: #303030 !important;
437 | padding: 16px;
438 | overflow: auto;
439 | font-size: 85%;
440 | line-height: 1.45;
441 | border-radius: 6px;
442 | word-wrap: normal;
443 | margin-bottom: 16px;
444 | font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
445 | } */
446 |
447 | .markdown-body table p {
448 | margin-bottom: 0;
449 | }
450 |
451 | .markdown-body ol li {
452 | list-style: decimal;
453 | }
454 |
455 | .markdown-body .nav ul {
456 | padding-left: 0.4rem;
457 | }
458 |
459 | .copy-btn {
460 | /* background: none;
461 | font-size: 0.6rem;
462 | padding: 4px 8px 2px 8px;
463 | border-radius: 2px;
464 | border: 1px solid #fff;
465 | cursor: pointer; */
466 | }
467 |
468 | .copy-btn:hover {
469 | /* text-decoration: underline; */
470 | }
471 |
472 | .markdown-body .task-list-item {
473 | display: flex;
474 | margin: 16px 0;
475 | }
476 |
477 | .calendarHeatmap {
478 | padding: 2rem;
479 | min-width: 98%;
480 | }
481 |
482 | svg.w-heatmap rect {
483 | cursor: auto !important;
484 | }
--------------------------------------------------------------------------------
/src/page/Post.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { useLocation, useParams, useNavigate } from 'react-router-dom';
3 | import WidgetBot, { API } from '@widgetbot/react-embed'
4 |
5 | import ReactGA from 'react-ga';
6 | import CONFIG from '../config';
7 |
8 |
9 | import Container from '../components/Container'
10 | import Nav from '../components/Nav';
11 | import Loading from '../components/Loading'
12 |
13 | import '../style.css'
14 | import 'github-markdown-css'
15 | import 'antd/dist/reset.css';
16 |
17 | import { getHeptabaseData, getClearCard, getClearImag, heptaToMD } from '../constantFunction'
18 |
19 | import useHash from "../hooks/useHash";
20 |
21 | import { Button, message, Tooltip } from 'antd';
22 |
23 | import Clipboard from 'clipboard';
24 |
25 | // 属性
26 | let ACTIVE_NOTE = '' // 焦点笔记 ID 例如 38d9247c-1b0b-47ca-a119-933af80d71c2
27 | let CURRENT_URL = window.location.href // 当前 URL,用来判断 URL 有变化时触发相关事件
28 | let windowWidth = window.innerWidth // 窗口宽度
29 | let minWidth = 600 // 以此宽度为分界线需渲染不同界面
30 | // 数据
31 | let HEPTABASE_DATA // hepta 数据
32 | let HOME_DATA // 首页数据
33 |
34 | if (CONFIG.ga) {
35 | ReactGA.initialize(CONFIG.ga);
36 | }
37 |
38 | // 文章页面
39 | function Post(props) {
40 | const [cardList, setCardList] = useState([]);
41 | const [activeNote, setActiveNote] = useState('null');
42 | const [showChatWindow, setShowChatWindow] = useState(false);
43 |
44 | let { param1 } = useParams();
45 | let location = useLocation();
46 | let navigate = useNavigate();
47 |
48 | // console.log('location.search');
49 | // console.log(location.search);
50 |
51 | // useEffect(() => {
52 | // // 在此可以处理 param1 或者其他路径参数的变化
53 | // console.log('useEffect param');
54 | // console.log(param1);
55 | // }, [param1]);
56 |
57 | useEffect(() => {
58 |
59 | console.log('location.search');
60 |
61 | // 根据 URL 显示卡片
62 | if (HOME_DATA) {
63 | herfToData()
64 | }
65 | if (CONFIG.ga && location.pathname !== '/') {
66 | ReactGA.pageview(location.pathname + location.search);
67 | }
68 |
69 | // 在此可以处理查询参数 myQueryParam 的变化
70 | }, [location.search]);
71 |
72 |
73 | useEffect(() => {
74 | // 复制到剪切板实例化
75 | const copy = new Clipboard('.copy-btn');
76 | copy.on('success', e => {
77 | // message.open({
78 | // type: 'success',
79 | // content: 'Link copied',
80 | // });
81 |
82 | });
83 | copy.on('error', function (e) {
84 | console.error('Action:', e.action);
85 | console.error('Trigger:', e.trigger);
86 | });
87 |
88 | // 请求 hepta 数据 getHeptabaseData
89 | getHeptabaseData().then((res) => {
90 |
91 | let heptabase_blog_data = res.data
92 |
93 | // 将数据保存到全局变量中
94 | HEPTABASE_DATA = heptabase_blog_data
95 | // 默认获取名为 about 的卡片作为首页,若无则获取配置中首个卡片作为首页
96 | HOME_DATA = res['pages']['about'] || res['pages']['firstPage']
97 | // 渲染 URL、数据
98 | herfToData()
99 |
100 | }).catch(error => {
101 | console.error('Error:', error);
102 | });
103 |
104 |
105 | }, [])
106 |
107 | const handleShowChatWindow = () => {
108 |
109 | setShowChatWindow(!showChatWindow)
110 |
111 | }
112 |
113 | // 根据 card id 获取 card content
114 | const findContent = (id, heptabase_blog_data) => {
115 | if (heptabase_blog_data === '') {
116 | return
117 | }
118 |
119 | let new_card
120 | console.log('Post findContent for:');
121 | for (let i = 0; i < heptabase_blog_data.cards.length; i++) {
122 |
123 | if (heptabase_blog_data.cards[i]['id'] == id) {
124 |
125 | // // 处理内容中的图片
126 | // heptabase_blog_data.cards[i] = getClearImag(heptabase_blog_data.cards[i])
127 |
128 | // 处理反向链接
129 | new_card = getClearCard(heptabase_blog_data.cards[i], heptabase_blog_data.cards)
130 | // heptabase_blog_data.cards[i] = new_card['card']
131 |
132 | new_card['card']['content'] = heptaToMD(new_card['card'])
133 |
134 | return new_card
135 |
136 | }
137 | }
138 |
139 | return new_card
140 |
141 | }
142 |
143 | // 文章内链接、反向链接点击
144 | const handleLinkClick = (link_id, current_id = undefined, type = -1) => {
145 | console.log('post.js handleLinkClick');
146 | let bingo = false
147 | cardList.forEach(item => {
148 |
149 | if (link_id === item['card']['id']) {
150 |
151 | // 界面上已显示点击的卡片
152 | bingo = true
153 |
154 | }
155 |
156 | });
157 |
158 | // 已经显示此卡片则不更新 URL 和数据
159 | if (bingo || current_id === undefined || current_id === null) {
160 |
161 |
162 | if (ACTIVE_NOTE !== link_id) {
163 |
164 | // 修改 URL 中的焦点卡片
165 | setUrlActiveNote(link_id)
166 |
167 | // 记录焦点卡片
168 | ACTIVE_NOTE = link_id
169 |
170 | // 如果是小尺寸设备,需要更新 UI 显示焦点卡片
171 | if (windowWidth < minWidth) {
172 |
173 | // setState({
174 | // activeNote: ACTIVE_NOTE
175 | // })
176 |
177 | setActiveNote(ACTIVE_NOTE)
178 |
179 | }
180 |
181 | }
182 |
183 | ScrollToActiveNote()
184 |
185 | return
186 |
187 | } else {
188 |
189 | // 打开新卡片
190 |
191 | // 先判断卡片是否存在
192 | let target_card = findContent(link_id, HEPTABASE_DATA)
193 | if (target_card === undefined) {
194 | // 卡片无效
195 | message.info('Invalid card');
196 |
197 | } else {
198 | let getUrlSearch_req = getUrlSearch(window.location.search)
199 | let url_search_list = getUrlSearch_req['url_search_list']
200 |
201 | let new_url_search = ''
202 | let current_page_index = -1
203 | for (let i = 0; i < url_search_list.length; i++) {
204 |
205 | if (url_search_list[i] === '') {
206 | continue
207 | }
208 |
209 | if (url_search_list[i] === current_id) {
210 | // URL 参数 === current_id
211 | current_page_index = i
212 | } else {
213 | // URL 参数 !== current_id
214 | }
215 |
216 | if (new_url_search == '') {
217 | new_url_search += '?note-id=' + url_search_list[i]
218 | } else {
219 | new_url_search += '¬e-id=' + url_search_list[i]
220 | }
221 |
222 | //如果当前 id === current_id,则忽略后面的所有 ID
223 | if (current_page_index > -1) {
224 | break;
225 | }
226 | }
227 |
228 | if (new_url_search == '') {
229 | new_url_search += '?note-id=' + link_id
230 | } else {
231 | new_url_search += '¬e-id=' + link_id
232 | }
233 |
234 |
235 | new_url_search += '&active-note-id=' + link_id
236 |
237 | // 设置 URL
238 | // window.history.pushState({}, '', window.location.origin + '/post' + new_url_search)
239 | navigate('/post' + new_url_search)
240 |
241 | // 记录 URL
242 | CURRENT_URL = window.location.origin + '/post' + new_url_search
243 |
244 | // 删除 URL 中不存在的 Card
245 | resetCardList()
246 | // 根据 URL 获取 card 数据
247 | herfToData()
248 | }
249 |
250 | }
251 |
252 | }
253 |
254 | // 根据 herf 渲染界面上显示的数据
255 | const herfToData = () => {
256 |
257 | // 首页的情况
258 | if (window.location.search === '') {
259 |
260 | // 找到首页卡片的 ID
261 | let main_id = HOME_DATA['id']
262 | // 设置 URL
263 | // window.location.replace(window.location.origin + '/post?note-id=' + main_id)
264 | navigate('/post?note-id=' + main_id, { replace: true });
265 |
266 | }
267 |
268 | // 从 URL 中获取 note id,根据 id 获取卡片数据
269 | let card_list = []
270 | let getUrlSearch_req = getUrlSearch(window.location.search)
271 | let url_search_list = getUrlSearch_req['url_search_list']
272 |
273 | for (let i = 0; i < url_search_list.length; i++) {
274 | if (url_search_list[i] == '') {
275 | continue
276 | }
277 | // 将数据保存到 card list 中
278 | card_list.push(findContent(url_search_list[i], HEPTABASE_DATA))
279 |
280 | }
281 |
282 | // 设置当前活跃的笔记(用户焦点)
283 | let activeNote
284 | if (getUrlSearch_req['active_str'] !== '') {
285 | activeNote = getUrlSearch_req['active_str'].replace('active-note-id=', '')
286 | } else {
287 | activeNote = card_list[card_list.length - 1]['card']['id']
288 | }
289 |
290 | // 根据 URL 渲染新的数据到界面上
291 | if (cardList !== card_list) {
292 | setCardList(card_list);
293 | }
294 |
295 |
296 | // 如果焦点发生变化
297 | if (ACTIVE_NOTE !== activeNote) {
298 | ACTIVE_NOTE = activeNote
299 | // 将最新的焦点设置到 URL 中
300 | setUrlActiveNote(ACTIVE_NOTE)
301 | }
302 |
303 | }
304 |
305 | // 当 URL 变化时(子组件 container 载入完毕后也会调用此方法)
306 | const handleHashChange = (url, cardId) => {
307 |
308 | // 如果 url 发生变化,则更新数据
309 | let old_url = getUrlSearch(CURRENT_URL)
310 | let new_url = getUrlSearch(url)
311 | console.log('new_url:');
312 | console.log(new_url);
313 | let old_url_1 = old_url['url_search_list'].join('-')
314 | let new_url_1 = new_url['url_search_list'].join('-')
315 |
316 | // 移除所有小标题
317 | const url_search_list = new_url.url_search_list
318 | if (url_search_list.length < 3) {
319 | // 移除小标题
320 | const noteTitles = document.querySelectorAll('.note_title');
321 | noteTitles.forEach(function (title) {
322 | title.remove();
323 | });
324 | // 选取所有同时具有 ".container" 类和 "mini" 类的元素
325 | const containersWithMini = document.querySelectorAll('.container.mini');
326 | // 遍历这些元素并移除 "mini" 类
327 | containersWithMini.forEach(function (container) {
328 | container.classList.remove('mini');
329 | });
330 |
331 | }
332 |
333 | // 定位到焦点卡片
334 | if (new_url['active_str'].indexOf(cardId) > -1) {
335 |
336 | // 定位到焦点卡片
337 | ScrollToActiveNote()
338 |
339 | }
340 |
341 | // 数据发生变化(忽略焦点变化)
342 | console.log('new_url_1 !== old_url_1:');
343 | console.log(new_url_1 !== old_url_1);
344 | if (new_url_1 !== old_url_1) {
345 |
346 | // 将当前 URL 保存到 state 中
347 | CURRENT_URL = url
348 |
349 | if (CURRENT_URL !== '') {
350 |
351 | herfToData()
352 |
353 | }
354 | }
355 |
356 | // 焦点发生变化
357 | if (old_url['active_str'] !== new_url['active_str']) {
358 |
359 | // 将当前 URL 保存到 state 中
360 | CURRENT_URL = url
361 |
362 | if (CURRENT_URL !== '') {
363 | // 记录新焦点到 state 中
364 | ACTIVE_NOTE = new_url['active_str'].replace('active-note-id=', '')
365 |
366 | // 如果是小尺寸设备,需要更新 UI 显示焦点卡片
367 | if (windowWidth < minWidth && activeNote !== ACTIVE_NOTE) {
368 |
369 | // setState({
370 | // activeNote: ACTIVE_NOTE
371 | // })
372 |
373 | setActiveNote(ACTIVE_NOTE)
374 |
375 | }
376 |
377 | }
378 |
379 | }
380 |
381 | // 删除 URL 中不存在的 Card
382 | resetCardList()
383 |
384 | // 设置卡片样式、小标题
385 | setCardMiniTitleAndStyle()
386 |
387 | // 增加分享按钮
388 | // addShareBtn()
389 |
390 |
391 |
392 | }
393 |
394 | const addShareBtn = () => {
395 | // 增加分享按钮
396 | // let btn = < button data-clipboard-text='这里是需要复制的文本123'
397 | // className="copy-btn"
398 | // type="button" > Copy
399 | let share_btn = document.createElement('button')
400 | share_btn.classList.add('copy-btn')
401 | share_btn.setAttribute('data-clipboard-text', '这里是需要复制的文本1232323')
402 | share_btn.innerText = '🔗'
403 |
404 |
405 | let notes = document.getElementsByClassName('note_article')
406 |
407 | for (let i = 0; i < notes.length; i++) {
408 | if (notes[i].getElementsByClassName('copy-btn').length > 0) {
409 | // 已经有分享按钮,不用重复添加
410 | continue
411 | } else {
412 | console.log(notes);
413 | let note_link = window.location.origin + '/post?note-id=' + notes[i].parentElement.getAttribute('note_id')
414 | share_btn.setAttribute('data-clipboard-text', note_link)
415 | notes[i].appendChild(share_btn)
416 | }
417 | }
418 |
419 |
420 | }
421 |
422 | // 删除 URL 中不存在的 Card
423 | const resetCardList = () => {
424 | let url = window.location.href
425 | //比对 url 和 cardList
426 | for (let i = 0; i < cardList.length; i++) {
427 | // url 中不存在此 card
428 | if (url.indexOf(cardList[i]['card']['id']) < 0) {
429 |
430 | // 删除 card
431 | cardList.splice(i, 1)
432 | setCardList(cardList)
433 |
434 | } else {
435 | // url 中存在此 card
436 | // continue
437 | }
438 |
439 | }
440 |
441 | }
442 |
443 | // 定位到焦点卡片
444 | const ScrollToActiveNote = () => {
445 |
446 | let note_list = document.getElementsByClassName('container')
447 | for (let j = 0; j < note_list.length; j++) {
448 | let note = note_list[j]
449 | // 定位到当前用户关注的笔记
450 | if (note.getAttribute('note_id') === ACTIVE_NOTE) {
451 |
452 | if (windowWidth > minWidth) {
453 | document.getElementsByClassName('notes')[0].scrollTo({ left: j * 650, behavior: 'smooth' })
454 |
455 |
456 | } else {
457 |
458 | // 如果是点击头部的页面(Projects、Posts 等)则需要定位到页面顶部
459 | let bingo = false
460 |
461 | if (sessionStorage.getItem('nav_type') > -1 || bingo) {
462 | // 滚动到顶部
463 | window.scrollTo(0, 0)
464 |
465 | // 重置 nav_type
466 | sessionStorage.setItem('nav_type', -1)
467 | }
468 |
469 |
470 | }
471 |
472 | break;
473 |
474 | }
475 |
476 | }
477 |
478 | }
479 |
480 | // 设置小标题、overlay 样式
481 | const setCardMiniTitleAndStyle = () => {
482 | console.log('setCardMiniTitleAndStyle');
483 | let notes = document.getElementsByClassName('container')
484 |
485 | for (let j = 0; j < notes.length; j++) {
486 |
487 | // 小标题
488 |
489 | let type = 0 // 记录标题在左侧还是右侧
490 | let note // 记录需要添加标题的节点
491 |
492 | // 判断卡片的位置,当遮挡前 1 个卡片时,前 1 个卡片显示垂直标题
493 | let left_mark = notes[j].getBoundingClientRect().x <= j * 40
494 | // 判断是否要显示右侧标题
495 | const chatWindowWidth = showChatWindow ? 480 : 0
496 | let right_mark = notes[j].getBoundingClientRect().x + 1 >= window.innerWidth - chatWindowWidth - (notes.length - j) * 40
497 |
498 | // 左侧小标题
499 | if (right_mark !== true) {
500 |
501 | if (left_mark) {
502 |
503 | if (j !== 0) {
504 | type = 1
505 | note = notes[j - 1]
506 | }
507 |
508 | } else {
509 |
510 | // 移除小标题
511 | if (j !== 0) {
512 | note = notes[j - 1]
513 | }
514 |
515 | if (note !== undefined) {
516 | let note_title = note.getElementsByClassName('note_title')[0]
517 | if (note_title !== undefined) {
518 |
519 | // 移除标题父级容器的类名标记
520 | note.classList.remove('mini')
521 |
522 | // 移除前一个元素的垂直标题
523 | note.removeChild(note_title)
524 | }
525 | }
526 |
527 | }
528 |
529 | }
530 |
531 |
532 | // 右侧小标题
533 | if (left_mark !== true) {
534 |
535 | if (right_mark) {
536 |
537 | type = 2
538 | note = notes[j]
539 | // 添加悬浮样式
540 | // note.classList.add('overlay')
541 |
542 | } else {
543 | // 移除小标题
544 | note = notes[j]
545 |
546 | if (note !== undefined && j !== 0) {
547 | let note_title = note.getElementsByClassName('note_title')[0]
548 | if (note_title !== undefined) {
549 |
550 | // 移除标题父级容器的类名标记
551 | note.classList.remove('mini')
552 |
553 | // 移除前一个元素的垂直标题
554 | note.removeChild(note_title)
555 | }
556 | }
557 |
558 | // 移除悬浮样式
559 | // note.classList.remove('overlay')
560 | }
561 |
562 | }
563 |
564 |
565 | // 需要显示小标题
566 | if (type > 0) {
567 |
568 | // 如果元素无标题
569 | if (note.classList.contains('mini') == false) {
570 | // 前一个元素显示垂直标题
571 | let note_title = document.createElement('div')
572 | note_title.classList.add('note_title')
573 |
574 |
575 | if (type === 1) {
576 | // 左侧小标题
577 | note_title.style.left = (j - 1) * 40 + 'px'
578 | } else {
579 | // 右侧小标题
580 | const chatWindowWidth = showChatWindow ? 460 : 0
581 | note_title.style.right = (notes.length - j) * 40 - 40 + chatWindowWidth + 'px'
582 | note_title.classList.add('overlay')
583 | }
584 |
585 | // 小标题文案
586 | let note_title_span = document.createElement('p')
587 |
588 | if (note.getElementsByTagName('H1').length === 0) {
589 | // 如果笔记中没有 H1 标题
590 | note_title_span.innerHTML = note.innerText.substring(0, 6) + '...'
591 | } else {
592 | note_title_span.innerHTML = note.getElementsByTagName('H1')[0].innerHTML
593 | }
594 |
595 | note_title_span.onclick = (event) => {
596 | console.log(event);
597 | console.log(event.target.innerText);
598 | console.log(note.getAttribute('note_id'));
599 | handleLinkClick(note.getAttribute('note_id'), undefined, 0)
600 | }
601 |
602 | // 小标题关闭按钮
603 | let note_close_button = document.createElement('span')
604 | note_close_button.innerHTML = ' '
605 | note_close_button.classList.add('note_close_button')
606 | note_close_button.onclick = (event) => {
607 |
608 | // 点击关闭按钮
609 |
610 | handleCardCloseClick(note.getAttribute('note_id'))
611 | }
612 |
613 | note_title.appendChild(note_title_span)
614 | note_title.appendChild(note_close_button)
615 | note.appendChild(note_title)
616 |
617 | note.classList.add('mini')
618 | }
619 |
620 | }
621 |
622 | // 样式
623 | if (j !== 0) {
624 | if (notes[j].getBoundingClientRect().x < notes[j - 1].getBoundingClientRect().x + notes[j - 1].getBoundingClientRect().width) {
625 | notes[j].classList.add('overlay')
626 | } else {
627 | notes[j].classList.remove('overlay')
628 | }
629 | }
630 |
631 |
632 | }
633 | }
634 |
635 | // 关闭卡片
636 | const handleCardCloseClick = (note_id) => {
637 |
638 | console.log('handleCardCloseClick');
639 | // 修改 URL
640 | let new_url = window.location.href.replace('note-id=' + note_id, '')
641 | // 设置新的 URL
642 | // window.history.pushState({}, '', new_url)
643 | navigate(new_url)
644 |
645 | // 记录 URL
646 | CURRENT_URL = window.location.href
647 |
648 | // 更新 UI
649 | herfToData()
650 |
651 | }
652 |
653 | // 获取 URL 参数
654 | const getUrlSearch = (location_search) => {
655 |
656 | let url_search = location_search.replace('?', '')
657 | url_search = url_search.replace(/&/gi, '')
658 |
659 | // 忽略焦点卡片
660 | let active_str = '' // 焦点卡片参数名称及其值
661 | let active_index = url_search.indexOf('active-note-id')
662 | if (active_index > -1) {
663 | let is_last_index = url_search.indexOf('note-id', active_index + 14)
664 | if (is_last_index > -1) {
665 | // 焦点卡片不是最后一个参数
666 | active_str = url_search.substring(active_index, is_last_index)
667 | } else {
668 | // 焦点卡片是最后一个参数
669 | active_str = url_search.substring(active_index, url_search.length)
670 | }
671 |
672 | }
673 |
674 | url_search = url_search.replace(active_str, '')
675 |
676 | let url_search_list = url_search.split('note-id=')
677 |
678 | return { 'url_search_list': url_search_list, 'active_str': active_str }
679 |
680 | }
681 |
682 | // 将焦点卡片 ID 写入 URL
683 | const setUrlActiveNote = (note_id) => {
684 |
685 | // 获取 URL 中的焦点卡片信息
686 | let getUrlSearch_req = getUrlSearch(window.location.search)
687 | let active_str = getUrlSearch_req['active_str']
688 |
689 | let new_url_search = window.location.search
690 |
691 | if (active_str === '') {
692 | // URL 中无焦点卡片
693 |
694 | new_url_search = new_url_search + '&active-note-id=' + note_id
695 |
696 | } else {
697 | // URL 中有焦点卡片
698 |
699 | // 如果焦点卡片无变化,则不更新
700 | if (active_str.indexOf(note_id) > -1) {
701 | return
702 | }
703 |
704 | new_url_search = new_url_search.replace(active_str, 'active-note-id=' + note_id)
705 | }
706 |
707 |
708 | // 设置 URL
709 | // window.history.pushState({}, '', window.location.origin + '/post' + new_url_search)
710 | const newURL = '/post' + new_url_search;
711 | navigate(newURL)
712 |
713 | // 记录 URL
714 | CURRENT_URL = window.location.origin + '/post' + new_url_search
715 |
716 | // setState({
717 | // location: window.location.href
718 | // })
719 |
720 | }
721 |
722 | // return() {
723 |
724 | if (HEPTABASE_DATA === null || cardList.length === 0) {
725 | return (
726 | {/*
*/}
727 |
730 |
731 |
732 | {/*
*/}
733 |
)
734 | } else {
735 |
736 | // console.log(state.activeNote);
737 |
738 | let card_list_dom = []
739 |
740 | //如果屏幕宽度较小,则只显示 1 条笔记
741 | if (windowWidth < minWidth) {
742 |
743 | // 获取用户关注的笔记进行展示
744 |
745 | let card = cardList[cardList.length - 1]
746 |
747 | for (let k = 0; k < cardList.length; k++) {
748 | if (cardList[k]['card']['id'] === ACTIVE_NOTE) {
749 | card = cardList[k]
750 | break;
751 | }
752 | }
753 |
754 | //设置笔记样式
755 | // left = index*40px; right = index*-40-400
756 | let note_style = {
757 | left: 0
758 | }
759 | card_list_dom.push( )
760 | } else {
761 | for (let i = 0; i < cardList.length; i++) {
762 | let card = cardList[i]
763 |
764 | //设置笔记样式
765 | // left = index*40px; right = index*-40-400
766 | let note_style = {
767 | left: i * 40 + 'px',
768 | right: -694.8 + (cardList.length - i) * 40 + 'px',
769 | flex: '0 0 auto'
770 | }
771 |
772 | let note =
773 | card_list_dom.push(note)
774 | }
775 | }
776 |
777 | // 设置网页标题
778 | for (let k = 0; k < cardList.length; k++) {
779 | if (cardList[k]['card']['id'] === ACTIVE_NOTE) {
780 |
781 | if (cardList[k]['card']['title'] !== 'About') {
782 | document.title = cardList[k]['card']['title']
783 | } else {
784 | document.title = CONFIG.title
785 | }
786 |
787 | break;
788 | }
789 | }
790 |
791 | return (
792 |
793 | //
794 |
795 |
minWidth} />
796 |
797 |
798 |
803 |
804 |
808 |
809 | {card_list_dom}
810 |
811 |
812 | {showChatWindow &&
}
823 |
824 |
825 |
826 |
827 | //
828 | )
829 | }
830 | // }
831 |
832 | }
833 |
834 | export default Post;
--------------------------------------------------------------------------------
/src/constantFunction.js:
--------------------------------------------------------------------------------
1 | import CONFIG from "./config";
2 | import { Modal } from 'antd';
3 | import heptabaseData from './resources/data.json';
4 |
5 | const { confirm } = Modal;
6 |
7 | const getCardName = (cardId) => {
8 |
9 | const cards = heptabaseData.data.cards
10 | for (let i = 0; i < cards.length; i++) {
11 | if (cards[i]['id'] === cardId) {
12 | return cards[i]
13 | }
14 | }
15 |
16 | return null
17 |
18 | }
19 |
20 | // fetch 错误时的反馈弹窗
21 | const showConfirm = () => {
22 | confirm({
23 | title: 'Sorry,some ting erro😥',
24 | // icon: ,
25 | content: 'Please try refresh',
26 | okText: 'Refresh',
27 | onOk() {
28 | console.log('Refresh');
29 | window.location.replace(window.location.href)
30 | },
31 | onCancel() {
32 | console.log('Cancel');
33 | },
34 | });
35 | };
36 |
37 | // 计算指定时间与当前的时间差
38 | const getLastEditedTime = (dateBegin) => {
39 |
40 | dateBegin = new Date(dateBegin)
41 |
42 | let dateEnd = new Date();
43 |
44 | // 时间差的毫秒数
45 | let dateDiff = dateEnd.getTime() - dateBegin.getTime()
46 | // 时间差的天数
47 | let dayDiff = Math.floor(dateDiff / (24 * 3600 * 1000))
48 |
49 | // 计算除天数外剩余的毫秒数
50 | let leave1 = dateDiff % (24 * 3600 * 1000)
51 | // 小时数
52 | let hours = Math.floor(leave1 / (3600 * 1000))
53 |
54 | // 计算除小时剩余的分钟数
55 | let leave2 = leave1 % (3600 * 1000)
56 | // 分钟数
57 | let minutes = Math.floor(leave2 / (60 * 1000))
58 |
59 | //计算相差的秒数
60 | let leave3 = leave2 % (60 * 1000)
61 | let seconds = Math.round(leave3 / 1000)
62 |
63 | return { 'day': dayDiff, 'hours': hours, 'minutes': minutes, 'seconds': seconds }
64 |
65 | }
66 |
67 | // 处理网易云音乐
68 | // 输入 markdown 格式的 URL,例如 [xxx](http:....),返回网易云音乐的 iframe HTML
69 | const setNeteaseMusic = (custom_old_card) => {
70 | // 判断类型是歌曲还是歌单
71 | let type = 2 //歌曲
72 | let height_1 = 52
73 | let height_2 = 32
74 | if (custom_old_card.indexOf('playlist') > -1 || custom_old_card.indexOf('album') > -1) {
75 |
76 | height_1 = 110
77 | height_2 = 90
78 |
79 | if (custom_old_card.indexOf('playlist') > -1) {
80 | type = 0 // 歌单
81 | }
82 | if (custom_old_card.indexOf('album') > -1) {
83 | type = 1 // 专辑
84 | }
85 | }
86 |
87 | // 获取歌曲 ID
88 | let music_id_reg = /[0-9]{4,14}/g
89 | let music_id_list = custom_old_card.match(music_id_reg)
90 |
91 | if (music_id_list) {
92 | // 匹配到 ID
93 | let music_id = music_id_list[0]
94 | let netease_music_iframe = '
'
95 |
96 | return netease_music_iframe
97 |
98 | } else {
99 | return undefined
100 | }
101 |
102 | }
103 |
104 | // 修复单个 md 文件中的 img
105 | const getClearImag = (card) => {
106 |
107 | // 修改图片后缀,避免图片无法显示
108 | // 找到  符号
111 | // 删除前面 2 步 index 中间的符号
112 |
113 | let content = card['content']
114 |
115 | // 支持的图片类型
116 | let img_type = ['.png', '.jpeg', '.jpg', '.gif']
117 | // 包含以下关键字则认为是图片
118 | let img_keyword_index = content.indexOf('![')
119 |
120 | while (img_keyword_index !== -1) {
121 |
122 |
123 | // 获取下一个 ) 索引
124 | let img_end_inex = content.indexOf(')', img_keyword_index)
125 |
126 | // 获取下一个 ] 索引
127 | let img_alt_end_inex = content.indexOf(']', img_keyword_index)
128 |
129 | // 获取图片扩展名索引
130 | let img_etc_index
131 | for (let i = 0; i < img_type.length; i++) {
132 | img_etc_index = content.indexOf(img_type[i], img_keyword_index + 1)
133 | if (img_etc_index >= 0 && img_etc_index <= img_end_inex) {
134 |
135 | // 如果格式字符是这种格式 ![....jpg] 内,则跳过
136 | if (content.substring(img_etc_index + img_type[i].length, img_etc_index + img_type[i].length + 2) === '](') {
137 | img_etc_index = content.indexOf(img_type[i], img_etc_index + 1)
138 |
139 | }
140 |
141 | img_etc_index += img_type[i].length
142 | break;
143 |
144 |
145 | }
146 | }
147 |
148 | if (img_keyword_index === -1 || img_end_inex === -1 || img_etc_index === -1) {
149 | break
150 | }
151 |
152 | let img_alt = content.substring(img_keyword_index + 2, img_alt_end_inex)
153 | let img_src = content.substring(img_alt_end_inex + 2, img_etc_index)
154 |
155 | // console.log('image keyword');
156 | // console.log(img_alt);
157 | // console.log(img_src);
158 |
159 | let old_img_str = content.substring(img_keyword_index, img_end_inex + 1)
160 |
161 |
162 | // 获取 = 索引
163 | let img_width_inex = old_img_str.indexOf('=')
164 |
165 | if (img_width_inex > -1 && old_img_str.indexOf('{{width') < 0) {
166 | //将图片宽度保存到 alt 中
167 | img_alt = img_alt + '{{width ' + old_img_str.substring(img_width_inex + 1, old_img_str.length - 2) + '}}'
168 | }
169 |
170 | let new_img_str = ''
171 |
172 | content = content.replace(old_img_str, new_img_str)
173 |
174 | // 获取 ![ 索引
175 | img_keyword_index = content.indexOf('![', img_keyword_index + 1)
176 |
177 |
178 | }
179 | card['content'] = content
180 | return card
181 |
182 | }
183 |
184 | // 处理单个 md 文件中的超链接
185 | const getClearCard = (card, cards) => {
186 |
187 | // // 找到 (./ 符号以及之后的第 1 个 ,或找到 {{ 符号 }}) 符号,截取这 2 个 index 中间的字符串
188 | // // 将上述字符串放在 card 数据中匹配
189 | // // 如果找到匹配的卡片:修改上述字符串的地址为 /post/post.id
190 | // let content = card['content']
191 | let this_card_id = card['id']
192 |
193 | // 处理反向连接
194 | // 如果 A 卡片中存在当前笔记的 ID,则 A 卡片为当前笔记的反向链接之一
195 | let backLinks = []
196 | for (let i = 0; i < cards.length; i++) {
197 | let content = cards[i]['content']
198 | if (typeof (content) !== 'string') {
199 | content = cards[i]['content'].innerHTML
200 | }
201 |
202 | if (content.indexOf(this_card_id) >= 0 && cards[i]['id'] !== this_card_id) {
203 |
204 | backLinks.push(cards[i])
205 |
206 | }
207 |
208 |
209 | }
210 |
211 | // card['content'] = content
212 | return { 'card': card, 'backLinks': backLinks }
213 |
214 | }
215 |
216 | // 从服务端获取 Heptabase 的笔记数据
217 | const getHeptabaseDataFromServer = async () => {
218 | let myHeaders = new Headers();
219 | myHeaders.append("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
220 |
221 | let requestOptions = {
222 | method: 'GET',
223 | headers: myHeaders,
224 | redirect: 'follow'
225 | };
226 |
227 | try {
228 | const whiteboard_id = CONFIG.whiteboard_id;
229 | const result = await fetch("https://api.blog.kii.la/?shared-id=" + whiteboard_id, requestOptions);
230 | const getDataResponse = await result.json();
231 |
232 | if (getDataResponse.code === 0) {
233 | // 成功获取数据
234 |
235 | const data = getDataResponse
236 | // 处理卡片数据
237 | const newData = handleHeptabaseData(data)
238 | return data
239 |
240 |
241 | } else {
242 | // 未成功获取,需要添加此白板到服务端中
243 |
244 | let myHeaders = new Headers();
245 | myHeaders.append("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
246 | myHeaders.append("Content-Type", "application/json");
247 |
248 | let raw = JSON.stringify({
249 | "shared_id": whiteboard_id,
250 | "allow_origin": []
251 | });
252 |
253 | let requestOptions = {
254 | method: 'POST',
255 | headers: myHeaders,
256 | body: raw,
257 | redirect: 'follow'
258 | };
259 |
260 | const result = await fetch("https://api.blog.kii.la/add", requestOptions)
261 | const addWhiteboardResponse = await result.json();
262 |
263 | if (addWhiteboardResponse.code === 0) {
264 |
265 | setTimeout(() => {
266 | // 添加白板后再次获取一次数据
267 | getHeptabaseDataFromServer()
268 | }, 5000);
269 |
270 | }
271 |
272 | }
273 |
274 |
275 | } catch (error) {
276 |
277 | console.log('error', error);
278 |
279 | }
280 | };
281 |
282 | // 获取 Heptabase 的笔记数据
283 | const getHeptabaseData = async () => {
284 | console.log('getHeptabaseData');
285 |
286 | return handleHeptabaseData(heptabaseData)
287 |
288 | // 获取本地数据
289 | let heptabaseDataFromLocal = JSON.parse(localStorage.getItem("heptabase_blog_data"))
290 |
291 |
292 | if (heptabaseDataFromLocal) {
293 |
294 | // 存在本地数据
295 | if (heptabaseDataFromLocal.data?.Etag && heptabaseDataFromLocal.whiteboard_id) {
296 |
297 | // 判断本地数据是否需要更新
298 | let myHeaders = new Headers();
299 | myHeaders.append("User-Agent", "Apifox/1.0.0 (https://apifox.com)");
300 |
301 | let requestOptions = {
302 | method: 'GET',
303 | headers: myHeaders,
304 | redirect: 'follow'
305 | };
306 |
307 | const whiteboard_id = CONFIG.whiteboard_id;
308 | const result = await fetch("https://api.blog.kii.la/etag?shared-id=" + whiteboard_id, requestOptions)
309 | const etagFromServer = await result.json();
310 |
311 | console.log('etagFromServer:');
312 | console.log(etagFromServer);
313 |
314 | // Etag 不同或者本地缓存的白板 ID 与配置中的不同
315 | if (etagFromServer.data !== heptabaseDataFromLocal.data.Etag || heptabaseDataFromLocal.whiteboard_id !== whiteboard_id) {
316 | //需要更新
317 | const data = await getHeptabaseDataFromServer();
318 | return data;
319 | } else {
320 | //不需要更新
321 | return heptabaseDataFromLocal;
322 | }
323 | } else {
324 | // 需要到服务端获取
325 | const data = await getHeptabaseDataFromServer();
326 | return data;
327 | }
328 |
329 | } else {
330 | // 本地不存在数据,则需要到服务端获取
331 | const heptabaseDataFromServer = await getHeptabaseDataFromServer();
332 | return heptabaseDataFromServer;
333 | }
334 | };
335 |
336 |
337 | const handleHeptabaseData = (data) => {
338 |
339 | data.data.cards = data.data.cards.sort((a, b) => {
340 |
341 | // 最近编辑时间
342 | return b.lastEditedTime < a.lastEditedTime ? -1 : 1
343 |
344 | })
345 |
346 | let pages = {}
347 | // 获取 About、Projects 页面的数据
348 | pages.about = undefined
349 | pages.firstPage = undefined
350 | // pages.projects = undefined
351 |
352 | // 存储去重后的数组
353 | let new_cards = []
354 | // 存储卡片 ID,用户判断是否重复
355 | let cards_id = []
356 |
357 | const configPages = CONFIG.pages
358 | const firstPageKey = Object.keys(configPages)[0]
359 | const firstPageId = configPages[firstPageKey];
360 |
361 |
362 | for (let i = 0; i < data.data.cards.length; i++) {
363 |
364 | // 首页
365 | if (data.data.cards[i]['title'].toLowerCase() === 'about') {
366 |
367 | pages.about = data.data.cards[i]
368 |
369 | }
370 |
371 | // 查找 CONFIG 的 pages 中第 1 个卡片的数据
372 | if (data.data.cards[i].id === firstPageId) {
373 | pages.firstPage = data.data.cards[i]
374 | }
375 |
376 |
377 | // Projects
378 | // if (data.data.cards[i]['title'].toLowerCase() === 'projects') {
379 |
380 | // pages.projects = data.data.cards[i]
381 |
382 | // }
383 |
384 | // 去重
385 | if (cards_id.indexOf(data.data.cards[i]['id']) > -1) {
386 | // 已存在此卡片,则忽略
387 | // console.log(data.cards[i]);
388 | } else {
389 |
390 | // 不存在此卡片
391 |
392 | // 最近编辑的时间差
393 | let timeDiff = getLastEditedTime(data.data.cards[i]['lastEditedTime'])
394 | data.data.cards[i].lastEditedTimeDiff = ''
395 | if (timeDiff['day'] > 0) {
396 | data.data.cards[i].lastEditedTimeDiff = 'Edited ' + timeDiff['day'] + ' days ago'
397 | } else if (timeDiff['hours'] > 0) {
398 |
399 | data.data.cards[i].lastEditedTimeDiff = 'Edited ' + timeDiff['hours'] + ' hours ago'
400 |
401 | } else if (timeDiff['minutes'] > 0) {
402 |
403 | data.data.cards[i].lastEditedTimeDiff = 'Edited ' + timeDiff['minutes'] + ' minutes ago'
404 |
405 | } else {
406 |
407 | data.data.cards[i].lastEditedTimeDiff = 'Edited just'
408 |
409 | }
410 |
411 | new_cards.push(data.data.cards[i])
412 | cards_id.push(data.data.cards[i]['id'])
413 |
414 | }
415 |
416 | }
417 |
418 | data.data.cards = new_cards
419 | data.frontGetTime = Date.parse(new Date()) / 1000
420 | data.pages = pages
421 | data.whiteboard_id = CONFIG.whiteboard_id
422 |
423 | // 存储数据到本地缓存
424 |
425 | try {
426 | localStorage.setItem("heptabase_blog_data", JSON.stringify(data))
427 | } catch (error) {
428 | console.log(error);
429 | }
430 |
431 | return data; // 返回结果
432 |
433 | }
434 |
435 |
436 |
437 | /**
438 | * @param {Object} Hpeta_card_data Hepta 卡片数据
439 | * @returns 返回拼接后的 DOM 元素
440 | */
441 | const heptaToMD = (Hpeta_card_data) => {
442 |
443 | // 如果对象已经是 DOM 则直接返回
444 | if (Hpeta_card_data['content'] instanceof HTMLElement) {
445 | return Hpeta_card_data['content']
446 | }
447 |
448 | let parent_card_id = Hpeta_card_data['id']
449 | let box = document.createElement('div')
450 | box = heptaContentTomd(JSON.parse(Hpeta_card_data['content'])['content'], box, parent_card_id)
451 | return box
452 |
453 |
454 | }
455 |
456 | /**
457 | *
458 | * @param {list} content_list block 列表
459 | * @param {string} parent_node 要添加子元素的父级 DOM 元素
460 | * @param {string} parent_card_id 当前卡片的 ID
461 | * @returns 返回拼接后的 md 字符串
462 | */
463 | const heptaContentTomd = (content_list, parent_node, parent_card_id) => {
464 |
465 | let new_node
466 | let number_list_index = 1
467 |
468 | //遍历 content list
469 | for (let i = 0; i < content_list.length; i++) {
470 |
471 | // 根据 type 进行处理
472 | switch (content_list[i]['type']) {
473 |
474 | case 'heading':
475 |
476 | new_node = document.createElement('H' + content_list[i]['attrs']['level'])
477 |
478 | break
479 |
480 | case 'card':
481 | new_node = document.createElement('span')
482 | new_node.innerHTML = content_list[i]['attrs']['cardTitle']
483 | if (content_list[i]['attrs']['cardTitle'] === undefined) {
484 | // 找不到卡片标题,根据卡片 ID 匹配标题
485 | const card = getCardName(content_list[i]['attrs']['cardId'])
486 |
487 | if (card) {
488 | new_node.innerHTML = card.title
489 | }
490 |
491 | }
492 |
493 | let bingo = false
494 |
495 | if (content_list[i]['attrs']['cardTitle'] === 'Invalid card') {
496 | // 未知卡片
497 | // 在数据中先找一下
498 | let heptabase_blog_data = heptabaseData
499 |
500 | for (let k = 0; k < heptabase_blog_data.data.cards.length; k++) {
501 | if (heptabase_blog_data.data.cards[k]['id'] === content_list[i]['attrs']['cardId']) {
502 | new_node.innerHTML = heptabase_blog_data.data.cards[k]['title']
503 | bingo = true
504 | break
505 | }
506 | }
507 |
508 | // if (bingo === true) {
509 | // new_node.classList.add('my_link')
510 | // new_node.classList.add('article_link')
511 | // new_node.setAttribute('path', '/post/' + content_list[i]['attrs']['cardId'])
512 | // new_node.setAttribute('parent_note_id', parent_card_id)
513 | // } else {
514 | // new_node.classList.add('unknown_card')
515 | // }
516 |
517 |
518 | }
519 |
520 | if (bingo === true || content_list[i]['attrs']['cardTitle'] !== 'Invalid card') {
521 | new_node.classList.add('my_link')
522 | new_node.classList.add('article_link')
523 | new_node.setAttribute('path', '/post/' + content_list[i]['attrs']['cardId'])
524 | new_node.setAttribute('parent_note_id', parent_card_id)
525 | } else {
526 | new_node.classList.add('unknown_card')
527 | }
528 |
529 |
530 |
531 |
532 |
533 |
534 |
535 | break
536 |
537 | case 'whiteboard':
538 | new_node = document.createTextNode(content_list[i]['attrs']['whiteboardName'])
539 | break
540 |
541 | case 'image':
542 |
543 | new_node = document.createElement('div')
544 | let imgBox = document.createElement('img')
545 | imgBox.setAttribute('src', content_list[i]['attrs']['src'])
546 | new_node.classList.add('imgBox')
547 | new_node.appendChild(imgBox)
548 |
549 | if (content_list[i]['attrs']['width'] !== null) {
550 | imgBox.setAttribute('style', 'width: ' + content_list[i]['attrs']['width']);
551 | }
552 |
553 |
554 | break
555 |
556 | case 'paragraph':
557 | // 如果父元素不是 task-list-item ,则创建 P 元素
558 | if (parent_node) {
559 |
560 | if (parent_node['className'] !== 'task-list-item') {
561 | new_node = document.createElement('p')
562 | } else {
563 | new_node = document.createElement('span')
564 | new_node.setAttribute('style', 'margin-left:4px');
565 | }
566 |
567 | }
568 |
569 | break
570 |
571 | case 'text':
572 | // 普通文本
573 | if (content_list[i]['text'].indexOf('{HTML}') > -1) {
574 | break
575 | }
576 |
577 |
578 | // 判断是否有行内样式,例如 strong、mark
579 |
580 | if ('marks' in content_list[i]) {
581 |
582 | // 有行内样式
583 | content_list[i]['marks'].forEach(mark => {
584 |
585 | switch (mark['type']) {
586 |
587 | // del-line
588 | case 'strike':
589 | new_node = document.createElement('del')
590 | new_node.innerText = content_list[i]['text']
591 | break
592 |
593 | // inline-code
594 | case 'code':
595 | new_node = document.createElement('code')
596 | new_node.innerText = content_list[i]['text']
597 | break
598 |
599 | // italic
600 | case 'em':
601 | new_node = document.createElement('em')
602 | new_node.innerText = content_list[i]['text']
603 | break
604 |
605 | // strong
606 | case 'strong':
607 | new_node = document.createElement('strong')
608 | new_node.innerText = content_list[i]['text']
609 | break
610 |
611 | case 'color':
612 |
613 | new_node = document.createElement('span')
614 |
615 | if (mark['attrs']['color']) {
616 |
617 | if (mark['attrs']['type'] === 'background') {
618 | // new_node.setAttribute('style', 'background-color: ' + mark['attrs']['color']);
619 |
620 | new_node.classList.add('highlight_bg')
621 | } else {
622 | // new_node.setAttribute('style', 'color: ' + mark['attrs']['color']);
623 | new_node.classList.add('highlight_color')
624 | }
625 |
626 | }
627 |
628 | new_node.innerText = content_list[i]['text']
629 | break
630 |
631 | case 'link':
632 | // let link_title = mark['attrs']['title']
633 | // if (link_title === null) {
634 | // link_title = mark['attrs']['href']
635 | // }
636 |
637 | if (mark['attrs']['data-internal-href'] !== null) {
638 | // 内部卡片链接
639 | new_node = document.createElement('span')
640 | new_node.innerHTML = content_list[i]['text']
641 | new_node.classList.add('my_link')
642 | new_node.classList.add('article_link')
643 | new_node.setAttribute('path', '/post/' + mark['attrs']['data-internal-href'].replace('meta://card/', ''))
644 | new_node.setAttribute('parent_note_id', parent_card_id)
645 |
646 | } else {
647 |
648 | if (mark['attrs']['href'].indexOf('app.heptabase') > -1 && mark['attrs']['href'].indexOf('card/') > -1) {
649 | // Link to block
650 | // 获取 card ID
651 | let card_id_index_start = mark['attrs']['href'].indexOf('card/')
652 | let card_id_index_end = mark['attrs']['href'].indexOf('#')
653 |
654 | if (card_id_index_start > -1) {
655 | let card_id = mark['attrs']['href'].substring(card_id_index_start + 5, card_id_index_end > -1 ? card_id_index_end : mark['attrs']['href'].length)
656 |
657 | new_node = document.createElement('span')
658 | new_node.innerHTML = content_list[i]['text']
659 | new_node.classList.add('my_link')
660 | new_node.classList.add('article_link')
661 | new_node.setAttribute('path', '/post/' + card_id)
662 | new_node.setAttribute('parent_note_id', parent_card_id)
663 |
664 | } else {
665 | // 外链
666 | new_node = document.createElement('a')
667 | new_node.classList.add('external_link')
668 | new_node.href = mark['attrs']['href']
669 | new_node.innerHTML = content_list[i]['text']
670 | }
671 |
672 | } else {
673 | // 外链
674 | new_node = document.createElement('a')
675 | new_node.classList.add('external_link')
676 | new_node.href = mark['attrs']['href']
677 | new_node.innerHTML = content_list[i]['text']
678 | }
679 |
680 |
681 |
682 |
683 | }
684 |
685 | break
686 | default:
687 | break
688 |
689 | }
690 |
691 | });
692 | } else {
693 | // 无行内样式
694 | // new_node = document.createElement('span')
695 | // new_node.innerText = new_node.innerText + content_list[i]['text']
696 |
697 | new_node = document.createTextNode(content_list[i]['text'])
698 |
699 | }
700 |
701 | break
702 |
703 | case 'bullet_list_item':
704 | // List 容器
705 | const bulletListBox = document.createElement('div')
706 | bulletListBox.classList.add('listBox')
707 |
708 | // List 手柄
709 | const bulletHand = document.createElement('div')
710 | bulletHand.classList.add('listBullet')
711 |
712 | // List 内容
713 | new_node = document.createElement('div')
714 | new_node.setAttribute('style', 'overflow: auto');
715 |
716 | bulletListBox.appendChild(bulletHand)
717 | bulletListBox.appendChild(new_node)
718 |
719 | parent_node.appendChild(bulletListBox)
720 |
721 | break
722 |
723 | case 'numbered_list_item':
724 |
725 | // 如果上一个节点不是 number_list 则此节点的 index 为 1,否则 index +=1
726 | if (i > 0) {
727 | if (content_list[i - 1]['type'] !== 'numbered_list_item') {
728 | number_list_index = 1
729 | } else {
730 | number_list_index += 1
731 | }
732 | }
733 |
734 | // List 容器
735 | const numberListBox = document.createElement('div')
736 | numberListBox.classList.add('listBox')
737 |
738 | // List 手柄
739 | const numberHand = document.createElement('div')
740 | // numberHand.classList.add('listBullet')
741 | numberHand.classList.add('numberListBullet')
742 | numberHand.setAttribute('data-before', number_list_index + '.')
743 | // numberHand.attr('--before-content', beforeContent)
744 |
745 | // List 内容
746 | new_node = document.createElement('div')
747 | new_node.setAttribute('style', 'overflow: auto');
748 |
749 | numberListBox.appendChild(numberHand)
750 | numberListBox.appendChild(new_node)
751 |
752 | parent_node.appendChild(numberListBox)
753 |
754 | break
755 |
756 | case 'todo_list_item':
757 | new_node = document.createElement('li')
758 |
759 | let task_input = document.createElement('input')
760 | task_input.type = 'checkbox'
761 | // task_input.checked = 'true'
762 | if (content_list[i]['attrs']['checked']) {
763 | task_input.setAttribute("checked", content_list[i]['attrs']['checked']);
764 | }
765 |
766 | task_input.disabled = true
767 |
768 | new_node.classList.add('task-list-item')
769 | // new_node.setAttribute('style', 'margin: 16px 0');
770 | new_node.appendChild(task_input)
771 | break
772 |
773 | case 'ordered_list':
774 | new_node = document.createElement('ol')
775 | break
776 |
777 | case 'bullet_list':
778 | new_node = document.createElement('ul')
779 | break
780 |
781 | case 'toggle_list':
782 | new_node = document.createElement('div')
783 | break
784 |
785 | case 'toggle_list_item':
786 | new_node = document.createElement('div')
787 | break
788 |
789 | case 'task_list':
790 | new_node = document.createElement('ul')
791 | new_node.classList.add('task-list')
792 | break
793 |
794 | case 'list_item':
795 | new_node = document.createElement('li')
796 |
797 | // 如果是 task
798 | if (parent_node.className.indexOf('task-list') > -1) {
799 | let task_input = document.createElement('input')
800 | task_input.type = 'checkbox'
801 | // task_input.checked = 'true'
802 | if (content_list[i]['attrs']['checked']) {
803 | task_input.setAttribute("checked", content_list[i]['attrs']['checked']);
804 | }
805 |
806 | task_input.disabled = true
807 |
808 | new_node.classList.add('task-list-item')
809 | // new_node.setAttribute('style', 'margin: 16px 0');
810 | new_node.appendChild(task_input)
811 | }
812 | break
813 |
814 | case 'horizontal_rule':
815 | new_node = document.createElement('hr')
816 | break
817 |
818 | case 'blockquote':
819 | new_node = document.createElement('blockquote')
820 | break
821 |
822 | case 'code_block':
823 |
824 | new_node = document.createElement('pre')
825 | new_node.classList.add('hljs')
826 | new_node.classList.add('language-' + content_list[i]['attrs']['params'])
827 |
828 | // new_node = React.createElement('SyntaxHighlighter')
829 |
830 | // 直接渲染 code block 内的 HTML
831 | if ('content' in content_list[i] && content_list[i]['attrs']['params'] === 'html') {
832 | if (content_list[i]['content'][0]['text'].indexOf('{HTML}') > -1) {
833 | new_node = document.createElement('div')
834 | new_node.classList.add('htmlBox')
835 | new_node.innerHTML = content_list[i]['content'][0]['text'].replace('{HTML}', '')
836 | // new_node.innerHTML = ''
837 | }
838 | }
839 |
840 | break
841 |
842 | case 'table':
843 | new_node = document.createElement('table')
844 | break
845 |
846 | case 'table_row':
847 | new_node = document.createElement('tr')
848 | break
849 |
850 | case 'table_header':
851 | new_node = document.createElement('th')
852 | break
853 |
854 | case 'table_cell':
855 | new_node = document.createElement('td')
856 | break
857 |
858 | case 'video':
859 | new_node = document.createElement('video')
860 | new_node.src = content_list[i]['attrs']['url']
861 | break
862 |
863 | case 'math_inline':
864 | new_node = document.createElement('span')
865 | break
866 |
867 | default:
868 | break
869 |
870 | }
871 |
872 |
873 |
874 | if (new_node !== undefined && parent_node !== undefined) {
875 |
876 | try {
877 | if (content_list[i]['type'] === 'numbered_list_item' || content_list[i]['type'] === 'bullet_list_item') {
878 | // parent_node.appendChild(new_node)
879 | } else {
880 | parent_node.appendChild(new_node)
881 | }
882 |
883 |
884 | } catch (error) {
885 | console.log(parent_node);
886 | }
887 |
888 | } else {
889 | console.log(parent_node);
890 | }
891 |
892 | if (new_node === undefined) {
893 | console.log(new_node);
894 | // new_node = parent_node
895 | }
896 |
897 | if (parent_node === undefined) {
898 | console.log(parent_node);
899 | // new_node = parent_node
900 | }
901 |
902 |
903 | // 如果还有子 content
904 | if ('content' in content_list[i]) {
905 |
906 | heptaContentTomd(content_list[i]['content'], new_node, parent_card_id)
907 |
908 | }
909 |
910 | }
911 |
912 |
913 |
914 | return parent_node
915 |
916 | }
917 |
918 |
919 | export { getHeptabaseData, getClearImag, getClearCard, heptaToMD }
--------------------------------------------------------------------------------
/src/output.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
3 | */
4 |
5 | /*
6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
8 | */
9 |
10 | *,
11 | ::before,
12 | ::after {
13 | box-sizing: border-box;
14 | /* 1 */
15 | border-width: 0;
16 | /* 2 */
17 | border-style: solid;
18 | /* 2 */
19 | border-color: #e5e7eb;
20 | /* 2 */
21 | }
22 |
23 | ::before,
24 | ::after {
25 | --tw-content: '';
26 | }
27 |
28 | /*
29 | 1. Use a consistent sensible line-height in all browsers.
30 | 2. Prevent adjustments of font size after orientation changes in iOS.
31 | 3. Use a more readable tab size.
32 | 4. Use the user's configured `sans` font-family by default.
33 | 5. Use the user's configured `sans` font-feature-settings by default.
34 | 6. Use the user's configured `sans` font-variation-settings by default.
35 | 7. Disable tap highlights on iOS
36 | */
37 |
38 | html,
39 | :host {
40 | line-height: 1.5;
41 | /* 1 */
42 | -webkit-text-size-adjust: 100%;
43 | /* 2 */
44 | /* 3 */
45 | tab-size: 4;
46 | /* 3 */
47 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
48 | /* 4 */
49 | -webkit-font-feature-settings: normal;
50 | font-feature-settings: normal;
51 | /* 5 */
52 | font-variation-settings: normal;
53 | /* 6 */
54 | -webkit-tap-highlight-color: transparent;
55 | /* 7 */
56 | }
57 |
58 | /*
59 | 1. Remove the margin in all browsers.
60 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
61 | */
62 |
63 | body {
64 | margin: 0;
65 | /* 1 */
66 | line-height: inherit;
67 | /* 2 */
68 | }
69 |
70 | /*
71 | 1. Add the correct height in Firefox.
72 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
73 | 3. Ensure horizontal rules are visible by default.
74 | */
75 |
76 | hr {
77 | height: 0;
78 | /* 1 */
79 | color: inherit;
80 | /* 2 */
81 | border-top-width: 1px;
82 | /* 3 */
83 | }
84 |
85 | /*
86 | Add the correct text decoration in Chrome, Edge, and Safari.
87 | */
88 |
89 | abbr:where([title]) {
90 | -webkit-text-decoration: underline dotted;
91 | text-decoration: underline dotted;
92 | }
93 |
94 | /*
95 | Remove the default font size and weight for headings.
96 | */
97 |
98 | h1,
99 | h2,
100 | h3,
101 | h4,
102 | h5,
103 | h6 {
104 | font-size: inherit;
105 | font-weight: inherit;
106 | }
107 |
108 | /*
109 | Reset links to optimize for opt-in styling instead of opt-out.
110 | */
111 |
112 | a {
113 | color: inherit;
114 | text-decoration: inherit;
115 | }
116 |
117 | /*
118 | Add the correct font weight in Edge and Safari.
119 | */
120 |
121 | b,
122 | strong {
123 | font-weight: bolder;
124 | }
125 |
126 | /*
127 | 1. Use the user's configured `mono` font-family by default.
128 | 2. Use the user's configured `mono` font-feature-settings by default.
129 | 3. Use the user's configured `mono` font-variation-settings by default.
130 | 4. Correct the odd `em` font sizing in all browsers.
131 | */
132 |
133 | code,
134 | kbd,
135 | samp,
136 | pre {
137 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
138 | /* 1 */
139 | -webkit-font-feature-settings: normal;
140 | font-feature-settings: normal;
141 | /* 2 */
142 | font-variation-settings: normal;
143 | /* 3 */
144 | font-size: 1em;
145 | /* 4 */
146 | }
147 |
148 | /*
149 | Add the correct font size in all browsers.
150 | */
151 |
152 | small {
153 | font-size: 80%;
154 | }
155 |
156 | /*
157 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
158 | */
159 |
160 | sub,
161 | sup {
162 | font-size: 75%;
163 | line-height: 0;
164 | position: relative;
165 | vertical-align: baseline;
166 | }
167 |
168 | sub {
169 | bottom: -0.25em;
170 | }
171 |
172 | sup {
173 | top: -0.5em;
174 | }
175 |
176 | /*
177 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
178 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
179 | 3. Remove gaps between table borders by default.
180 | */
181 |
182 | table {
183 | text-indent: 0;
184 | /* 1 */
185 | border-color: inherit;
186 | /* 2 */
187 | border-collapse: collapse;
188 | /* 3 */
189 | }
190 |
191 | /*
192 | 1. Change the font styles in all browsers.
193 | 2. Remove the margin in Firefox and Safari.
194 | 3. Remove default padding in all browsers.
195 | */
196 |
197 | button,
198 | input,
199 | optgroup,
200 | select,
201 | textarea {
202 | font-family: inherit;
203 | /* 1 */
204 | -webkit-font-feature-settings: inherit;
205 | font-feature-settings: inherit;
206 | /* 1 */
207 | font-variation-settings: inherit;
208 | /* 1 */
209 | font-size: 100%;
210 | /* 1 */
211 | font-weight: inherit;
212 | /* 1 */
213 | line-height: inherit;
214 | /* 1 */
215 | letter-spacing: inherit;
216 | /* 1 */
217 | color: inherit;
218 | /* 1 */
219 | margin: 0;
220 | /* 2 */
221 | padding: 0;
222 | /* 3 */
223 | }
224 |
225 | /*
226 | Remove the inheritance of text transform in Edge and Firefox.
227 | */
228 |
229 | button,
230 | select {
231 | text-transform: none;
232 | }
233 |
234 | /*
235 | 1. Correct the inability to style clickable types in iOS and Safari.
236 | 2. Remove default button styles.
237 | */
238 |
239 | button,
240 | input:where([type='button']),
241 | input:where([type='reset']),
242 | input:where([type='submit']) {
243 | -webkit-appearance: button;
244 | /* 1 */
245 | background-color: transparent;
246 | /* 2 */
247 | background-image: none;
248 | /* 2 */
249 | }
250 |
251 | /*
252 | Use the modern Firefox focus style for all focusable elements.
253 | */
254 |
255 | :-moz-focusring {
256 | outline: auto;
257 | }
258 |
259 | /*
260 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
261 | */
262 |
263 | :-moz-ui-invalid {
264 | box-shadow: none;
265 | }
266 |
267 | /*
268 | Add the correct vertical alignment in Chrome and Firefox.
269 | */
270 |
271 | progress {
272 | vertical-align: baseline;
273 | }
274 |
275 | /*
276 | Correct the cursor style of increment and decrement buttons in Safari.
277 | */
278 |
279 | ::-webkit-inner-spin-button,
280 | ::-webkit-outer-spin-button {
281 | height: auto;
282 | }
283 |
284 | /*
285 | 1. Correct the odd appearance in Chrome and Safari.
286 | 2. Correct the outline style in Safari.
287 | */
288 |
289 | [type='search'] {
290 | -webkit-appearance: textfield;
291 | /* 1 */
292 | outline-offset: -2px;
293 | /* 2 */
294 | }
295 |
296 | /*
297 | Remove the inner padding in Chrome and Safari on macOS.
298 | */
299 |
300 | ::-webkit-search-decoration {
301 | -webkit-appearance: none;
302 | }
303 |
304 | /*
305 | 1. Correct the inability to style clickable types in iOS and Safari.
306 | 2. Change font properties to `inherit` in Safari.
307 | */
308 |
309 | ::-webkit-file-upload-button {
310 | -webkit-appearance: button;
311 | /* 1 */
312 | font: inherit;
313 | /* 2 */
314 | }
315 |
316 | /*
317 | Add the correct display in Chrome and Safari.
318 | */
319 |
320 | summary {
321 | display: list-item;
322 | }
323 |
324 | /*
325 | Removes the default spacing and border for appropriate elements.
326 | */
327 |
328 | blockquote,
329 | dl,
330 | dd,
331 | h1,
332 | h2,
333 | h3,
334 | h4,
335 | h5,
336 | h6,
337 | hr,
338 | figure,
339 | p,
340 | pre {
341 | margin: 0;
342 | }
343 |
344 | fieldset {
345 | margin: 0;
346 | padding: 0;
347 | }
348 |
349 | legend {
350 | padding: 0;
351 | }
352 |
353 | ol,
354 | ul,
355 | menu {
356 | list-style: none;
357 | margin: 0;
358 | padding: 0;
359 | }
360 |
361 | /*
362 | Reset default styling for dialogs.
363 | */
364 |
365 | dialog {
366 | padding: 0;
367 | }
368 |
369 | /*
370 | Prevent resizing textareas horizontally by default.
371 | */
372 |
373 | textarea {
374 | resize: vertical;
375 | }
376 |
377 | /*
378 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
379 | 2. Set the default placeholder color to the user's configured gray 400 color.
380 | */
381 |
382 | input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
383 | opacity: 1;
384 | /* 1 */
385 | color: #9ca3af;
386 | /* 2 */
387 | }
388 |
389 | input::placeholder,
390 | textarea::placeholder {
391 | opacity: 1;
392 | /* 1 */
393 | color: #9ca3af;
394 | /* 2 */
395 | }
396 |
397 | /*
398 | Set the default cursor for buttons.
399 | */
400 |
401 | button,
402 | [role="button"] {
403 | cursor: pointer;
404 | }
405 |
406 | /*
407 | Make sure disabled buttons don't get the pointer cursor.
408 | */
409 |
410 | :disabled {
411 | cursor: default;
412 | }
413 |
414 | /*
415 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
416 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
417 | This can trigger a poorly considered lint error in some tools but is included by design.
418 | */
419 |
420 | img,
421 | svg,
422 | video,
423 | canvas,
424 | audio,
425 | iframe,
426 | embed,
427 | object {
428 | display: block;
429 | /* 1 */
430 | vertical-align: middle;
431 | /* 2 */
432 | }
433 |
434 | /*
435 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
436 | */
437 |
438 | img,
439 | video {
440 | max-width: 100%;
441 | height: auto;
442 | }
443 |
444 | /* Make elements with the HTML hidden attribute stay hidden by default */
445 |
446 | [hidden] {
447 | display: none;
448 | }
449 |
450 | :root, [data-theme] {
451 | color: hsl(var(--nextui-foreground));
452 | background-color: hsl(var(--nextui-background));
453 | }
454 |
455 | *, ::before, ::after {
456 | --tw-border-spacing-x: 0;
457 | --tw-border-spacing-y: 0;
458 | --tw-translate-x: 0;
459 | --tw-translate-y: 0;
460 | --tw-rotate: 0;
461 | --tw-skew-x: 0;
462 | --tw-skew-y: 0;
463 | --tw-scale-x: 1;
464 | --tw-scale-y: 1;
465 | --tw-pan-x: ;
466 | --tw-pan-y: ;
467 | --tw-pinch-zoom: ;
468 | --tw-scroll-snap-strictness: proximity;
469 | --tw-gradient-from-position: ;
470 | --tw-gradient-via-position: ;
471 | --tw-gradient-to-position: ;
472 | --tw-ordinal: ;
473 | --tw-slashed-zero: ;
474 | --tw-numeric-figure: ;
475 | --tw-numeric-spacing: ;
476 | --tw-numeric-fraction: ;
477 | --tw-ring-inset: ;
478 | --tw-ring-offset-width: 0px;
479 | --tw-ring-offset-color: #fff;
480 | --tw-ring-color: rgb(59 130 246 / 0.5);
481 | --tw-ring-offset-shadow: 0 0 #0000;
482 | --tw-ring-shadow: 0 0 #0000;
483 | --tw-shadow: 0 0 #0000;
484 | --tw-shadow-colored: 0 0 #0000;
485 | --tw-blur: ;
486 | --tw-brightness: ;
487 | --tw-contrast: ;
488 | --tw-grayscale: ;
489 | --tw-hue-rotate: ;
490 | --tw-invert: ;
491 | --tw-saturate: ;
492 | --tw-sepia: ;
493 | --tw-drop-shadow: ;
494 | --tw-backdrop-blur: ;
495 | --tw-backdrop-brightness: ;
496 | --tw-backdrop-contrast: ;
497 | --tw-backdrop-grayscale: ;
498 | --tw-backdrop-hue-rotate: ;
499 | --tw-backdrop-invert: ;
500 | --tw-backdrop-opacity: ;
501 | --tw-backdrop-saturate: ;
502 | --tw-backdrop-sepia: ;
503 | --tw-contain-size: ;
504 | --tw-contain-layout: ;
505 | --tw-contain-paint: ;
506 | --tw-contain-style: ;
507 | }
508 |
509 | ::-webkit-backdrop {
510 | --tw-border-spacing-x: 0;
511 | --tw-border-spacing-y: 0;
512 | --tw-translate-x: 0;
513 | --tw-translate-y: 0;
514 | --tw-rotate: 0;
515 | --tw-skew-x: 0;
516 | --tw-skew-y: 0;
517 | --tw-scale-x: 1;
518 | --tw-scale-y: 1;
519 | --tw-pan-x: ;
520 | --tw-pan-y: ;
521 | --tw-pinch-zoom: ;
522 | --tw-scroll-snap-strictness: proximity;
523 | --tw-gradient-from-position: ;
524 | --tw-gradient-via-position: ;
525 | --tw-gradient-to-position: ;
526 | --tw-ordinal: ;
527 | --tw-slashed-zero: ;
528 | --tw-numeric-figure: ;
529 | --tw-numeric-spacing: ;
530 | --tw-numeric-fraction: ;
531 | --tw-ring-inset: ;
532 | --tw-ring-offset-width: 0px;
533 | --tw-ring-offset-color: #fff;
534 | --tw-ring-color: rgb(59 130 246 / 0.5);
535 | --tw-ring-offset-shadow: 0 0 #0000;
536 | --tw-ring-shadow: 0 0 #0000;
537 | --tw-shadow: 0 0 #0000;
538 | --tw-shadow-colored: 0 0 #0000;
539 | --tw-blur: ;
540 | --tw-brightness: ;
541 | --tw-contrast: ;
542 | --tw-grayscale: ;
543 | --tw-hue-rotate: ;
544 | --tw-invert: ;
545 | --tw-saturate: ;
546 | --tw-sepia: ;
547 | --tw-drop-shadow: ;
548 | --tw-backdrop-blur: ;
549 | --tw-backdrop-brightness: ;
550 | --tw-backdrop-contrast: ;
551 | --tw-backdrop-grayscale: ;
552 | --tw-backdrop-hue-rotate: ;
553 | --tw-backdrop-invert: ;
554 | --tw-backdrop-opacity: ;
555 | --tw-backdrop-saturate: ;
556 | --tw-backdrop-sepia: ;
557 | --tw-contain-size: ;
558 | --tw-contain-layout: ;
559 | --tw-contain-paint: ;
560 | --tw-contain-style: ;
561 | }
562 |
563 | ::backdrop {
564 | --tw-border-spacing-x: 0;
565 | --tw-border-spacing-y: 0;
566 | --tw-translate-x: 0;
567 | --tw-translate-y: 0;
568 | --tw-rotate: 0;
569 | --tw-skew-x: 0;
570 | --tw-skew-y: 0;
571 | --tw-scale-x: 1;
572 | --tw-scale-y: 1;
573 | --tw-pan-x: ;
574 | --tw-pan-y: ;
575 | --tw-pinch-zoom: ;
576 | --tw-scroll-snap-strictness: proximity;
577 | --tw-gradient-from-position: ;
578 | --tw-gradient-via-position: ;
579 | --tw-gradient-to-position: ;
580 | --tw-ordinal: ;
581 | --tw-slashed-zero: ;
582 | --tw-numeric-figure: ;
583 | --tw-numeric-spacing: ;
584 | --tw-numeric-fraction: ;
585 | --tw-ring-inset: ;
586 | --tw-ring-offset-width: 0px;
587 | --tw-ring-offset-color: #fff;
588 | --tw-ring-color: rgb(59 130 246 / 0.5);
589 | --tw-ring-offset-shadow: 0 0 #0000;
590 | --tw-ring-shadow: 0 0 #0000;
591 | --tw-shadow: 0 0 #0000;
592 | --tw-shadow-colored: 0 0 #0000;
593 | --tw-blur: ;
594 | --tw-brightness: ;
595 | --tw-contrast: ;
596 | --tw-grayscale: ;
597 | --tw-hue-rotate: ;
598 | --tw-invert: ;
599 | --tw-saturate: ;
600 | --tw-sepia: ;
601 | --tw-drop-shadow: ;
602 | --tw-backdrop-blur: ;
603 | --tw-backdrop-brightness: ;
604 | --tw-backdrop-contrast: ;
605 | --tw-backdrop-grayscale: ;
606 | --tw-backdrop-hue-rotate: ;
607 | --tw-backdrop-invert: ;
608 | --tw-backdrop-opacity: ;
609 | --tw-backdrop-saturate: ;
610 | --tw-backdrop-sepia: ;
611 | --tw-contain-size: ;
612 | --tw-contain-layout: ;
613 | --tw-contain-paint: ;
614 | --tw-contain-style: ;
615 | }
616 |
617 | .sr-only {
618 | position: absolute;
619 | width: 1px;
620 | height: 1px;
621 | padding: 0;
622 | margin: -1px;
623 | overflow: hidden;
624 | clip: rect(0, 0, 0, 0);
625 | white-space: nowrap;
626 | border-width: 0;
627 | }
628 |
629 | .pointer-events-none {
630 | pointer-events: none;
631 | }
632 |
633 | .static {
634 | position: static;
635 | }
636 |
637 | .fixed {
638 | position: fixed;
639 | }
640 |
641 | .relative {
642 | position: relative;
643 | }
644 |
645 | .sticky {
646 | position: -webkit-sticky;
647 | position: sticky;
648 | }
649 |
650 | .inset-x-0 {
651 | left: 0px;
652 | right: 0px;
653 | }
654 |
655 | .bottom-0 {
656 | bottom: 0px;
657 | }
658 |
659 | .top-0 {
660 | top: 0px;
661 | }
662 |
663 | .top-\[var\(--navbar-height\)\] {
664 | top: var(--navbar-height);
665 | }
666 |
667 | .z-30 {
668 | z-index: 30;
669 | }
670 |
671 | .z-40 {
672 | z-index: 40;
673 | }
674 |
675 | .box-border {
676 | box-sizing: border-box;
677 | }
678 |
679 | .flex {
680 | display: flex;
681 | }
682 |
683 | .hidden {
684 | display: none;
685 | }
686 |
687 | .h-\[calc\(100dvh_-_var\(--navbar-height\)_-_1px\)\] {
688 | height: calc(100dvh - var(--navbar-height) - 1px);
689 | }
690 |
691 | .h-\[var\(--navbar-height\)\] {
692 | height: var(--navbar-height);
693 | }
694 |
695 | .h-auto {
696 | height: auto;
697 | }
698 |
699 | .h-full {
700 | height: 100%;
701 | }
702 |
703 | .w-6 {
704 | width: 1.5rem;
705 | }
706 |
707 | .w-full {
708 | width: 100%;
709 | }
710 |
711 | .w-screen {
712 | width: 100vw;
713 | }
714 |
715 | .max-w-\[1024px\] {
716 | max-width: 1024px;
717 | }
718 |
719 | .max-w-\[1280px\] {
720 | max-width: 1280px;
721 | }
722 |
723 | .max-w-\[1536px\] {
724 | max-width: 1536px;
725 | }
726 |
727 | .max-w-\[640px\] {
728 | max-width: 640px;
729 | }
730 |
731 | .max-w-\[768px\] {
732 | max-width: 768px;
733 | }
734 |
735 | .max-w-full {
736 | max-width: 100%;
737 | }
738 |
739 | .flex-grow {
740 | flex-grow: 1;
741 | }
742 |
743 | .basis-0 {
744 | flex-basis: 0px;
745 | }
746 |
747 | .list-none {
748 | list-style-type: none;
749 | }
750 |
751 | .flex-row {
752 | flex-direction: row;
753 | }
754 |
755 | .flex-col {
756 | flex-direction: column;
757 | }
758 |
759 | .flex-nowrap {
760 | flex-wrap: nowrap;
761 | }
762 |
763 | .items-center {
764 | align-items: center;
765 | }
766 |
767 | .justify-start {
768 | justify-content: flex-start;
769 | }
770 |
771 | .justify-center {
772 | justify-content: center;
773 | }
774 |
775 | .justify-between {
776 | justify-content: space-between;
777 | }
778 |
779 | .gap-2 {
780 | gap: 0.5rem;
781 | }
782 |
783 | .gap-4 {
784 | gap: 1rem;
785 | }
786 |
787 | .overflow-y-auto {
788 | overflow-y: auto;
789 | }
790 |
791 | .whitespace-nowrap {
792 | white-space: nowrap;
793 | }
794 |
795 | .rounded-small {
796 | border-radius: var(--nextui-radius-small);
797 | }
798 |
799 | .border-b {
800 | border-bottom-width: 1px;
801 | }
802 |
803 | .border-divider {
804 | --tw-border-opacity: 1;
805 | border-color: hsl(var(--nextui-divider) / var(--nextui-divider-opacity, var(--tw-border-opacity)));
806 | }
807 |
808 | .bg-background {
809 | --tw-bg-opacity: 1;
810 | background-color: hsl(var(--nextui-background) / var(--nextui-background-opacity, var(--tw-bg-opacity)));
811 | }
812 |
813 | .bg-background\/70 {
814 | background-color: hsl(var(--nextui-background) / 0.7);
815 | }
816 |
817 | .bg-transparent {
818 | background-color: transparent;
819 | }
820 |
821 | .bg-stripe-gradient {
822 | background-image: linear-gradient(45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.1) 75%, transparent 75%, transparent);
823 | }
824 |
825 | .px-6 {
826 | padding-left: 1.5rem;
827 | padding-right: 1.5rem;
828 | }
829 |
830 | .pt-2 {
831 | padding-top: 0.5rem;
832 | }
833 |
834 | .text-large {
835 | font-size: var(--nextui-font-size-large);
836 | line-height: var(--nextui-line-height-large);
837 | }
838 |
839 | .text-medium {
840 | font-size: var(--nextui-font-size-medium);
841 | line-height: var(--nextui-line-height-medium);
842 | }
843 |
844 | .text-inherit {
845 | color: inherit;
846 | }
847 |
848 | .no-underline {
849 | text-decoration-line: none;
850 | }
851 |
852 | .shadow {
853 | --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
854 | --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);
855 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
856 | }
857 |
858 | .outline-none {
859 | outline: 2px solid transparent;
860 | outline-offset: 2px;
861 | }
862 |
863 | .backdrop-blur-lg {
864 | --tw-backdrop-blur: blur(16px);
865 | -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
866 | backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
867 | }
868 |
869 | .backdrop-blur-xl {
870 | --tw-backdrop-blur: blur(24px);
871 | -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
872 | backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
873 | }
874 |
875 | .backdrop-saturate-150 {
876 | --tw-backdrop-saturate: saturate(1.5);
877 | -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
878 | backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
879 | }
880 |
881 | .transition-opacity {
882 | transition-property: opacity;
883 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
884 | transition-duration: 150ms;
885 | }
886 |
887 | :root,.light,[data-theme="light"] {
888 | color-scheme: light;
889 | --nextui-background: 0 0% 100%;
890 | --nextui-foreground-50: 0 0% 98%;
891 | --nextui-foreground-100: 240 5% 96%;
892 | --nextui-foreground-200: 240 6% 90%;
893 | --nextui-foreground-300: 240 5% 84%;
894 | --nextui-foreground-400: 240 5% 65%;
895 | --nextui-foreground-500: 240 4% 46%;
896 | --nextui-foreground-600: 240 5% 34%;
897 | --nextui-foreground-700: 240 5% 26%;
898 | --nextui-foreground-800: 240 4% 16%;
899 | --nextui-foreground-900: 240 6% 10%;
900 | --nextui-foreground: 202 24% 9%;
901 | --nextui-divider: 0 0% 7%;
902 | --nextui-divider-opacity: 0.15;
903 | --nextui-focus: 212 100% 47%;
904 | --nextui-overlay: 0 0% 0%;
905 | --nextui-content1: 0 0% 100%;
906 | --nextui-content1-foreground: 202 24% 9%;
907 | --nextui-content2: 240 5% 96%;
908 | --nextui-content2-foreground: 240 4% 16%;
909 | --nextui-content3: 240 6% 90%;
910 | --nextui-content3-foreground: 240 5% 26%;
911 | --nextui-content4: 240 5% 84%;
912 | --nextui-content4-foreground: 240 5% 34%;
913 | --nextui-default-50: 0 0% 98%;
914 | --nextui-default-100: 240 5% 96%;
915 | --nextui-default-200: 240 6% 90%;
916 | --nextui-default-300: 240 5% 84%;
917 | --nextui-default-400: 240 5% 65%;
918 | --nextui-default-500: 240 4% 46%;
919 | --nextui-default-600: 240 5% 34%;
920 | --nextui-default-700: 240 5% 26%;
921 | --nextui-default-800: 240 4% 16%;
922 | --nextui-default-900: 240 6% 10%;
923 | --nextui-default-foreground: 0 0% 0%;
924 | --nextui-default: 240 5% 84%;
925 | --nextui-primary-50: 213 92% 95%;
926 | --nextui-primary-100: 212 92% 90%;
927 | --nextui-primary-200: 212 92% 79%;
928 | --nextui-primary-300: 212 92% 69%;
929 | --nextui-primary-400: 212 92% 58%;
930 | --nextui-primary-500: 212 100% 47%;
931 | --nextui-primary-600: 212 100% 38%;
932 | --nextui-primary-700: 212 100% 29%;
933 | --nextui-primary-800: 212 100% 19%;
934 | --nextui-primary-900: 212 100% 10%;
935 | --nextui-primary-foreground: 0 0% 100%;
936 | --nextui-primary: 212 100% 47%;
937 | --nextui-secondary-50: 270 62% 95%;
938 | --nextui-secondary-100: 270 59% 89%;
939 | --nextui-secondary-200: 270 59% 79%;
940 | --nextui-secondary-300: 270 59% 68%;
941 | --nextui-secondary-400: 270 59% 58%;
942 | --nextui-secondary-500: 270 67% 47%;
943 | --nextui-secondary-600: 270 67% 38%;
944 | --nextui-secondary-700: 270 67% 28%;
945 | --nextui-secondary-800: 270 67% 19%;
946 | --nextui-secondary-900: 270 67% 9%;
947 | --nextui-secondary-foreground: 0 0% 100%;
948 | --nextui-secondary: 270 67% 47%;
949 | --nextui-success-50: 147 64% 95%;
950 | --nextui-success-100: 146 61% 89%;
951 | --nextui-success-200: 146 62% 77%;
952 | --nextui-success-300: 146 63% 66%;
953 | --nextui-success-400: 146 62% 55%;
954 | --nextui-success-500: 146 79% 44%;
955 | --nextui-success-600: 146 80% 35%;
956 | --nextui-success-700: 146 79% 26%;
957 | --nextui-success-800: 146 80% 17%;
958 | --nextui-success-900: 146 78% 9%;
959 | --nextui-success-foreground: 0 0% 0%;
960 | --nextui-success: 146 79% 44%;
961 | --nextui-warning-50: 55 92% 95%;
962 | --nextui-warning-100: 37 91% 91%;
963 | --nextui-warning-200: 37 91% 82%;
964 | --nextui-warning-300: 37 91% 73%;
965 | --nextui-warning-400: 37 91% 64%;
966 | --nextui-warning-500: 37 91% 55%;
967 | --nextui-warning-600: 37 74% 44%;
968 | --nextui-warning-700: 37 74% 33%;
969 | --nextui-warning-800: 37 75% 22%;
970 | --nextui-warning-900: 37 75% 11%;
971 | --nextui-warning-foreground: 0 0% 0%;
972 | --nextui-warning: 37 91% 55%;
973 | --nextui-danger-50: 339 92% 95%;
974 | --nextui-danger-100: 340 92% 90%;
975 | --nextui-danger-200: 339 90% 80%;
976 | --nextui-danger-300: 339 91% 71%;
977 | --nextui-danger-400: 339 90% 61%;
978 | --nextui-danger-500: 339 90% 51%;
979 | --nextui-danger-600: 339 87% 41%;
980 | --nextui-danger-700: 339 86% 31%;
981 | --nextui-danger-800: 339 87% 20%;
982 | --nextui-danger-900: 340 85% 10%;
983 | --nextui-danger-foreground: 0 0% 100%;
984 | --nextui-danger: 339 90% 51%;
985 | --nextui-spacing-unit: 4px;
986 | --nextui-spacing-unit-0: 0px;
987 | --nextui-spacing-unit-1: 0.25rem;
988 | --nextui-spacing-unit-2: 0.5rem;
989 | --nextui-spacing-unit-3: 0.75rem;
990 | --nextui-spacing-unit-4: 1rem;
991 | --nextui-spacing-unit-5: 1.25rem;
992 | --nextui-spacing-unit-6: 1.5rem;
993 | --nextui-spacing-unit-7: 1.75rem;
994 | --nextui-spacing-unit-8: 2rem;
995 | --nextui-spacing-unit-9: 2.25rem;
996 | --nextui-spacing-unit-10: 2.5rem;
997 | --nextui-spacing-unit-11: 2.75rem;
998 | --nextui-spacing-unit-12: 3rem;
999 | --nextui-spacing-unit-13: 3.25rem;
1000 | --nextui-spacing-unit-14: 3.5rem;
1001 | --nextui-spacing-unit-15: 3.75rem;
1002 | --nextui-spacing-unit-16: 4rem;
1003 | --nextui-spacing-unit-17: 4.25rem;
1004 | --nextui-spacing-unit-18: 4.5rem;
1005 | --nextui-spacing-unit-20: 5rem;
1006 | --nextui-spacing-unit-24: 6rem;
1007 | --nextui-spacing-unit-28: 7rem;
1008 | --nextui-spacing-unit-32: 8rem;
1009 | --nextui-spacing-unit-36: 9rem;
1010 | --nextui-spacing-unit-40: 10rem;
1011 | --nextui-spacing-unit-44: 11rem;
1012 | --nextui-spacing-unit-48: 12rem;
1013 | --nextui-spacing-unit-52: 13rem;
1014 | --nextui-spacing-unit-56: 14rem;
1015 | --nextui-spacing-unit-60: 15rem;
1016 | --nextui-spacing-unit-64: 16rem;
1017 | --nextui-spacing-unit-72: 18rem;
1018 | --nextui-spacing-unit-80: 20rem;
1019 | --nextui-spacing-unit-96: 24rem;
1020 | --nextui-spacing-unit-xs: 0.5rem;
1021 | --nextui-spacing-unit-sm: 0.75rem;
1022 | --nextui-spacing-unit-md: 1rem;
1023 | --nextui-spacing-unit-lg: 1.375rem;
1024 | --nextui-spacing-unit-xl: 2.25rem;
1025 | --nextui-spacing-unit-2xl: 3rem;
1026 | --nextui-spacing-unit-3xl: 5rem;
1027 | --nextui-spacing-unit-4xl: 7.5rem;
1028 | --nextui-spacing-unit-5xl: 14rem;
1029 | --nextui-spacing-unit-6xl: 18rem;
1030 | --nextui-spacing-unit-7xl: 24rem;
1031 | --nextui-spacing-unit-8xl: 32rem;
1032 | --nextui-spacing-unit-9xl: 40rem;
1033 | --nextui-spacing-unit-3_5: 0.875rem;
1034 | --nextui-disabled-opacity: .5;
1035 | --nextui-divider-weight: 1px;
1036 | --nextui-font-size-tiny: 0.75rem;
1037 | --nextui-font-size-small: 0.875rem;
1038 | --nextui-font-size-medium: 1rem;
1039 | --nextui-font-size-large: 1.125rem;
1040 | --nextui-line-height-tiny: 1rem;
1041 | --nextui-line-height-small: 1.25rem;
1042 | --nextui-line-height-medium: 1.5rem;
1043 | --nextui-line-height-large: 1.75rem;
1044 | --nextui-radius-small: 8px;
1045 | --nextui-radius-medium: 12px;
1046 | --nextui-radius-large: 14px;
1047 | --nextui-border-width-small: 1px;
1048 | --nextui-border-width-medium: 2px;
1049 | --nextui-border-width-large: 3px;
1050 | --nextui-box-shadow-small: 0px 0px 5px 0px rgb(0 0 0 / 0.02), 0px 2px 10px 0px rgb(0 0 0 / 0.06), 0px 0px 1px 0px rgb(0 0 0 / 0.3);
1051 | --nextui-box-shadow-medium: 0px 0px 15px 0px rgb(0 0 0 / 0.03), 0px 2px 30px 0px rgb(0 0 0 / 0.08), 0px 0px 1px 0px rgb(0 0 0 / 0.3);
1052 | --nextui-box-shadow-large: 0px 0px 30px 0px rgb(0 0 0 / 0.04), 0px 30px 60px 0px rgb(0 0 0 / 0.12), 0px 0px 1px 0px rgb(0 0 0 / 0.3);
1053 | --nextui-hover-opacity: .8;
1054 | }
1055 |
1056 | .dark,[data-theme="dark"] {
1057 | color-scheme: dark;
1058 | --nextui-background: 0 0% 0%;
1059 | --nextui-foreground-50: 240 6% 10%;
1060 | --nextui-foreground-100: 240 4% 16%;
1061 | --nextui-foreground-200: 240 5% 26%;
1062 | --nextui-foreground-300: 240 5% 34%;
1063 | --nextui-foreground-400: 240 4% 46%;
1064 | --nextui-foreground-500: 240 5% 65%;
1065 | --nextui-foreground-600: 240 5% 84%;
1066 | --nextui-foreground-700: 240 6% 90%;
1067 | --nextui-foreground-800: 240 5% 96%;
1068 | --nextui-foreground-900: 0 0% 98%;
1069 | --nextui-foreground: 210 6% 93%;
1070 | --nextui-focus: 212 100% 47%;
1071 | --nextui-overlay: 0 0% 0%;
1072 | --nextui-divider: 0 0% 100%;
1073 | --nextui-divider-opacity: 0.15;
1074 | --nextui-content1: 240 6% 10%;
1075 | --nextui-content1-foreground: 0 0% 98%;
1076 | --nextui-content2: 240 4% 16%;
1077 | --nextui-content2-foreground: 240 5% 96%;
1078 | --nextui-content3: 240 5% 26%;
1079 | --nextui-content3-foreground: 240 6% 90%;
1080 | --nextui-content4: 240 5% 34%;
1081 | --nextui-content4-foreground: 240 5% 84%;
1082 | --nextui-default-50: 240 6% 10%;
1083 | --nextui-default-100: 240 4% 16%;
1084 | --nextui-default-200: 240 5% 26%;
1085 | --nextui-default-300: 240 5% 34%;
1086 | --nextui-default-400: 240 4% 46%;
1087 | --nextui-default-500: 240 5% 65%;
1088 | --nextui-default-600: 240 5% 84%;
1089 | --nextui-default-700: 240 6% 90%;
1090 | --nextui-default-800: 240 5% 96%;
1091 | --nextui-default-900: 0 0% 98%;
1092 | --nextui-default-foreground: 0 0% 100%;
1093 | --nextui-default: 240 5% 26%;
1094 | --nextui-primary-50: 212 100% 10%;
1095 | --nextui-primary-100: 212 100% 19%;
1096 | --nextui-primary-200: 212 100% 29%;
1097 | --nextui-primary-300: 212 100% 38%;
1098 | --nextui-primary-400: 212 100% 47%;
1099 | --nextui-primary-500: 212 92% 58%;
1100 | --nextui-primary-600: 212 92% 69%;
1101 | --nextui-primary-700: 212 92% 79%;
1102 | --nextui-primary-800: 212 92% 90%;
1103 | --nextui-primary-900: 213 92% 95%;
1104 | --nextui-primary-foreground: 0 0% 100%;
1105 | --nextui-primary: 212 100% 47%;
1106 | --nextui-secondary-50: 270 67% 9%;
1107 | --nextui-secondary-100: 270 67% 19%;
1108 | --nextui-secondary-200: 270 67% 28%;
1109 | --nextui-secondary-300: 270 67% 38%;
1110 | --nextui-secondary-400: 270 67% 47%;
1111 | --nextui-secondary-500: 270 59% 58%;
1112 | --nextui-secondary-600: 270 59% 68%;
1113 | --nextui-secondary-700: 270 59% 79%;
1114 | --nextui-secondary-800: 270 59% 89%;
1115 | --nextui-secondary-900: 270 62% 95%;
1116 | --nextui-secondary-foreground: 0 0% 100%;
1117 | --nextui-secondary: 270 59% 58%;
1118 | --nextui-success-50: 146 78% 9%;
1119 | --nextui-success-100: 146 80% 17%;
1120 | --nextui-success-200: 146 79% 26%;
1121 | --nextui-success-300: 146 80% 35%;
1122 | --nextui-success-400: 146 79% 44%;
1123 | --nextui-success-500: 146 62% 55%;
1124 | --nextui-success-600: 146 63% 66%;
1125 | --nextui-success-700: 146 62% 77%;
1126 | --nextui-success-800: 146 61% 89%;
1127 | --nextui-success-900: 147 64% 95%;
1128 | --nextui-success-foreground: 0 0% 0%;
1129 | --nextui-success: 146 79% 44%;
1130 | --nextui-warning-50: 37 75% 11%;
1131 | --nextui-warning-100: 37 75% 22%;
1132 | --nextui-warning-200: 37 74% 33%;
1133 | --nextui-warning-300: 37 74% 44%;
1134 | --nextui-warning-400: 37 91% 55%;
1135 | --nextui-warning-500: 37 91% 64%;
1136 | --nextui-warning-600: 37 91% 73%;
1137 | --nextui-warning-700: 37 91% 82%;
1138 | --nextui-warning-800: 37 91% 91%;
1139 | --nextui-warning-900: 55 92% 95%;
1140 | --nextui-warning-foreground: 0 0% 0%;
1141 | --nextui-warning: 37 91% 55%;
1142 | --nextui-danger-50: 340 85% 10%;
1143 | --nextui-danger-100: 339 87% 20%;
1144 | --nextui-danger-200: 339 86% 31%;
1145 | --nextui-danger-300: 339 87% 41%;
1146 | --nextui-danger-400: 339 90% 51%;
1147 | --nextui-danger-500: 339 90% 61%;
1148 | --nextui-danger-600: 339 91% 71%;
1149 | --nextui-danger-700: 339 90% 80%;
1150 | --nextui-danger-800: 340 92% 90%;
1151 | --nextui-danger-900: 339 92% 95%;
1152 | --nextui-danger-foreground: 0 0% 100%;
1153 | --nextui-danger: 339 90% 51%;
1154 | --nextui-spacing-unit: 4px;
1155 | --nextui-spacing-unit-0: 0px;
1156 | --nextui-spacing-unit-1: 0.25rem;
1157 | --nextui-spacing-unit-2: 0.5rem;
1158 | --nextui-spacing-unit-3: 0.75rem;
1159 | --nextui-spacing-unit-4: 1rem;
1160 | --nextui-spacing-unit-5: 1.25rem;
1161 | --nextui-spacing-unit-6: 1.5rem;
1162 | --nextui-spacing-unit-7: 1.75rem;
1163 | --nextui-spacing-unit-8: 2rem;
1164 | --nextui-spacing-unit-9: 2.25rem;
1165 | --nextui-spacing-unit-10: 2.5rem;
1166 | --nextui-spacing-unit-11: 2.75rem;
1167 | --nextui-spacing-unit-12: 3rem;
1168 | --nextui-spacing-unit-13: 3.25rem;
1169 | --nextui-spacing-unit-14: 3.5rem;
1170 | --nextui-spacing-unit-15: 3.75rem;
1171 | --nextui-spacing-unit-16: 4rem;
1172 | --nextui-spacing-unit-17: 4.25rem;
1173 | --nextui-spacing-unit-18: 4.5rem;
1174 | --nextui-spacing-unit-20: 5rem;
1175 | --nextui-spacing-unit-24: 6rem;
1176 | --nextui-spacing-unit-28: 7rem;
1177 | --nextui-spacing-unit-32: 8rem;
1178 | --nextui-spacing-unit-36: 9rem;
1179 | --nextui-spacing-unit-40: 10rem;
1180 | --nextui-spacing-unit-44: 11rem;
1181 | --nextui-spacing-unit-48: 12rem;
1182 | --nextui-spacing-unit-52: 13rem;
1183 | --nextui-spacing-unit-56: 14rem;
1184 | --nextui-spacing-unit-60: 15rem;
1185 | --nextui-spacing-unit-64: 16rem;
1186 | --nextui-spacing-unit-72: 18rem;
1187 | --nextui-spacing-unit-80: 20rem;
1188 | --nextui-spacing-unit-96: 24rem;
1189 | --nextui-spacing-unit-xs: 0.5rem;
1190 | --nextui-spacing-unit-sm: 0.75rem;
1191 | --nextui-spacing-unit-md: 1rem;
1192 | --nextui-spacing-unit-lg: 1.375rem;
1193 | --nextui-spacing-unit-xl: 2.25rem;
1194 | --nextui-spacing-unit-2xl: 3rem;
1195 | --nextui-spacing-unit-3xl: 5rem;
1196 | --nextui-spacing-unit-4xl: 7.5rem;
1197 | --nextui-spacing-unit-5xl: 14rem;
1198 | --nextui-spacing-unit-6xl: 18rem;
1199 | --nextui-spacing-unit-7xl: 24rem;
1200 | --nextui-spacing-unit-8xl: 32rem;
1201 | --nextui-spacing-unit-9xl: 40rem;
1202 | --nextui-spacing-unit-3_5: 0.875rem;
1203 | --nextui-disabled-opacity: .5;
1204 | --nextui-divider-weight: 1px;
1205 | --nextui-font-size-tiny: 0.75rem;
1206 | --nextui-font-size-small: 0.875rem;
1207 | --nextui-font-size-medium: 1rem;
1208 | --nextui-font-size-large: 1.125rem;
1209 | --nextui-line-height-tiny: 1rem;
1210 | --nextui-line-height-small: 1.25rem;
1211 | --nextui-line-height-medium: 1.5rem;
1212 | --nextui-line-height-large: 1.75rem;
1213 | --nextui-radius-small: 8px;
1214 | --nextui-radius-medium: 12px;
1215 | --nextui-radius-large: 14px;
1216 | --nextui-border-width-small: 1px;
1217 | --nextui-border-width-medium: 2px;
1218 | --nextui-border-width-large: 3px;
1219 | --nextui-box-shadow-small: 0px 0px 5px 0px rgb(0 0 0 / 0.05), 0px 2px 10px 0px rgb(0 0 0 / 0.2), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15);
1220 | --nextui-box-shadow-medium: 0px 0px 15px 0px rgb(0 0 0 / 0.06), 0px 2px 30px 0px rgb(0 0 0 / 0.22), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15);
1221 | --nextui-box-shadow-large: 0px 0px 30px 0px rgb(0 0 0 / 0.07), 0px 30px 60px 0px rgb(0 0 0 / 0.26), inset 0px 0px 1px 0px rgb(255 255 255 / 0.15);
1222 | --nextui-hover-opacity: .9;
1223 | }
1224 |
1225 | .tap-highlight-transparent {
1226 | -webkit-tap-highlight-color: transparent;
1227 | }
1228 |
1229 | .transition-opacity {
1230 | transition-property: opacity;
1231 | transition-timing-function: ease;
1232 | transition-duration: 250ms;
1233 | }
1234 |
1235 | .before\:block::before {
1236 | content: var(--tw-content);
1237 | display: block;
1238 | }
1239 |
1240 | .before\:h-px::before {
1241 | content: var(--tw-content);
1242 | height: 1px;
1243 | }
1244 |
1245 | .before\:w-6::before {
1246 | content: var(--tw-content);
1247 | width: 1.5rem;
1248 | }
1249 |
1250 | .before\:-translate-y-1::before {
1251 | content: var(--tw-content);
1252 | --tw-translate-y: -0.25rem;
1253 | -webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1254 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1255 | }
1256 |
1257 | .before\:rotate-0::before {
1258 | content: var(--tw-content);
1259 | --tw-rotate: 0deg;
1260 | -webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1261 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1262 | }
1263 |
1264 | .before\:bg-current::before {
1265 | content: var(--tw-content);
1266 | background-color: currentColor;
1267 | }
1268 |
1269 | .before\:transition-transform::before {
1270 | content: var(--tw-content);
1271 | transition-property: -webkit-transform;
1272 | transition-property: transform;
1273 | transition-property: transform, -webkit-transform;
1274 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1275 | transition-duration: 150ms;
1276 | }
1277 |
1278 | .before\:duration-150::before {
1279 | content: var(--tw-content);
1280 | transition-duration: 150ms;
1281 | }
1282 |
1283 | .before\:content-\[\'\'\]::before {
1284 | --tw-content: '';
1285 | content: var(--tw-content);
1286 | }
1287 |
1288 | .before\:transition-transform::before {
1289 | content: var(--tw-content);
1290 | transition-property: -webkit-transform;
1291 | transition-property: transform;
1292 | transition-property: transform, -webkit-transform;
1293 | transition-timing-function: ease;
1294 | transition-duration: 250ms;
1295 | }
1296 |
1297 | .after\:block::after {
1298 | content: var(--tw-content);
1299 | display: block;
1300 | }
1301 |
1302 | .after\:h-px::after {
1303 | content: var(--tw-content);
1304 | height: 1px;
1305 | }
1306 |
1307 | .after\:w-6::after {
1308 | content: var(--tw-content);
1309 | width: 1.5rem;
1310 | }
1311 |
1312 | .after\:translate-y-1::after {
1313 | content: var(--tw-content);
1314 | --tw-translate-y: 0.25rem;
1315 | -webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1316 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1317 | }
1318 |
1319 | .after\:rotate-0::after {
1320 | content: var(--tw-content);
1321 | --tw-rotate: 0deg;
1322 | -webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1323 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1324 | }
1325 |
1326 | .after\:bg-current::after {
1327 | content: var(--tw-content);
1328 | background-color: currentColor;
1329 | }
1330 |
1331 | .after\:transition-transform::after {
1332 | content: var(--tw-content);
1333 | transition-property: -webkit-transform;
1334 | transition-property: transform;
1335 | transition-property: transform, -webkit-transform;
1336 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1337 | transition-duration: 150ms;
1338 | }
1339 |
1340 | .after\:duration-150::after {
1341 | content: var(--tw-content);
1342 | transition-duration: 150ms;
1343 | }
1344 |
1345 | .after\:content-\[\'\'\]::after {
1346 | --tw-content: '';
1347 | content: var(--tw-content);
1348 | }
1349 |
1350 | .after\:transition-transform::after {
1351 | content: var(--tw-content);
1352 | transition-property: -webkit-transform;
1353 | transition-property: transform;
1354 | transition-property: transform, -webkit-transform;
1355 | transition-timing-function: ease;
1356 | transition-duration: 250ms;
1357 | }
1358 |
1359 | .data-\[focus-visible\=true\]\:z-10[data-focus-visible=true] {
1360 | z-index: 10;
1361 | }
1362 |
1363 | .data-\[open\=true\]\:flex[data-open=true] {
1364 | display: flex;
1365 | }
1366 |
1367 | .data-\[justify\=end\]\:flex-grow[data-justify=end] {
1368 | flex-grow: 1;
1369 | }
1370 |
1371 | .data-\[justify\=start\]\:flex-grow[data-justify=start] {
1372 | flex-grow: 1;
1373 | }
1374 |
1375 | .data-\[justify\=end\]\:basis-0[data-justify=end] {
1376 | flex-basis: 0px;
1377 | }
1378 |
1379 | .data-\[justify\=start\]\:basis-0[data-justify=start] {
1380 | flex-basis: 0px;
1381 | }
1382 |
1383 | .data-\[justify\=start\]\:justify-start[data-justify=start] {
1384 | justify-content: flex-start;
1385 | }
1386 |
1387 | .data-\[justify\=end\]\:justify-end[data-justify=end] {
1388 | justify-content: flex-end;
1389 | }
1390 |
1391 | .data-\[justify\=center\]\:justify-center[data-justify=center] {
1392 | justify-content: center;
1393 | }
1394 |
1395 | .data-\[menu-open\=true\]\:border-none[data-menu-open=true] {
1396 | border-style: none;
1397 | }
1398 |
1399 | .data-\[active\=true\]\:font-semibold[data-active=true] {
1400 | font-weight: 600;
1401 | }
1402 |
1403 | .data-\[focus-visible\=true\]\:outline-2[data-focus-visible=true] {
1404 | outline-width: 2px;
1405 | }
1406 |
1407 | .data-\[focus-visible\=true\]\:outline-offset-2[data-focus-visible=true] {
1408 | outline-offset: 2px;
1409 | }
1410 |
1411 | .data-\[focus-visible\=true\]\:outline-focus[data-focus-visible=true] {
1412 | outline-color: hsl(var(--nextui-focus) / var(--nextui-focus-opacity, 1));
1413 | }
1414 |
1415 | .data-\[menu-open\=true\]\:backdrop-blur-xl[data-menu-open=true] {
1416 | --tw-backdrop-blur: blur(24px);
1417 | -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1418 | backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
1419 | }
1420 |
1421 | .group[data-pressed=true] .group-data-\[pressed\=true\]\:opacity-70 {
1422 | opacity: 0.7;
1423 | }
1424 |
1425 | .group[data-open=true] .group-data-\[open\=true\]\:before\:translate-y-px::before {
1426 | content: var(--tw-content);
1427 | --tw-translate-y: 1px;
1428 | -webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1429 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1430 | }
1431 |
1432 | .group[data-open=true] .group-data-\[open\=true\]\:before\:rotate-45::before {
1433 | content: var(--tw-content);
1434 | --tw-rotate: 45deg;
1435 | -webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1436 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1437 | }
1438 |
1439 | .group[data-open=true] .group-data-\[open\=true\]\:after\:translate-y-0::after {
1440 | content: var(--tw-content);
1441 | --tw-translate-y: 0px;
1442 | -webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1443 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1444 | }
1445 |
1446 | .group[data-open=true] .group-data-\[open\=true\]\:after\:-rotate-45::after {
1447 | content: var(--tw-content);
1448 | --tw-rotate: -45deg;
1449 | -webkit-transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1450 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1451 | }
--------------------------------------------------------------------------------