BLOG-5 feat: self tags animation
This commit is contained in:
parent
738008b983
commit
7f1a202cc7
@ -1,7 +1,7 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
reactStrictMode: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
@ -15,9 +15,11 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
|
"@reduxjs/toolkit": "^2.5.0",
|
||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"react-redux": "^9.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
86
frontend/pnpm-lock.yaml
generated
86
frontend/pnpm-lock.yaml
generated
@ -23,6 +23,9 @@ importers:
|
|||||||
'@fortawesome/react-fontawesome':
|
'@fortawesome/react-fontawesome':
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.0.0)
|
version: 0.2.2(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.0.0)
|
||||||
|
'@reduxjs/toolkit':
|
||||||
|
specifier: ^2.5.0
|
||||||
|
version: 2.5.0(react-redux@9.2.0(@types/react@19.0.7)(react@19.0.0)(redux@5.0.1))(react@19.0.0)
|
||||||
next:
|
next:
|
||||||
specifier: 15.1.4
|
specifier: 15.1.4
|
||||||
version: 15.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 15.1.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@ -32,6 +35,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.0.0(react@19.0.0)
|
version: 19.0.0(react@19.0.0)
|
||||||
|
react-redux:
|
||||||
|
specifier: ^9.2.0
|
||||||
|
version: 9.2.0(@types/react@19.0.7)(react@19.0.0)(redux@5.0.1)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
@ -357,6 +363,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
'@reduxjs/toolkit@2.5.0':
|
||||||
|
resolution: {integrity: sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.9.0 || ^17.0.0 || ^18 || ^19
|
||||||
|
react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-redux:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0':
|
'@rtsao/scc@1.1.0':
|
||||||
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
|
||||||
|
|
||||||
@ -389,6 +406,9 @@ packages:
|
|||||||
'@types/react@19.0.7':
|
'@types/react@19.0.7':
|
||||||
resolution: {integrity: sha512-MoFsEJKkAtZCrC1r6CM8U22GzhG7u2Wir8ons/aCKH6MBdD1ibV24zOSSkdZVUKqN5i396zG5VKLYZ3yaUZdLA==}
|
resolution: {integrity: sha512-MoFsEJKkAtZCrC1r6CM8U22GzhG7u2Wir8ons/aCKH6MBdD1ibV24zOSSkdZVUKqN5i396zG5VKLYZ3yaUZdLA==}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6':
|
||||||
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.20.0':
|
'@typescript-eslint/eslint-plugin@8.20.0':
|
||||||
resolution: {integrity: sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==}
|
resolution: {integrity: sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -979,6 +999,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
immer@10.1.1:
|
||||||
|
resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==}
|
||||||
|
|
||||||
import-fresh@3.3.0:
|
import-fresh@3.3.0:
|
||||||
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -1462,6 +1485,18 @@ packages:
|
|||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
|
react-redux@9.2.0:
|
||||||
|
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': ^18.2.25 || ^19
|
||||||
|
react: ^18.0 || ^19
|
||||||
|
redux: ^5.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
redux:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react@19.0.0:
|
react@19.0.0:
|
||||||
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
|
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -1473,6 +1508,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||||
engines: {node: '>=8.10.0'}
|
engines: {node: '>=8.10.0'}
|
||||||
|
|
||||||
|
redux-thunk@3.1.0:
|
||||||
|
resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
|
||||||
|
peerDependencies:
|
||||||
|
redux: ^5.0.0
|
||||||
|
|
||||||
|
redux@5.0.1:
|
||||||
|
resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@ -1481,6 +1524,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
reselect@5.1.1:
|
||||||
|
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||||
|
|
||||||
resolve-from@4.0.0:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -1729,6 +1775,11 @@ packages:
|
|||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
use-sync-external-store@1.4.0:
|
||||||
|
resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
|
|
||||||
@ -2010,6 +2061,16 @@ snapshots:
|
|||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@reduxjs/toolkit@2.5.0(react-redux@9.2.0(@types/react@19.0.7)(react@19.0.0)(redux@5.0.1))(react@19.0.0)':
|
||||||
|
dependencies:
|
||||||
|
immer: 10.1.1
|
||||||
|
redux: 5.0.1
|
||||||
|
redux-thunk: 3.1.0(redux@5.0.1)
|
||||||
|
reselect: 5.1.1
|
||||||
|
optionalDependencies:
|
||||||
|
react: 19.0.0
|
||||||
|
react-redux: 9.2.0(@types/react@19.0.7)(react@19.0.0)(redux@5.0.1)
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
|
|
||||||
'@rushstack/eslint-patch@1.10.5': {}
|
'@rushstack/eslint-patch@1.10.5': {}
|
||||||
@ -2038,6 +2099,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
csstype: 3.1.3
|
csstype: 3.1.3
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
'@typescript-eslint/eslint-plugin@8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3)':
|
'@typescript-eslint/eslint-plugin@8.20.0(@typescript-eslint/parser@8.20.0(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.18.0(jiti@1.21.7))(typescript@5.7.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/regexpp': 4.12.1
|
'@eslint-community/regexpp': 4.12.1
|
||||||
@ -2840,6 +2903,8 @@ snapshots:
|
|||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
|
|
||||||
|
immer@10.1.1: {}
|
||||||
|
|
||||||
import-fresh@3.3.0:
|
import-fresh@3.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
@ -3266,6 +3331,15 @@ snapshots:
|
|||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
|
react-redux@9.2.0(@types/react@19.0.7)(react@19.0.0)(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
'@types/use-sync-external-store': 0.0.6
|
||||||
|
react: 19.0.0
|
||||||
|
use-sync-external-store: 1.4.0(react@19.0.0)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.0.7
|
||||||
|
redux: 5.0.1
|
||||||
|
|
||||||
react@19.0.0: {}
|
react@19.0.0: {}
|
||||||
|
|
||||||
read-cache@1.0.0:
|
read-cache@1.0.0:
|
||||||
@ -3276,6 +3350,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
redux-thunk@3.1.0(redux@5.0.1):
|
||||||
|
dependencies:
|
||||||
|
redux: 5.0.1
|
||||||
|
|
||||||
|
redux@5.0.1: {}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@ -3296,6 +3376,8 @@ snapshots:
|
|||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
set-function-name: 2.0.2
|
set-function-name: 2.0.2
|
||||||
|
|
||||||
|
reselect@5.1.1: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
@ -3642,6 +3724,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
use-sync-external-store@1.4.0(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
which-boxed-primitive@1.1.1:
|
which-boxed-primitive@1.1.1:
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import Navbar from "@/ui/layout/navbar";
|
import Navbar from "@/lib/common/presenter/ui/Navbar";
|
||||||
import Footer from "@/ui/layout/footer";
|
import Footer from "@/lib/common/presenter/ui/Footer";
|
||||||
|
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import SelfTags from "@/lib/home/presenter/ui/SelfTags";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
@ -13,29 +13,7 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
<span>魷魚</span>
|
<span>魷魚</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="relative w-full max-w-screen-md">
|
<SelfTags />
|
||||||
<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">
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
<Hashtag tag="React" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Hashtag(props: { tag: string }) {
|
|
||||||
return <span className="text-nowrap text-gray-400"># {props.tag}</span>;
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { faGithub, faYoutube } from "@fortawesome/free-brands-svg-icons";
|
import { faGithub, faYoutube } from "@fortawesome/free-brands-svg-icons";
|
||||||
import { faEnvelope } from "@fortawesome/free-solid-svg-icons";
|
import { faEnvelope } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
@ -1,5 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-gray-300">
|
<div className="border-b border-gray-300">
|
6
frontend/src/lib/home/presenter/redux/tagHooks.ts
Normal file
6
frontend/src/lib/home/presenter/redux/tagHooks.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import tagStore from "./tagStore";
|
||||||
|
|
||||||
|
export const useTagDispatch = useDispatch.withTypes<typeof tagStore.dispatch>();
|
||||||
|
export const useTagSelector =
|
||||||
|
useSelector.withTypes<ReturnType<typeof tagStore.getState>>();
|
72
frontend/src/lib/home/presenter/redux/tagSlice.ts
Normal file
72
frontend/src/lib/home/presenter/redux/tagSlice.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
import tagStore from "./tagStore";
|
||||||
|
import shuffleArray from "@/lib/util/shuffleArray";
|
||||||
|
|
||||||
|
export interface TagState {
|
||||||
|
tags: string[];
|
||||||
|
timer?: NodeJS.Timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TagStartedActionPayload {
|
||||||
|
interval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tagSlice = createSlice({
|
||||||
|
name: "tag",
|
||||||
|
initialState: {
|
||||||
|
tags: [],
|
||||||
|
timer: undefined,
|
||||||
|
} as TagState,
|
||||||
|
reducers: {
|
||||||
|
started: (state, action: PayloadAction<TagStartedActionPayload>) => {
|
||||||
|
state.tags = shuffleArray(tagsCollection);
|
||||||
|
state.timer = setInterval(() => {
|
||||||
|
tagStore.dispatch(tagSlice.actions.shuffled());
|
||||||
|
}, action.payload.interval);
|
||||||
|
},
|
||||||
|
shuffled: (state) => {
|
||||||
|
state.tags = shuffleArray(tagsCollection);
|
||||||
|
},
|
||||||
|
stopped: (state) => {
|
||||||
|
clearInterval(state.timer);
|
||||||
|
state.timer = undefined;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const tagStartedAction = tagSlice.actions.started;
|
||||||
|
export const tagStoppedAction = tagSlice.actions.stopped;
|
||||||
|
|
||||||
|
const tagReducer = tagSlice.reducer;
|
||||||
|
export default tagReducer;
|
||||||
|
|
||||||
|
const tagsCollection = [
|
||||||
|
"APP",
|
||||||
|
"C++",
|
||||||
|
"Design Pattern",
|
||||||
|
"Docker",
|
||||||
|
"Flutter",
|
||||||
|
"Go",
|
||||||
|
"Java",
|
||||||
|
"LINER",
|
||||||
|
"Linux",
|
||||||
|
"Python",
|
||||||
|
"Squid",
|
||||||
|
"TypeScript",
|
||||||
|
"中央大學",
|
||||||
|
"全端",
|
||||||
|
"分享",
|
||||||
|
"前端",
|
||||||
|
"後端",
|
||||||
|
"教學",
|
||||||
|
"暴肝",
|
||||||
|
"知識",
|
||||||
|
"碼農",
|
||||||
|
"科技",
|
||||||
|
"科普",
|
||||||
|
"程式設計",
|
||||||
|
"資工系",
|
||||||
|
"軟體工程",
|
||||||
|
"遊戲",
|
||||||
|
"魷魚",
|
||||||
|
];
|
10
frontend/src/lib/home/presenter/redux/tagStore.ts
Normal file
10
frontend/src/lib/home/presenter/redux/tagStore.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import tagReducer from "./tagSlice";
|
||||||
|
|
||||||
|
const tagStore = configureStore({
|
||||||
|
reducer: {
|
||||||
|
tag: tagReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tagStore;
|
55
frontend/src/lib/home/presenter/ui/SelfTags.tsx
Normal file
55
frontend/src/lib/home/presenter/ui/SelfTags.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import tagStore from "../redux/tagStore";
|
||||||
|
import { useTagDispatch, useTagSelector } from "../redux/tagHooks";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { tagStartedAction, tagStoppedAction } from "../redux/tagSlice";
|
||||||
|
|
||||||
|
export default function SelfTags() {
|
||||||
|
return (
|
||||||
|
<Provider store={tagStore}>
|
||||||
|
<_SelfTags />
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _SelfTags() {
|
||||||
|
const tags = useTagSelector((state) => state.tag.tags);
|
||||||
|
const dispatch = useTagDispatch();
|
||||||
|
|
||||||
|
const [isTagsVisible, setIsTagsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(tagStartedAction({ interval: 4000 }));
|
||||||
|
return () => {
|
||||||
|
dispatch(tagStoppedAction());
|
||||||
|
setIsTagsVisible(false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tags.length === 0) return;
|
||||||
|
setIsTagsVisible(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsTagsVisible(false);
|
||||||
|
}, 3500);
|
||||||
|
}, [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">
|
||||||
|
{tags.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>;
|
||||||
|
}
|
8
frontend/src/lib/util/shuffleArray.ts
Normal file
8
frontend/src/lib/util/shuffleArray.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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;
|
||||||
|
}
|
@ -2,7 +2,7 @@ import type { Config } from "tailwindcss";
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
"./src/ui/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/lib/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user