BLOG-58 refactor: remove nextjs code
Some checks failed
PR Title Check / pr-title-check (pull_request) Successful in 13s
Frontend CI / build (push) Failing after 44s

This commit is contained in:
SquidSpirit 2025-07-23 05:19:36 +08:00
parent b0215d167d
commit eb4b2f4d6a
64 changed files with 1745 additions and 6779 deletions

View File

@ -1,23 +0,0 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@ -1,16 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/app.css"
}

View File

@ -1,26 +0,0 @@
FROM node:22-alpine AS base
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack enable pnpm
FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN apk add --no-cache libc6-compat && \
pnpm install --frozen-lockfile
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm run build
FROM base AS runner
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
COPY --from=builder /app/build ./build
RUN pnpm install --prod --frozen-lockfile
EXPOSE 3000
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
CMD ["node", "build"]

View File

@ -1,46 +0,0 @@
{
"name": "squidspirit-blog",
"private": true,
"version": "0.1.1",
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@fortawesome/fontawesome-free": "^7.0.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-node": "^5.2.13",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^7.0.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
},
"packageManager": "pnpm@10.12.4"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +0,0 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

149
frontend/.gitignore vendored
View File

@ -1,136 +1,23 @@
# Logs node_modules
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html) # Output
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json .output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# Runtime data # OS
pids .DS_Store
*.pid Thumbs.db
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover # Env
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env .env
.env.development.local .env.*
.env.test.local !.env.example
.env.production.local !.env.test
.env.local
# parcel-bundler cache (https://parceljs.org/) # Vite
.cache vite.config.js.timestamp-*
.parcel-cache vite.config.ts.timestamp-*
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# vitepress build output
**/.vitepress/dist
# vitepress cache directory
**/.vitepress/cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@ -1,11 +1,16 @@
{ {
"plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], "useTabs": true,
"printWidth": 120, "singleQuote": true,
"tabWidth": 2, "trailingComma": "none",
"trailingComma": "all", "printWidth": 100,
"singleQuote": false, "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"semi": true, "overrides": [
"importOrder": ["^[\\w-]+(/.*)?$", "^@[\\w-]+(/.*)?$", "^@/(app|lib)(/.*)?$", "^\\..*$"], {
"importOrderSeparation": true, "files": "*.svelte",
"importOrderSortSpecifiers": true "options": {
"parser": "svelte"
}
}
],
"tailwindStylesheet": "./src/app.css"
} }

View File

@ -1,27 +1,26 @@
FROM node:20-alpine AS base FROM node:22-alpine AS base
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0
RUN corepack enable pnpm
FROM base AS deps FROM base AS deps
WORKDIR /app WORKDIR /app
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml ./
RUN apk add --no-cache libc6-compat && \ RUN apk add --no-cache libc6-compat && \
corepack enable pnpm && \
pnpm install --frozen-lockfile pnpm install --frozen-lockfile
FROM base AS builder FROM base AS builder
WORKDIR /app WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
RUN corepack enable pnpm && \ RUN pnpm run build
pnpm run build
FROM base AS runner FROM base AS runner
WORKDIR /app WORKDIR /app
ENV NODE_ENV=production COPY package.json pnpm-lock.yaml ./
COPY --from=builder /app/public ./public COPY --from=builder /app/build ./build
COPY --from=builder /app/.next/standalone ./ RUN pnpm install --prod --frozen-lockfile
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000 EXPOSE 3000
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0 ENV HOSTNAME=0.0.0.0
ENV PORT=3000 ENV PORT=3000
CMD ["node", "server.js"] CMD ["node", "build"]

View File

@ -1,36 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

View File

@ -1,16 +0,0 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -1,13 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Avoid from rendering twice in development mode
reactStrictMode: false,
output: "standalone",
env: {
APP_VERSION: process.env.npm_package_version,
},
};
export default nextConfig;

View File

@ -1,39 +1,46 @@
{ {
"name": "squidspirit-blog", "name": "squidspirit-blog",
"version": "0.1.1",
"private": true, "private": true,
"license": "MIT", "version": "0.1.1",
"type": "module",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "vite dev",
"build": "next build", "build": "vite build",
"start": "next start", "preview": "vite preview",
"lint": "next lint" "prepare": "svelte-kit sync || echo ''",
}, "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"packageManager": "pnpm@10.0.0", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"dependencies": { "format": "prettier --write .",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "lint": "prettier --check . && eslint ."
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@reduxjs/toolkit": "^2.5.0",
"next": "15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-redux": "^9.2.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.2.0", "@eslint/compat": "^1.2.5",
"@trivago/prettier-plugin-sort-imports": "^5.2.1", "@eslint/js": "^9.18.0",
"@types/node": "^20.17.14", "@fortawesome/fontawesome-free": "^7.0.0",
"@types/react": "^19.0.7", "@sveltejs/adapter-auto": "^6.0.0",
"@types/react-dom": "^19.0.3", "@sveltejs/adapter-node": "^5.2.13",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"eslint": "^9.18.0", "eslint": "^9.18.0",
"eslint-config-next": "15.1.4", "eslint-config-prettier": "^10.0.1",
"postcss": "^8.5.1", "eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.10", "prettier-plugin-svelte": "^3.3.3",
"tailwindcss": "^3.4.17", "prettier-plugin-tailwindcss": "^0.6.11",
"typescript": "^5.7.3" "svelte": "^5.0.0",
} "svelte-check": "^4.0.0",
"tailwindcss": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^7.0.4"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild"
]
},
"packageManager": "pnpm@10.12.4"
} }

4453
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

View File

@ -1,107 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="logo-dark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
showguides="true"
inkscape:zoom="1.6365655"
inkscape:cx="235.55428"
inkscape:cy="251.4412"
inkscape:window-width="2560"
inkscape:window-height="1408"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g6" />
<defs
id="defs1" />
<g
inkscape:label="圖層 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g7"
transform="translate(0,6.9758125)">
<g
id="g5"
transform="matrix(0.41693719,0,0,0.41693719,150.81231,275.51745)">
<path
id="rect3"
style="fill:#5599ff;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 92.692319,228.77148 c -6.62659,0.0347 -13.23998,2.60866 -18.29101,7.7129 l -106.36915,107.49023 c -4.37464,4.42071 -6.82971,9.99467 -7.38868,15.71289 -0.0568,0.58091 -0.0743,1.16324 -0.0918,1.74609 -0.009,0.2751 -0.0347,0.549 -0.0352,0.82422 5.1e-4,0.27458 0.0259,0.54783 0.0352,0.82227 0.0175,0.58285 0.035,1.16518 0.0918,1.74609 0.55897,5.71823 3.01404,11.29218 7.38867,15.7129 L 74.401299,488.0293 c 10.10208,10.20847 26.453641,10.29348 36.662111,0.19141 10.20848,-10.10208 10.29348,-26.45169 0.19141,-36.66016 L 22.883719,362.25781 111.25481,272.95508 c 10.10208,-10.20848 10.01708,-26.56004 -0.19139,-36.66211 -5.10424,-5.05104 -11.744511,-7.5562 -18.371101,-7.52149 z" />
<rect
style="fill:#80b3ff;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
id="rect5"
width="51.848202"
height="283.7283"
x="345.60474"
y="-379.78085"
ry="25.924101"
transform="matrix(0.92365402,0.38322741,0.38197175,-0.924174,0,0)" />
<path
id="path5"
style="fill:#5599ff;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 411.88104,228.77148 c 6.6266,0.0347 13.24,2.60866 18.291,7.7129 l 106.3691,107.49023 c 4.3746,4.42071 6.8297,9.99467 7.3887,15.71289 0.057,0.58091 0.074,1.16324 0.092,1.74609 0.01,0.2751 0.035,0.549 0.035,0.82422 -5e-4,0.27458 -0.026,0.54783 -0.035,0.82227 -0.018,0.58285 -0.035,1.16518 -0.092,1.74609 -0.559,5.71823 -3.0141,11.29218 -7.3887,15.7129 L 430.17204,488.0293 c -10.1021,10.20847 -26.45364,10.29348 -36.66211,0.19141 -10.20848,-10.10208 -10.29348,-26.45169 -0.19141,-36.66016 l 88.37112,-89.30274 -88.37111,-89.30273 c -10.10208,-10.20848 -10.01708,-26.56004 0.19139,-36.66211 5.10424,-5.05104 11.74451,-7.5562 18.37112,-7.52149 z" />
</g>
<g
id="g6"
transform="translate(6.697046)">
<path
sodipodi:type="star"
style="fill:#80b3ff;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
id="path6"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="184.49283"
sodipodi:cy="58.761181"
sodipodi:r1="127.72986"
sodipodi:r2="63.864929"
sodipodi:arg1="0.52359878"
sodipodi:arg2="1.5707963"
inkscape:rounded="0.14"
inkscape:randomized="0"
d="m 295.11013,122.62611 c -15.48642,26.82327 -205.748182,26.82327 -221.234604,0 C 58.389104,95.802839 153.51998,-68.968678 184.49283,-68.968678 c 30.97284,10e-7 126.10372,164.771518 110.6173,191.594788 z"
inkscape:transform-center-y="-13.622002"
transform="matrix(-0.33629476,-0.57358618,0.58247964,-0.33116014,275.59758,225.7177)"
inkscape:transform-center-x="1.522155" />
<path
sodipodi:type="star"
style="fill:#5599ff;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
id="path1"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="184.49283"
sodipodi:cy="58.761181"
sodipodi:r1="127.72986"
sodipodi:r2="63.864929"
sodipodi:arg1="0.52359878"
sodipodi:arg2="1.5707963"
inkscape:rounded="0.16"
inkscape:randomized="0"
d="m 295.11013,122.62611 c -17.69877,30.65517 -203.535836,30.65517 -221.234604,0 C 56.176758,91.970943 149.09529,-68.968678 184.49283,-68.968678 c 35.39754,10e-7 128.31607,160.939623 110.6173,191.594788 z"
inkscape:transform-center-y="-30.185815"
transform="matrix(0.82249869,0,0,1.2327295,97.557844,171.10648)" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,107 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="logo-light.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
showguides="true"
inkscape:zoom="1.6365655"
inkscape:cx="235.55428"
inkscape:cy="251.4412"
inkscape:window-width="2560"
inkscape:window-height="1408"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g7" />
<defs
id="defs1" />
<g
inkscape:label="圖層 1"
inkscape:groupmode="layer"
id="layer1">
<g
id="g7"
transform="translate(0,6.9758125)">
<g
id="g5"
transform="matrix(0.41693719,0,0,0.41693719,150.81231,275.51745)">
<path
id="rect3"
style="fill:#2a7fff;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 92.692319,228.77148 c -6.62659,0.0347 -13.23998,2.60866 -18.29101,7.7129 l -106.36915,107.49023 c -4.37464,4.42071 -6.82971,9.99467 -7.38868,15.71289 -0.0568,0.58091 -0.0743,1.16324 -0.0918,1.74609 -0.009,0.2751 -0.0347,0.549 -0.0352,0.82422 5.1e-4,0.27458 0.0259,0.54783 0.0352,0.82227 0.0175,0.58285 0.035,1.16518 0.0918,1.74609 0.55897,5.71823 3.01404,11.29218 7.38867,15.7129 L 74.401299,488.0293 c 10.10208,10.20847 26.453641,10.29348 36.662111,0.19141 10.20848,-10.10208 10.29348,-26.45169 0.19141,-36.66016 L 22.883719,362.25781 111.25481,272.95508 c 10.10208,-10.20848 10.01708,-26.56004 -0.19139,-36.66211 -5.10424,-5.05104 -11.744511,-7.5562 -18.371101,-7.52149 z" />
<rect
style="fill:#0044aa;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
id="rect5"
width="51.848202"
height="283.7283"
x="345.60474"
y="-379.78085"
ry="25.924101"
transform="matrix(0.92365402,0.38322741,0.38197175,-0.924174,0,0)" />
<path
id="path5"
style="fill:#2a7fff;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 411.88104,228.77148 c 6.6266,0.0347 13.24,2.60866 18.291,7.7129 l 106.3691,107.49023 c 4.3746,4.42071 6.8297,9.99467 7.3887,15.71289 0.057,0.58091 0.074,1.16324 0.092,1.74609 0.01,0.2751 0.035,0.549 0.035,0.82422 -5e-4,0.27458 -0.026,0.54783 -0.035,0.82227 -0.018,0.58285 -0.035,1.16518 -0.092,1.74609 -0.559,5.71823 -3.0141,11.29218 -7.3887,15.7129 L 430.17204,488.0293 c -10.1021,10.20847 -26.45364,10.29348 -36.66211,0.19141 -10.20848,-10.10208 -10.29348,-26.45169 -0.19141,-36.66016 l 88.37112,-89.30274 -88.37111,-89.30273 c -10.10208,-10.20848 -10.01708,-26.56004 0.19139,-36.66211 5.10424,-5.05104 11.74451,-7.5562 18.37112,-7.52149 z" />
</g>
<g
id="g6"
transform="translate(6.697046)">
<path
sodipodi:type="star"
style="fill:#0044aa;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
id="path6"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="184.49283"
sodipodi:cy="58.761181"
sodipodi:r1="127.72986"
sodipodi:r2="63.864929"
sodipodi:arg1="0.52359878"
sodipodi:arg2="1.5707963"
inkscape:rounded="0.14"
inkscape:randomized="0"
d="m 295.11013,122.62611 c -15.48642,26.82327 -205.748182,26.82327 -221.234604,0 C 58.389104,95.802839 153.51998,-68.968678 184.49283,-68.968678 c 30.97284,10e-7 126.10372,164.771518 110.6173,191.594788 z"
inkscape:transform-center-y="-13.622002"
transform="matrix(-0.33629476,-0.57358618,0.58247964,-0.33116014,275.59758,225.7177)"
inkscape:transform-center-x="1.522155" />
<path
sodipodi:type="star"
style="fill:#2a7fff;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
id="path1"
inkscape:flatsided="true"
sodipodi:sides="3"
sodipodi:cx="184.49283"
sodipodi:cy="58.761181"
sodipodi:r1="127.72986"
sodipodi:r2="63.864929"
sodipodi:arg1="0.52359878"
sodipodi:arg2="1.5707963"
inkscape:rounded="0.16"
inkscape:randomized="0"
d="m 295.11013,122.62611 c -17.69877,30.65517 -203.535836,30.65517 -221.234604,0 C 56.176758,91.970943 149.09529,-68.968678 184.49283,-68.968678 c 35.39754,10e-7 128.31607,160.939623 110.6173,191.594788 z"
inkscape:transform-center-y="-30.185815"
transform="matrix(0.82249869,0,0,1.2327295,97.557844,171.10648)" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,2 +0,0 @@
User-agent: *
Allow: /

View File

@ -1,10 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-white font-sans text-base font-normal text-gray-600;
--tool-bar-height: 4rem;
--content-height: calc(100vh - var(--tool-bar-height));
}

View File

@ -1,63 +0,0 @@
import type { Metadata } from "next";
import { Noto_Sans_Mono, Noto_Sans_TC } from "next/font/google";
import localFont from "next/font/local";
import Footer from "@/lib/common/framework/ui/Footer";
import Navbar from "@/lib/common/framework/ui/Navbar";
import "./globals.css";
const notoSansTc = Noto_Sans_TC({
variable: "--font-noto-sans-tc",
subsets: ["latin"],
});
const notoSansMono = Noto_Sans_Mono({
variable: "--font-noto-sans-mono",
subsets: ["latin"],
});
const hackNerdMono = localFont({
src: "./_font/HackNerdMono.woff2",
variable: "--font-hack-nerd-mono",
});
export const metadata: Metadata = {
title: "魚之魷魂 SquidSpirit",
description: "程式、科技、教學、分享",
icons: {
icon: [
{
media: "(prefers-color-scheme: light)",
url: "/icon/logo-light.svg",
href: "/icon/logo-light.svg",
},
{
media: "(prefers-color-scheme: dark)",
url: "/icon/logo-dark.svg",
href: "/icon/logo-dark.svg",
},
],
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="zh-Hant">
<head>
<meta name="app-version" content={process.env.APP_VERSION} />
</head>
<body className={`${notoSansTc.variable} ${notoSansMono.variable} ${hackNerdMono.variable} antialiased`}>
<div className="min-h-screen">
<Navbar />
{children}
</div>
<Footer />
</body>
</html>
);
}

View File

@ -1,16 +0,0 @@
export default function NotFoundPage() {
return (
<div className="mx-auto flex min-h-[--content-height] max-w-screen-xl flex-col items-center justify-center px-4 md:px-6">
<div className="flex flex-row items-end gap-x-4 md:gap-x-6">
<h1 className="text-5xl font-extrabold text-gray-800 underline md:text-7xl">404</h1>
<h2 className="flex flex-row items-center gap-x-2 text-2xl font-bold md:gap-x-2.5 md:text-3xl">
<div className="h-7 w-1.5 bg-gray-800 md:h-9 md:w-2" />
<div className="rounded-md bg-blue-600 px-1 py-px md:px-1.5 md:py-0.5">
<span className="text-white">Not</span>
</div>
<span className="text-gray-800">Found.</span>
</h2>
</div>
</div>
);
}

View File

@ -1,13 +0,0 @@
import AboutMe from "@/lib/home/framework/ui/AboutMe";
import FirstView from "@/lib/home/framework/ui/FirstView";
import Motto from "@/lib/home/framework/ui/Motto";
export default function HomePage() {
return (
<div>
<FirstView />
<AboutMe />
<Motto />
</div>
);
}

View File

@ -1,35 +0,0 @@
import Link from "next/link";
import { faGitAlt, faYoutube } from "@fortawesome/free-brands-svg-icons";
import { faEnvelope } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default function Footer() {
return (
<div className="border-t border-gray-300">
<div className="mx-auto flex max-w-screen-xl flex-col items-center justify-center gap-4 px-4 py-12 md:flex-row md:px-6">
<div className="flex flex-row items-center justify-center gap-x-4">
<Link href="https://www.youtube.com/@squidspirit16" target="_blank">
<FontAwesomeIcon className="size-4" icon={faYoutube} title="YouTube Channel" />
</Link>
<Link href="mailto:squid@squidspirit.com">
<FontAwesomeIcon className="size-4" icon={faEnvelope} title="Email" />
</Link>
<Link href="https://git.squidspirit.com/squid/blog" target="_blank">
<FontAwesomeIcon className="size-4" icon={faGitAlt} title="Git Repository" />
</Link>
</div>
<Devider className="max-md:hidden" />
<span className="text-sm">Copyright © 2025 SquidSpirit</span>
</div>
</div>
);
}
function Devider(props: { className?: string }) {
return (
<div className={props.className}>
<div className="h-4 w-0.5 bg-gray-300" />
</div>
);
}

View File

@ -1,28 +0,0 @@
import Image from "next/image";
import Link from "next/link";
export default function Navbar() {
return (
<div className="border-b border-gray-300">
<div className="mx-auto flex h-[--tool-bar-height] max-w-screen-xl flex-row items-center justify-between px-4 md:px-6">
<Link className="flex flex-row items-center gap-x-2" href="/">
<Image className="mt-1 size-10" src="/icon/logo-light.svg" alt="SquidSpirit" height={40} width={40} />
<span className="text-2xl font-black text-gray-800"></span>
</Link>
<div className="flex flex-row items-center gap-x-6">
<Action label="首頁" link="/" isSelected={true} />
</div>
</div>
</div>
);
}
function Action(props: { label: string; link: string; isSelected: boolean }) {
return (
<div className={`rounded px-1.5 ${props.isSelected ? "bg-blue-600" : "bg-transparent"}`}>
<Link className={`font-extrabold ${props.isSelected ? "text-white" : "text-gray-800"}`} href={props.link}>
{props.label}
</Link>
</div>
);
}

View File

@ -1,9 +0,0 @@
import Terminal from "@/lib/home/framework/ui/Terminal";
export default function AboutMe() {
return (
<div className="mx-auto flex max-w-screen-xl flex-col items-center justify-center gap-y-2.5 px-4 py-32 md:gap-y-8 md:px-24 md:py-32">
<Terminal />
</div>
);
}

View File

@ -1,41 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
export default function AnimatedMark(props: { text: string; direction: "left" | "right" }) {
const [isVisible, setIsVisible] = useState(false);
const element = useRef<HTMLSpanElement | null>(null);
const origin = props.direction === "left" ? "origin-left" : "origin-right";
useEffect(() => {
if (!element.current) {
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
});
},
{ threshold: 1 },
);
observer.observe(element.current);
return () => observer.disconnect();
}, []);
return (
<span
ref={element}
className={`rounded-md bg-blue-600 px-1 py-0.5 text-white transition-transform delay-500 duration-1000 md:rounded-lg md:px-2.5 md:py-2 ${origin} ${isVisible ? "scale-x-100" : "scale-x-0"} `}
>
<span className="scale-x-100">{props.text}</span>
</span>
);
}

View File

@ -1,17 +0,0 @@
import SelfTags from "@/lib/home/framework/ui/SelfTags";
export default function FirstView() {
return (
<div className="mx-auto flex min-h-[--content-height] max-w-screen-xl flex-col justify-center gap-y-2.5 px-4 md:gap-y-8 md:px-6">
<h2 className="text-3xl font-bold text-gray-800 md:text-6xl">Hello </h2>
<h1 className="flex flex-row items-center gap-x-2 text-4xl font-extrabold text-gray-800 md:text-7xl">
<span></span>
<div className="rounded-lg bg-blue-600 px-1.5 py-1">
<span className="text-white">Squid</span>
</div>
<span></span>
</h1>
<SelfTags />
</div>
);
}

View File

@ -1,18 +0,0 @@
import AnimatedMark from "@/lib/home/framework/ui/AnimatedMark";
export default function Motto() {
return (
<div className="mx-auto flex h-screen max-w-screen-xl flex-col items-center justify-center gap-y-2.5 px-4 md:gap-y-8 md:px-6">
<div className="flex w-[19rem] flex-col gap-y-3 text-3xl font-bold text-gray-800 md:w-[38rem] md:gap-y-4 md:text-6xl">
<div className="flex w-full flex-row items-center justify-start gap-x-2.5">
<span>Keep</span>
<AnimatedMark text="Learning" direction="left" />
</div>
<div className="flex w-full flex-row items-center justify-end gap-x-2.5 md:gap-x-4">
<AnimatedMark text="Keep" direction="right" />
<span>Progressing</span>
</div>
</div>
</div>
);
}

View File

@ -1,66 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { Provider } from "react-redux";
import { useTagDispatch, useTagSelector } from "@/lib/home/presenter/tagHook";
import { tagShuffledAction } from "@/lib/home/presenter/tagReducer";
import tagStore from "@/lib/home/presenter/tagStore";
export default function SelfTags() {
return (
<Provider store={tagStore}>
<SelfTagsProvided />
</Provider>
);
}
function SelfTagsProvided() {
const tags = useTagSelector((state) => state.tags);
const dispatch = useTagDispatch();
// Initialize with placeholder to prevent flickering
const [tagsToShow, setTagsToShow] = useState<string[]>([""]);
const [isTagsVisible, setIsTagsVisible] = useState(false);
const timer = useRef<NodeJS.Timeout | undefined>(undefined);
// On mount
useEffect(() => {
timer.current = setInterval(() => {
dispatch(tagShuffledAction());
}, 4000);
return () => {
clearInterval(timer.current);
timer.current = undefined;
};
}, [dispatch]);
// On tags changed
useEffect(() => {
setIsTagsVisible(false);
setTimeout(() => {
setTagsToShow(tags);
setIsTagsVisible(true);
}, 500);
}, [tags]);
return (
<div
className={`relative w-full max-w-screen-md transition-opacity duration-500 ${isTagsVisible ? "opacity-100" : "opacity-0"}`}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-transparent via-60% to-white" />
<div className="flex flex-row items-center gap-x-2 overflow-hidden">
{tagsToShow.map((tag) => (
<Hashtag key={tag} tag={tag} />
))}
</div>
</div>
);
}
function Hashtag(props: { tag: string }) {
return <span className="text-nowrap text-gray-400"># {props.tag}</span>;
}

View File

@ -1,153 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
export default function Terminal() {
const [isReady, setIsReady] = useState(false);
const [currentIndex, setCurrentLineIndex] = useState(0);
const element = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!element.current) {
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsReady(true);
observer.disconnect();
}
});
},
{ threshold: 1 },
);
observer.observe(element.current);
return () => observer.disconnect();
}, [currentIndex]);
function onLineCompleted() {
if (currentIndex < lines.length - 1) {
setCurrentLineIndex((prev) => prev + 1);
}
}
return (
<div
ref={element}
className={`flex w-full flex-col gap-y-1.5 rounded-2xl border-4 border-true-gray-800 bg-true-gray-700 p-4 pb-28 font-mono font-medium text-gray-50 shadow-lg transition-opacity duration-300 md:gap-y-2.5 md:rounded-3xl md:border-8 md:p-8 md:pb-32 md:text-xl md:shadow-xl ${isReady ? "opacity-100" : "opacity-0"}`}
>
{lines.slice(0, currentIndex).map((line, index) => (
<NormalLine key={index} text={line} />
))}
{isReady ? <LastLine key={currentIndex} text={lines[currentIndex]} onCompleted={onLineCompleted} /> : null}
</div>
);
}
function NormalLine(props: { text: string }) {
return (
<div className="flex w-full flex-row gap-x-1.5 md:gap-x-2">
<span className="text-green-400"></span>
<span>{props.text}</span>
</div>
);
}
function LastLine(props: { text: string; onCompleted?: () => void }) {
const [timeText, setTimeText] = useState("");
const [textToShow, setTextToShow] = useState("");
useEffect(() => {
let interval: NodeJS.Timeout | undefined = undefined;
setTimeout(() => {
interval = setInterval(() => {
setTextToShow((prev) => {
if (prev.length < props.text.length) {
return prev + props.text[prev.length];
} else {
clearInterval(interval);
return prev;
}
});
}, 50);
}, 300);
return () => clearInterval(interval);
}, [props.text]);
useEffect(() => {
if (textToShow.length === props.text.length) {
setTimeout(() => {
props.onCompleted?.();
}, 300);
}
}, [props, textToShow]);
useEffect(() => {
setTimeText(dateToString(new Date()));
const interval = setInterval(() => {
setTimeText(dateToString(new Date()));
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<div className="flex w-full flex-col pt-1.5 leading-5 md:pt-2.5 md:leading-7">
<div className="flex w-full flex-row flex-nowrap items-center gap-x-1.5 text-nowrap md:gap-x-2">
<span>
squid{" "}
<span className="text-blue-500">
~<span className="max-md:hidden">/Documents/blog</span>
</span>
</span>
<div className="h-0.5 w-full bg-gray-50" />
<span> {timeText}</span>
</div>
<div className="flex w-full flex-row gap-x-1.5 md:gap-x-2">
<span>
<span className="text-green-400"></span>
</span>
<span>{textToShow}</span>
<Cursor />
</div>
</div>
);
}
function Cursor() {
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
const interval = setInterval(() => {
setIsVisible((prev) => !prev);
setTimeout(() => {
setIsVisible((prev) => !prev);
}, 500);
}, 1000);
return () => clearInterval(interval);
}, []);
return <span className={isVisible ? "" : "hidden"}></span>;
}
function dateToString(date: Date) {
return date.toLocaleString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false });
}
const lines = [
"大家好,我是 Squid 魷魚",
"身為一位軟體工程師",
"平常最喜歡埋首於程式碼的世界",
"鑽研各種新奇有趣的技術",
"在這裡",
"我會分享我的技術筆記、開發心得",
"還有各式各樣實用工具的評測與介紹",
"一起探索數位世界的無限可能吧!",
];

View File

@ -1,6 +0,0 @@
import { useDispatch, useSelector } from "react-redux";
import tagStore from "@/lib/home/presenter/tagStore";
export const useTagDispatch = useDispatch.withTypes<typeof tagStore.dispatch>();
export const useTagSelector = useSelector.withTypes<ReturnType<typeof tagStore.getState>>();

View File

@ -1,55 +0,0 @@
import { createAction, createReducer } from "@reduxjs/toolkit";
import shuffleArray from "@/lib/util/shuffleArray";
export const tagShuffledAction = createAction("tag/shuffled");
const tagReducer = createReducer<TagState>(
() => ({
tags: shuffleArray(tagsCollection),
}),
(builder) => {
builder.addCase(tagShuffledAction, (state) => {
state.tags = shuffleArray(tagsCollection);
});
},
);
export default tagReducer;
export type TagAction = ReturnType<typeof tagShuffledAction>;
export interface TagState {
tags: string[];
timer?: NodeJS.Timeout;
}
const tagsCollection = [
"APP",
"C++",
"Design Pattern",
"Docker",
"Flutter",
"Go",
"Java",
"LINER",
"Linux",
"Python",
"Squid",
"TypeScript",
"中央大學",
"全端",
"分享",
"前端",
"後端",
"教學",
"暴肝",
"知識",
"碼農",
"科技",
"科普",
"程式設計",
"資工系",
"軟體工程",
"遊戲",
"魷魚",
];

View File

@ -1,7 +0,0 @@
import { configureStore } from "@reduxjs/toolkit";
import tagReducer, { TagAction, TagState } from "@/lib/home/presenter/tagReducer";
export default configureStore<TagState, TagAction>({
reducer: tagReducer,
});

View File

@ -1,8 +0,0 @@
export default function shuffleArray<T>(array: T[]): T[] {
const newArray = [...array];
for (let i = newArray.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
}
return newArray;
}

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,28 +0,0 @@
import type { Config } from "tailwindcss";
export default {
content: ["./src/lib/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}"],
theme: {
extend: {
fontFamily: {
sans: ["var(--font-noto-sans-tc)"],
mono: ["var(--font-hack-nerd-mono)", "var(--font-noto-sans-mono)"],
},
colors: {
"true-gray": {
"50": "#fafafa",
"100": "#f5f5f5",
"200": "#e5e5e5",
"300": "#d4d4d4",
"400": "#a3a3a3",
"500": "#737373",
"600": "#525252",
"700": "#404040",
"800": "#262626",
"900": "#171717",
},
},
},
},
plugins: [],
} satisfies Config;

View File

@ -1,27 +1,19 @@
{ {
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "checkJs": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true, "esModuleInterop": true,
"module": "esnext", "forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "skipLibCheck": true,
"jsx": "preserve", "sourceMap": true,
"incremental": true, "strict": true,
"plugins": [ "moduleResolution": "bundler"
{
"name": "next"
} }
], // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
"paths": { // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
"@/*": ["./src/*"] //
} // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
}, // from the referenced tsconfig.json - TypeScript does not merge them in
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
} }