BLOG-58 Rewrite frontend with svelte kit #61
23
frontend-v2/.gitignore
vendored
23
frontend-v2/.gitignore
vendored
@ -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-*
|
@ -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"
|
||||
}
|
@ -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"]
|
@ -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"
|
||||
}
|
2762
frontend-v2/pnpm-lock.yaml
generated
2762
frontend-v2/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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
149
frontend/.gitignore
vendored
@ -1,136 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
node_modules
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
# Output
|
||||
.output
|
||||
.vercel
|
||||
.netlify
|
||||
.wrangler
|
||||
/.svelte-kit
|
||||
/build
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
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.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.*
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# 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.*
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
@ -1,11 +1,16 @@
|
||||
{
|
||||
"plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"],
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": false,
|
||||
"semi": true,
|
||||
"importOrder": ["^[\\w-]+(/.*)?$", "^@[\\w-]+(/.*)?$", "^@/(app|lib)(/.*)?$", "^\\..*$"],
|
||||
"importOrderSeparation": true,
|
||||
"importOrderSortSpecifiers": true
|
||||
"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"
|
||||
}
|
||||
|
@ -1,27 +1,26 @@
|
||||
FROM node:20-alpine AS base
|
||||
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 && \
|
||||
corepack enable pnpm && \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
RUN corepack enable pnpm && \
|
||||
pnpm run build
|
||||
RUN pnpm run build
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/.next/standalone ./
|
||||
COPY --from=builder /app/.next/static ./.next/static
|
||||
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", "server.js"]
|
||||
CMD ["node", "build"]
|
||||
|
@ -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.
|
@ -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;
|
5
frontend/next-env.d.ts
vendored
5
frontend/next-env.d.ts
vendored
@ -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.
|
@ -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;
|
@ -1,39 +1,46 @@
|
||||
{
|
||||
"name": "squidspirit-blog",
|
||||
"version": "0.1.1",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"packageManager": "pnpm@10.0.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@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": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.1",
|
||||
"@types/node": "^20.17.14",
|
||||
"@types/react": "^19.0.7",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-next": "15.1.4",
|
||||
"postcss": "^8.5.1",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.10",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
"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"
|
||||
}
|
||||
|
4453
frontend/pnpm-lock.yaml
generated
4453
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
@ -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 |
@ -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 |
@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Allow: /
|
Binary file not shown.
@ -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));
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>;
|
||||
}
|
@ -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 魷魚",
|
||||
"身為一位軟體工程師",
|
||||
"平常最喜歡埋首於程式碼的世界",
|
||||
"鑽研各種新奇有趣的技術",
|
||||
"在這裡",
|
||||
"我會分享我的技術筆記、開發心得",
|
||||
"還有各式各樣實用工具的評測與介紹",
|
||||
"一起探索數位世界的無限可能吧!",
|
||||
];
|
@ -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>>();
|
@ -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",
|
||||
"中央大學",
|
||||
"全端",
|
||||
"分享",
|
||||
"前端",
|
||||
"後端",
|
||||
"教學",
|
||||
"暴肝",
|
||||
"知識",
|
||||
"碼農",
|
||||
"科技",
|
||||
"科普",
|
||||
"程式設計",
|
||||
"資工系",
|
||||
"軟體工程",
|
||||
"遊戲",
|
||||
"魷魚",
|
||||
];
|
@ -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,
|
||||
});
|
@ -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;
|
||||
}
|
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 5.3 KiB |
@ -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;
|
@ -1,27 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user