├── .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 |
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 |
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 |
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 |
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
🚀 Loading...
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 | 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
🚀 Loading...
55 | 56 | if (page_id != '' || isLoading !== true) { 57 | return
58 |
59 |
63 |
; 64 | } else { 65 | return
66 |
67 |
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 |
    60 | {post.title} 61 |
    62 | 63 | 64 | 65 |
  • 66 | ) 67 | } 68 | 69 | return ( 70 |
    71 |
      {postList}
    72 |
    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 | 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 | ![](https://media.heptabase.com/v1/images/3120a828-7e72-4637-aaff-ff8b5d72a2b3/9fbf682c-71a2-4dd6-b915-056bc841b1e0/image.png) 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 | ![](https://media.heptabase.com/v1/images/3120a828-7e72-4637-aaff-ff8b5d72a2b3/3a644a97-7a6a-4ef3-8ca5-a7a2b72c7d41/image.png) 42 | 43 | 3. 根据你的喜好在上述白板中创建一些卡片,例如 About 介绍自己、Projects 介绍自己参与的项目等等 44 | 45 | ### GitHub 46 | 47 | 1. Fork [项目](https://github.com/draJiang/Heptabase-Blog) 48 | 49 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-01-2920.55.33@2x20240129205602.png) 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 | ![](https://media.heptabase.com/v1/images/3120a828-7e72-4637-aaff-ff8b5d72a2b3/ca5fc266-33e4-45f4-a504-d5addfeacae2/CleanShot2023-02-2323.34.27-2x.png) 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 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-05-3119.25.4920240531192617.png) 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 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-01-2921.02.23@2x20240129210251.png) 95 | 96 | 3. 自定义域名 97 | 98 | 在 Vercel 中打开项目,在 Settings 中设置自己的域名 99 | 100 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-01-3002.29.41@2x20240130023011.png) 101 | 102 | ## 更新内容 103 | 104 | Heptabase 的白板更新后不会自动同步到网站中,需要手动进行更新,手动更新方法: 105 | 106 | 1. 在你的 GitHub 仓库中打开 Actions 107 | 108 | 2. 点击 Run workflow 109 | 110 | 3. 几分钟后网站的内容就会与 Heptabase 同步 111 | 112 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-05-3119.37.21@2x20240531193753.png) 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 | Share your knowledge - based on your Heptabase data | Product Hunt 136 | ``` 137 | 138 | Bandcamp 139 | 140 | ```html 141 | {HTML} 142 | 143 | ``` 144 | 145 | 上述样式在编辑器中是这个样子: 146 | 147 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-02-2205.09.49@2x20240222051016.png) 148 | 149 | ## 拉取远程更新 150 | 151 | 当此项目功能更新后你可以通过以下方式同步更新你的网站。 152 | 153 | 首先需要在终端中安装 git 以及安装 [VScode](https://code.visualstudio.com/download) 客户端。 154 | 155 | ### 将自己的项目克隆到本地 156 | 157 | 首先获取你自己的项目 URL 158 | 159 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-03-1000.14.04@2x20240310001520.png) 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 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-03-1000.21.21@2x20240310002214.png) 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 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-03-1000.35.50@2x20240310003617.png) 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 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-03-1000.41.16@2x20240310004135.png) 210 | 211 | 在左侧的文件列表中,你可能会看到一些红色的文件名称,意味着这些文件中存在冲突,你需要点击这些文件逐个解决冲突。 212 | 213 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-03-1000.42.24@2x20240310004251.png) 214 | 215 | 通常 `config.js` 、LOGO 等你需要自行自定义的内容,你可以选择「Accept Current Change」,其他则可以选择「Accept Incoming Change」 216 | 217 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/W5WefL20240310004635.png) 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 | ![](https://jiangzilong-image.oss-cn-beijing.aliyuncs.com/uPic/CleanShot2024-03-1000.55.42@2x20240310005608.png) 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 | ![](https://media.heptabase.com/v1/images/3120a828-7e72-4637-aaff-ff8b5d72a2b3/7fa14e75-12eb-4ff4-97bc-1b5f0582d7ae/image.png) 266 | 267 | 另一方面,笔记、文章数据在不同平台有多份副本,后续修改起来就要穿梭在不同平台中进行更新,维护成本高。 268 | 269 | Heptabase 本身也支持公开笔记,但移动端的支持不太好,于是决定自己开发了一个。 270 | 271 | ### 博客 vs 数字花园 272 | 273 | 数字花园的理念与我正在使用的卡片笔记法、Heptabase 的设计哲学更加贴近,所以放弃了持续 1 年的博客,改用数字花园的方式维护自己的个人站点,下面会详细介绍一下原因。 274 | 275 | ## 对数字花园的理解 276 | 277 | 聊一下我对数字花园理解,以及如何将这些理解体现到网站的设计上。 278 | 279 | ### 知识的持续性 280 | 281 | 数字花园的内容是持续迭代的,我可以发布尚不成熟的想法(不一定要等到输出完整的文章)并且可以在发布后持续的修订。所以在笔记列表中,不是强调笔记的创建时间而是展示最近编辑时间,并将最近编辑的笔记展示在最前,方便阅读者理解笔记的活跃状态。 282 | 283 | ![](https://media.heptabase.com/v1/images/3120a828-7e72-4637-aaff-ff8b5d72a2b3/3a43cd07-74f1-4223-a696-2ac8cf82c590/image.png) 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 | ![](https://media.heptabase.com/v1/images/3120a828-7e72-4637-aaff-ff8b5d72a2b3/25b1dc56-b936-4afd-a3c6-eec9c58a47c9/image.png) 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 =
    223 |
    🔗LINKS TO THIS PAGE
    224 |
      225 | 👻 226 |
    227 |
    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 =
    243 |
    🔗LINKS TO THIS PAGE
    244 |
      245 | {backLinks} 246 |
    247 |
    248 | } 249 | 250 | 251 | 252 | return
    253 | 254 |
    255 |
    256 |
    257 | 258 | 259 |
    260 | 261 |
    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 | {/*
    ) 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 |
    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 | // 找到 ![]( 符号 109 | // 找到上述符号之后的第 1 个 jpg#/png#/gif# 符号 110 | // 找到上一个步骤后的第 1 个 ) 符号 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 = '![' + img_alt + '](' + img_src + ')' 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 | } --------------------------------------------------------------------------------