diff --git a/frontend/src/app/HackNerdMono.woff2 b/frontend/src/app/HackNerdMono.woff2 new file mode 100644 index 0000000..00efde8 Binary files /dev/null and b/frontend/src/app/HackNerdMono.woff2 differ diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 034d269..f0b6cd0 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -3,7 +3,7 @@ @tailwind utilities; body { - @apply bg-white text-base font-normal text-gray-600; + @apply bg-white font-sans text-base font-normal text-gray-600; --tool-bar-height: 4rem; --content-height: calc(100vh - var(--tool-bar-height)); diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 7e4bc7d..c63d8d6 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,15 +1,27 @@ import type { Metadata } from "next"; -import { Inter } from "next/font/google"; +import { Noto_Sans_Mono, Noto_Sans_TC } from "next/font/google"; +import localFont from "next/font/local"; import Footer from "@/lib/common/presenter/ui/Footer"; import Navbar from "@/lib/common/presenter/ui/Navbar"; import "./globals.css"; -const inter = Inter({ +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: "./HackNerdMono.woff2", + variable: "--font-hack-nerd-mono", +}); + export const metadata: Metadata = { title: "魚之魷魂 SquidSpirit", description: "程式、科技、教學、分享", @@ -22,7 +34,7 @@ export default function RootLayout({ }>) { return ( - +
{children} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 8c21ab0..91218ef 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1,17 +1,13 @@ -import SelfTags from "@/lib/home/framework/ui/SelfTags"; +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 ( -
-

Hello 大家好!

-

- 我是 -
- Squid -
- 魷魚 -

- +
+ + +
); } diff --git a/frontend/src/lib/home/framework/ui/AboutMe.tsx b/frontend/src/lib/home/framework/ui/AboutMe.tsx new file mode 100644 index 0000000..072b7f6 --- /dev/null +++ b/frontend/src/lib/home/framework/ui/AboutMe.tsx @@ -0,0 +1,9 @@ +import Terminal from "@/lib/home/framework/ui/Terminal"; + +export default function AboutMe() { + return ( +
+ +
+ ); +} diff --git a/frontend/src/lib/home/framework/ui/AnimatedMark.tsx b/frontend/src/lib/home/framework/ui/AnimatedMark.tsx new file mode 100644 index 0000000..33bbdcf --- /dev/null +++ b/frontend/src/lib/home/framework/ui/AnimatedMark.tsx @@ -0,0 +1,41 @@ +"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(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 ( + + {props.text} + + ); +} diff --git a/frontend/src/lib/home/framework/ui/FirstView.tsx b/frontend/src/lib/home/framework/ui/FirstView.tsx new file mode 100644 index 0000000..2dcb929 --- /dev/null +++ b/frontend/src/lib/home/framework/ui/FirstView.tsx @@ -0,0 +1,17 @@ +import SelfTags from "@/lib/home/framework/ui/SelfTags"; + +export default function FirstView() { + return ( +
+

Hello 大家好!

+

+ 我是 +
+ Squid +
+ 魷魚 +

+ +
+ ); +} diff --git a/frontend/src/lib/home/framework/ui/Motto.tsx b/frontend/src/lib/home/framework/ui/Motto.tsx new file mode 100644 index 0000000..632836c --- /dev/null +++ b/frontend/src/lib/home/framework/ui/Motto.tsx @@ -0,0 +1,18 @@ +import AnimatedMark from "@/lib/home/framework/ui/AnimatedMark"; + +export default function Motto() { + return ( +
+
+
+ Keep + +
+
+ + Progressing +
+
+
+ ); +} diff --git a/frontend/src/lib/home/framework/ui/Terminal.tsx b/frontend/src/lib/home/framework/ui/Terminal.tsx new file mode 100644 index 0000000..bdba586 --- /dev/null +++ b/frontend/src/lib/home/framework/ui/Terminal.tsx @@ -0,0 +1,153 @@ +"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(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 ( +
+ {lines.slice(0, currentIndex).map((line, index) => ( + + ))} + {isReady ? : null} +
+ ); +} + +function NormalLine(props: { text: string }) { + return ( +
+ + {props.text} +
+ ); +} + +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 ( +
+
+ + ╭─  squid{" "} + + ~/Documents/blog + + +
+  {timeText} +
+
+ + ╰─ + + {textToShow} + +
+
+ ); +} + +function Cursor() { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + const interval = setInterval(() => { + setIsVisible((prev) => !prev); + setTimeout(() => { + setIsVisible((prev) => !prev); + }, 500); + }, 1000); + + return () => clearInterval(interval); + }, []); + + return ; +} + +function dateToString(date: Date) { + return date.toLocaleString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }); +} + +const lines = [ + "大家好,我是 Squid 魷魚", + "身為一位軟體工程師", + "平常最喜歡埋首於程式碼的世界", + "鑽研各種新奇有趣的技術", + "在這裡", + "我會分享我的技術筆記、開發心得", + "還有各式各樣實用工具的評測與介紹", + "一起探索數位世界的無限可能吧!", +]; diff --git a/frontend/tailwind.config.ts b/frontend/tailwind.config.ts index 11fcf7f..1b8c78f 100644 --- a/frontend/tailwind.config.ts +++ b/frontend/tailwind.config.ts @@ -1,13 +1,27 @@ import type { Config } from "tailwindcss"; export default { - content: [ - "./src/lib/**/*.{js,ts,jsx,tsx,mdx}", - "./src/app/**/*.{js,ts,jsx,tsx,mdx}", - ], + 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: [],