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 (
+
+ );
+}
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: [],