├── public ├── bg.png └── favicon.svg ├── src ├── types │ ├── next-handler.d.ts │ ├── env.d.ts │ ├── system.d.ts │ ├── services.d.ts │ ├── hitokoto.d.ts │ └── onedrive.d.ts ├── hooks │ ├── use-icon.ts │ ├── use-isomorphic-layout-effect.ts │ ├── use-edit.ts │ ├── use-hitokoto.ts │ ├── use-onedrive-data.ts │ ├── use-media-query.ts │ ├── use-services.ts │ └── use-onedrive.ts ├── components │ ├── link │ │ └── index.tsx │ ├── data-center │ │ ├── data-view.tsx │ │ ├── date-tag.tsx │ │ └── index.tsx │ ├── hitokoto │ │ └── index.tsx │ ├── theme-toggle │ │ └── index.tsx │ ├── header │ │ ├── index.tsx │ │ └── options.tsx │ ├── footer │ │ ├── index.tsx │ │ └── system-info.tsx │ ├── settings-option │ │ ├── index.tsx │ │ └── sync-data.tsx │ └── service-card │ │ ├── index.tsx │ │ └── edit-card.tsx ├── lib │ ├── constant.ts │ ├── utils.ts │ ├── onedrive-auth.ts │ ├── fetcher.ts │ └── services.ts ├── pages │ ├── api │ │ ├── services │ │ │ ├── index.ts │ │ │ └── [action].ts │ │ ├── env │ │ │ └── index.ts │ │ ├── system │ │ │ └── [type].ts │ │ └── onedrive │ │ │ └── index.ts │ ├── settings.tsx │ ├── index.tsx │ ├── _document.tsx │ └── _app.tsx └── styles │ └── app.css ├── .github ├── image │ ├── edit.png │ ├── normal.png │ ├── edit-dark.png │ └── normal-dark.png └── workflows │ └── docker.yml ├── eslint.config.js ├── .dockerignore ├── next.config.js ├── next-env.d.ts ├── .env.development ├── .env.example ├── docker-compose.yaml ├── .gitignore ├── tsconfig.json ├── unocss.config.ts ├── services.json ├── README.md ├── Dockerfile └── package.json /public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kahosan/home-page/HEAD/public/bg.png -------------------------------------------------------------------------------- /src/types/next-handler.d.ts: -------------------------------------------------------------------------------- 1 | export type Action = 'add' | 'delete' | 'edit' | 'update'; 2 | -------------------------------------------------------------------------------- /.github/image/edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kahosan/home-page/HEAD/.github/image/edit.png -------------------------------------------------------------------------------- /.github/image/normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kahosan/home-page/HEAD/.github/image/normal.png -------------------------------------------------------------------------------- /.github/image/edit-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kahosan/home-page/HEAD/.github/image/edit-dark.png -------------------------------------------------------------------------------- /.github/image/normal-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kahosan/home-page/HEAD/.github/image/normal-dark.png -------------------------------------------------------------------------------- /src/hooks/use-icon.ts: -------------------------------------------------------------------------------- 1 | export function useIcon(icon: string) { 2 | return icon.replace('carbon:', '').trim(); 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { kaho } = require('eslint-config-kaho'); 4 | 5 | module.exports = kaho(); 6 | -------------------------------------------------------------------------------- /src/types/env.d.ts: -------------------------------------------------------------------------------- 1 | export interface Env { 2 | title?: string 3 | headerTitle?: string 4 | blog?: string 5 | twitter?: string 6 | } 7 | -------------------------------------------------------------------------------- /src/types/system.d.ts: -------------------------------------------------------------------------------- 1 | export interface CpuInfoResponse { 2 | usage: string 3 | } 4 | 5 | export interface MemoryInfoResponse { 6 | free: number 7 | } 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | .git 8 | .vscode 9 | dist 10 | 11 | .env.production 12 | .env.development 13 | .env.example 14 | -------------------------------------------------------------------------------- /src/types/services.d.ts: -------------------------------------------------------------------------------- 1 | export interface Service { 2 | name: string 3 | path: string 4 | description: string 5 | icon: string 6 | } 7 | 8 | export interface ActionsResponse { 9 | msg: string 10 | } 11 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @type {import('next').NextConfig} 5 | */ 6 | const nextConfig = { 7 | reactStrictMode: true, 8 | output: 'standalone' 9 | }; 10 | 11 | module.exports = nextConfig; 12 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /src/hooks/use-isomorphic-layout-effect.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-restricted-imports -- ignore 2 | import { useEffect, useLayoutEffect } from 'react'; 3 | import { isBrowser } from 'src/lib/utils'; 4 | 5 | export const useIsomorphicLayoutEffect = isBrowser ? useLayoutEffect : useEffect; 6 | -------------------------------------------------------------------------------- /src/hooks/use-edit.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from 'jotai'; 2 | 3 | export const isEditAtom = atom(false); 4 | 5 | export function useEdit() { 6 | const [isEdit, setIsEdit] = useAtom(isEditAtom); 7 | const toggleEditMode = () => setIsEdit(!isEdit); 8 | 9 | return { 10 | isEdit, 11 | toggleEditMode 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/link/index.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | href?: string 3 | children?: React.ReactNode 4 | } 5 | 6 | export default function Link({ href, children, ...props }: Props & JSX.IntrinsicElements['a']) { 7 | return ( 8 | {children} 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | API_URL=http://localhost:3000 2 | FILE_PATH= 3 | NEXT_PUBLIC_HOME_TWITTER= 4 | NEXT_PUBLIC_HOME_BLOG= 5 | 6 | NEXT_PUBLIC_HOME_HEADER_TITLE=Data Center 7 | NEXT_PUBLIC_HOME_TITLE=NAS 数据中心 8 | 9 | # 用于验证 OneDrive API 可以自建 10 | NEXT_PUBLIC_ONEDRIVE_CLIENT_ID=23020e85-55d0-49bc-bb27-9620d91892ba 11 | NEXT_PUBLIC_ONEDRIVE_CLIENT_SECRET=eFg8Q~fynP-XPN1nyui2JgzJbA7g4TJm9NnurbnH -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_URL= 2 | 3 | # 默认留空即可,如有需要按绝对路径填写 4 | FILE_PATH= 5 | 6 | NEXT_PUBLIC_HOME_TWITTER= 7 | NEXT_PUBLIC_HOME_BLOG= 8 | 9 | NEXT_PUBLIC_HOME_HEADER_TITLE=Data Center 10 | NEXT_PUBLIC_HOME_TITLE=NAS 数据中心 11 | 12 | # 用于验证 OneDrive API 可以自建 13 | NEXT_PUBLIC_ONEDRIVE_CLIENT_ID=23020e85-55d0-49bc-bb27-9620d91892ba 14 | NEXT_PUBLIC_ONEDRIVE_CLIENT_SECRET=eFg8Q~fynP-XPN1nyui2JgzJbA7g4TJm9NnurbnH -------------------------------------------------------------------------------- /src/types/hitokoto.d.ts: -------------------------------------------------------------------------------- 1 | export interface HitokotoRespones { 2 | id: number 3 | uuid: string 4 | hitokoto: string 5 | type: string 6 | from: string 7 | from_who: any 8 | creator: string 9 | creator_uid: number 10 | reviewer: number 11 | commit_from: string 12 | created_at: string 13 | length: number 14 | } 15 | 16 | export interface RequestError { 17 | message: string 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/constant.ts: -------------------------------------------------------------------------------- 1 | export const TWITTER = process.env.NEXT_PUBLIC_HOME_TWITTER; 2 | export const BLOG = process.env.NEXT_PUBLIC_HOME_BLOG; 3 | export const HEADER_TITLE = process.env.NEXT_PUBLIC_HOME_HEADER_TITLE; 4 | export const TITLE = process.env.NEXT_PUBLIC_HOME_TITLE; 5 | export const CLIENT_ID = process.env.NEXT_PUBLIC_ONEDRIVE_CLIENT_ID ?? ''; 6 | export const CLIENT_SECRET = process.env.NEXT_PUBLIC_ONEDRIVE_CLIENT_SECRET ?? ''; 7 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Service } from 'src/types/services'; 2 | 3 | export const isBrowser = typeof window !== 'undefined'; 4 | 5 | export function validateFormDataForService(service: Service | undefined) { 6 | if (!service) 7 | return '请填写所需数据'; 8 | 9 | for (const [k, v] of Object.entries(service)) { 10 | if (!v) 11 | return `${k} 没有填写`; 12 | } 13 | } 14 | export const generatorRespError = (msg: string) => ({ msg }); 15 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | home-page: 3 | container_name: home-page 4 | image: kahosan/home-page 5 | user: 1000:1000 6 | environment: 7 | - TZ=Asia/Shanghai 8 | - NEXT_PUBLIC_HOME_TWITTER=https://twitter.com/kaho_suyf 9 | - NEXT_PUBLIC_HOME_BLOG=https://blog.kahosan.top/ 10 | - NEXT_PUBLIC_HOME_HEADER_TITLE=Data Center 11 | - NEXT_PUBLIC_HOME_TITLE=NAS 数据中心 12 | volumes: 13 | - ./services.json:/app/services.json 14 | ports: 15 | - 3000:3000 16 | -------------------------------------------------------------------------------- /src/hooks/use-hitokoto.ts: -------------------------------------------------------------------------------- 1 | import useSWR from 'swr'; 2 | import { fetcher } from 'src/lib/fetcher'; 3 | 4 | interface Hitokoto { 5 | hitokoto: string 6 | from: string 7 | } 8 | 9 | /* 10 | * 数据来源于 Hitokoto,感谢免费分享 11 | * {@link https://hitokoto.cn/} 12 | */ 13 | 14 | export function useHitokoto() { 15 | return useSWR( 16 | 'https://v1.hitokoto.cn/?c=a', 17 | fetcher, 18 | { 19 | revalidateOnFocus: false, 20 | onError(e) { 21 | console.error(e.message); 22 | } 23 | } 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/components/data-center/data-view.tsx: -------------------------------------------------------------------------------- 1 | import { Loading } from '@geist-ui/core'; 2 | 3 | import ServiceCard from '../service-card'; 4 | 5 | import type { Service } from 'src/types/services'; 6 | 7 | export default function DataView({ servicesData }: { servicesData: Service[] | undefined }) { 8 | if (!servicesData || servicesData.length === 0) 9 | return ; 10 | 11 | return ( 12 |
13 | {servicesData.map(service => )} 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/api/services/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiHandler } from 'next'; 2 | import { generatorRespError } from 'src/lib/utils'; 3 | import { getServicesData } from 'src/lib/services'; 4 | 5 | const handler: NextApiHandler = async (req, res) => { 6 | if (req.method !== 'GET') 7 | res.status(405).json(generatorRespError(`请求方法 ${req.method ?? ''} 不支持`)); 8 | 9 | try { 10 | res.status(200).json(await getServicesData()); 11 | } catch (e) { 12 | if (e instanceof Error) 13 | res.status(500).json(generatorRespError(e.message)); 14 | } 15 | }; 16 | 17 | export default handler; 18 | -------------------------------------------------------------------------------- /src/components/data-center/date-tag.tsx: -------------------------------------------------------------------------------- 1 | import getYear from 'date-fns/getYear'; 2 | import getMonth from 'date-fns/getMonth'; 3 | import getDate from 'date-fns/getDate'; 4 | import getDay from 'date-fns/getDay'; 5 | 6 | export default function DateTag() { 7 | const day = { 8 | 0: '星期日', 9 | 1: '星期一', 10 | 2: '星期二', 11 | 3: '星期三', 12 | 4: '星期四', 13 | 5: '星期五', 14 | 6: '星期六' 15 | } as const; 16 | const date = new Date(); 17 | 18 | const dateText = `${getYear(date)} 年 ${getMonth(date) + 1} 月 ${getDate(date)} 日 ${day[getDay(date)]}`; 19 | 20 | return ( 21 |

{dateText}

22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/hitokoto/index.tsx: -------------------------------------------------------------------------------- 1 | import { Loading } from '@geist-ui/core'; 2 | import { useHitokoto } from 'src/hooks/use-hitokoto'; 3 | 4 | export default function Hitokoto() { 5 | const { data, error } = useHitokoto(); 6 | 7 | if (error) 8 | return

一言加载失败

; 9 | 10 | return ( 11 | data?.hitokoto 12 | ? ( 13 | <> 14 | {data.hitokoto} 15 |

16 | 来源: {data.from} 17 |

18 | 19 | ) 20 | : ( 21 |
22 | 一言加载中 23 | 24 |
25 | ) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/pages/api/env/index.ts: -------------------------------------------------------------------------------- 1 | import { generatorRespError } from 'src/lib/utils'; 2 | import type { NextApiHandler } from 'next'; 3 | 4 | const handler: NextApiHandler = (req, res) => { 5 | if (req.method !== 'GET') 6 | res.status(405).json(generatorRespError(`请求方法 ${req.method ?? ''} 不支持`)); 7 | 8 | const { NEXT_PUBLIC_HOME_TITLE, NEXT_PUBLIC_HOME_HEADER_TITLE, NEXT_PUBLIC_HOME_BLOG, NEXT_PUBLIC_HOME_TWITTER } = process.env; 9 | 10 | res.status(200).json({ 11 | title: NEXT_PUBLIC_HOME_TITLE, 12 | headerTitle: NEXT_PUBLIC_HOME_HEADER_TITLE, 13 | blog: NEXT_PUBLIC_HOME_BLOG, 14 | twitter: NEXT_PUBLIC_HOME_TWITTER 15 | }); 16 | }; 17 | 18 | export default handler; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | dist 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env.production 31 | .env 32 | .env.local 33 | !.env*.example 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | 41 | # tailwind 42 | .turbo 43 | .vscode 44 | styles/dist.css 45 | 46 | # services 47 | dev_services.json -------------------------------------------------------------------------------- /src/hooks/use-onedrive-data.ts: -------------------------------------------------------------------------------- 1 | import { useAtom } from 'jotai'; 2 | import { atomWithStorage } from 'jotai/utils'; 3 | 4 | export interface AccessTokenWrapper { 5 | expires: number 6 | token: string 7 | } 8 | 9 | interface OnedriveData { 10 | authCode: string 11 | refreshToken: string 12 | accessToken: AccessTokenWrapper 13 | } 14 | 15 | const onedriveAtom = atomWithStorage('onedrive-data', { 16 | authCode: '', 17 | refreshToken: '', 18 | accessToken: { 19 | expires: 0, 20 | token: '' 21 | } 22 | }); 23 | 24 | export const useOnedriveData = () => useAtom(onedriveAtom); 25 | 26 | export function calcAccessTokenExpires(expires: number) { 27 | return Date.now() + expires * 1000; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/theme-toggle/index.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { useAtom } from 'jotai'; 4 | import { themeAtom } from 'src/pages/_app'; 5 | 6 | export default function ThemeToggle() { 7 | const [themeType, setTheme] = useAtom(themeAtom); 8 | 9 | const toggle = useCallback(() => { 10 | const theme = themeType === 'dark' ? 'light' : 'dark'; 11 | 12 | if (theme === 'dark') { 13 | setTheme(theme); 14 | document.documentElement.classList.add('dark'); 15 | } else { 16 | document.documentElement.classList.remove('dark'); 17 | setTheme(theme); 18 | } 19 | }, [themeType, setTheme]); 20 | 21 | return ( 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/styles/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | html { 8 | background-color: #f1f1f1; 9 | color: #1d1d1d; 10 | overflow: auto; 11 | } 12 | 13 | html.dark { 14 | background-color: #0c0c0c; 15 | color: #e2e2e2; 16 | color-scheme: dark; 17 | } 18 | 19 | body { 20 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 21 | } 22 | 23 | a { 24 | color: inherit; 25 | text-decoration: none; 26 | -webkit-tap-highlight-color: transparent; 27 | } 28 | 29 | .custom-bg { 30 | width: 100%; 31 | height: 10rem; 32 | bottom: 50px; 33 | right: 0; 34 | background: url(/bg.png) no-repeat bottom right; 35 | background-size: contain; 36 | margin-bottom: 0.875rem; 37 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es5", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "jsx": "preserve", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/ui/*": ["ui/*"], 12 | "@/lib/*": ["lib/*"], 13 | "@/styles/*": ["styles/*"] 14 | }, 15 | "resolveJsonModule": true, 16 | "allowJs": true, 17 | "noEmit": true, 18 | "isolatedModules": true, 19 | "esModuleInterop": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "strict": true, 22 | "skipLibCheck": true, 23 | "plugins": [ 24 | { 25 | "name": "next" 26 | } 27 | ] 28 | }, 29 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 30 | "exclude": ["node_modules"] 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/api/system/[type].ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @eslint-react/naming-convention/filename -- ignore 2 | import * as si from 'systeminformation'; 3 | import type { NextApiHandler } from 'next'; 4 | 5 | import { generatorRespError } from 'src/lib/utils'; 6 | 7 | const handler: NextApiHandler = async (req, res) => { 8 | if (req.method !== 'GET') { 9 | res.status(405).json(generatorRespError(`请求方法 ${req.method ?? ''} 不支持`)); 10 | return; 11 | } 12 | 13 | switch (req.query.type) { 14 | case 'cpuinfo': 15 | res.status(200).json({ usage: `${(await si.currentLoad()).currentLoad.toFixed(2)}%` }); 16 | break; 17 | case 'meminfo': 18 | res.status(200).json({ free: `${((await si.mem()).free / (1024 ** 3)).toFixed(2)} GB` }); 19 | break; 20 | default: 21 | res.status(400).json(generatorRespError('未知的请求类型')); 22 | } 23 | }; 24 | 25 | export default handler; 26 | -------------------------------------------------------------------------------- /src/pages/api/onedrive/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiHandler } from 'next'; 2 | 3 | import { generatorRespError } from 'src/lib/utils'; 4 | import { getAuthTokenWithCode, getAuthTokenWithRefreshToken } from 'src/lib/onedrive-auth'; 5 | 6 | const handler: NextApiHandler = async (req, res) => { 7 | if (req.method !== 'POST') { 8 | res.status(405).json(generatorRespError(`请求方法 ${req.method ?? ''} 不支持`)); 9 | return; 10 | } 11 | 12 | const { code, refresh_token } = req.query as Record; 13 | if (refresh_token) { // if refresh_token is provided, use it to get new access_token 14 | try { 15 | const data = await getAuthTokenWithRefreshToken(refresh_token); 16 | res.status(200).json(data); 17 | } catch (e) { 18 | res.status(500).json(e); 19 | } 20 | } else if (code) { 21 | try { 22 | const data = await getAuthTokenWithCode(code); 23 | res.status(200).json(data); 24 | } catch (e) { 25 | res.status(500).json(e); 26 | } 27 | } else { 28 | res.status(400).json(generatorRespError('code 或 refresh_token 不能为空')); 29 | } 30 | }; 31 | 32 | export default handler; 33 | -------------------------------------------------------------------------------- /src/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from '@geist-ui/core'; 2 | 3 | import NextLink from 'next/link'; 4 | 5 | import useSWR from 'swr'; 6 | import { fetcher } from 'src/lib/fetcher'; 7 | 8 | import ThemeToggle from '../theme-toggle'; 9 | import Options from './options'; 10 | 11 | import type { Env } from 'src/types/env'; 12 | 13 | export default function Header() { 14 | // docker 动态加载 env 15 | const { data } = useSWR('/api/env', fetcher); 16 | return ( 17 |
18 |
19 |

{data?.headerTitle}

20 |
21 | 22 | 23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/components/data-center/index.tsx: -------------------------------------------------------------------------------- 1 | import Footer from '../footer'; 2 | import Hitokoto from '../hitokoto'; 3 | 4 | import DateTag from './date-tag'; 5 | import DataView from './data-view'; 6 | 7 | import { useServices } from 'src/hooks/use-services'; 8 | 9 | import useSWR from 'swr'; 10 | import { fetcher } from 'src/lib/fetcher'; 11 | 12 | import type { Env } from 'src/types/env'; 13 | 14 | export default function DataCenter() { 15 | const { servicesData } = useServices(); 16 | // docker 动态加载 env 17 | const { data } = useSWR('/api/env', fetcher); 18 | return ( 19 |
20 |
21 |
22 |

{data?.title}

23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/settings.tsx: -------------------------------------------------------------------------------- 1 | import { Divider } from '@geist-ui/core'; 2 | 3 | import Head from 'next/head'; 4 | import NextLink from 'next/link'; 5 | import SettingsOption from 'src/components/settings-option'; 6 | 7 | import ThemeToggle from 'src/components/theme-toggle'; 8 | 9 | export type SettingItems = '同步数据' | '基本设置'; 10 | 11 | export default function Settings() { 12 | return ( 13 | <> 14 |
15 | 16 | 17 | BQ Settings 18 | 19 |
20 |

设置

21 |
22 | 23 | 24 |
25 | 26 |
27 |
28 |
29 |
30 | 31 |
32 | 33 | 34 |
35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | push_to_registry: 13 | name: Push Docker image to Docker Hub 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: get_image_name 17 | run: | 18 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 19 | # Use Docker `latest` tag convention 20 | [ "$VERSION" == "master" ] && VERSION=latest 21 | repository=${{ github.repository }} 22 | IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/home-page:$VERSION 23 | echo IMAGE_NAME=$IMAGE_NAME 24 | echo "IMAGE_NAME=$IMAGE_NAME" >> $GITHUB_ENV 25 | - name: Check out the repo 26 | uses: actions/checkout@v2 27 | - name: Log in to Docker Hub 28 | uses: docker/login-action@v2 29 | with: 30 | username: ${{ secrets.DOCKER_USERNAME }} 31 | password: ${{ secrets.DOCKER_TOKEN }} 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v2 34 | - name: Push to Docker Hub 35 | uses: docker/build-push-action@v4 36 | with: 37 | platforms: linux/amd64,linux/arm64 38 | push: true 39 | tags: ${{ env.IMAGE_NAME }} -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | import { SWRConfig } from 'swr'; 4 | 5 | import Header from '../components/header'; 6 | import DataCenter from '../components/data-center'; 7 | 8 | import { BLOG, HEADER_TITLE, TITLE, TWITTER } from 'src/lib/constant'; 9 | import type { Service } from 'src/types/services'; 10 | import type { Env } from 'src/types/env'; 11 | 12 | export default function HomePage({ fallback }: Record) { 13 | return ( 14 | 15 | 16 | 17 | BQ Center 18 | 19 |
20 | 21 | 22 | ); 23 | } 24 | 25 | export async function getServerSideProps() { 26 | let servicesData: Service[] = []; 27 | const envData: Env = { 28 | title: TITLE, 29 | headerTitle: HEADER_TITLE, 30 | blog: BLOG, 31 | twitter: TWITTER 32 | }; 33 | 34 | if (process.env.API_URL !== '' && process.env.API_URL !== undefined) { 35 | const res = await fetch(`${process.env.API_URL}/api/services`); 36 | servicesData = await res.json(); 37 | } 38 | 39 | return { 40 | props: { 41 | fallback: { 42 | '/api/services': servicesData, 43 | '/api/env': envData 44 | } 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import { fetcher } from 'src/lib/fetcher'; 2 | import useSWR from 'swr'; 3 | 4 | import getYear from 'date-fns/getYear'; 5 | 6 | import Link from '../link'; 7 | import SystemInfo from './system-info'; 8 | 9 | import type { Env } from 'src/types/env'; 10 | 11 | export default function Footer() { 12 | const { data } = useSWR('/api/env', fetcher); 13 | return ( 14 |
15 |
16 |
17 |
18 | 19 |
20 |

21 | Blog 22 | Twitter 23 |

24 |
25 |
26 |
27 |

Home-Page强力驱动

28 | © {getYear(new Date())} 29 | PowerBy  30 | Next.js & React 31 |
32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @eslint-react/naming-convention/filename -- ignore 2 | import type { DocumentContext, DocumentInitialProps } from 'next/document'; 3 | import Document, { Head, Html, Main, NextScript } from 'next/document'; 4 | 5 | function MyDocument() { 6 | return ( 7 | 8 | 9 | 10 |