BLOG-7_home_page_about_me (#26)
All checks were successful
Frontend CI / build (push) Successful in 1m22s

### Description

- Implement the home page about me section
  - Terminal styled self introduction
  - Animated Motto

### Package Changes

_No response_

### Screenshots

| Desktop (2K 2160x1440) | Mobile (iPhone16 Pro 390x844) |
| --- | --- |
| <video src="attachments/a6ce3af0-1f25-4f79-89af-191e2148949f" title="Screencast From 2025-01-20 03-23-37.mp4" controls></video> | <video src="attachments/59e4ae24-dc16-4a56-88a9-11b9912c8fd4" title="Screencast From 2025-01-20 03-32-20.mp4" controls></video> |

### Reference

Resolves #7

### Checklist

- [x] A milestone is set

Reviewed-on: #26
Co-authored-by: SquidSpirit <squid@squidspirit.com>
Co-committed-by: SquidSpirit <squid@squidspirit.com>
This commit is contained in:
SquidSpirit 2025-01-20 03:35:28 +08:00 committed by squid
parent 741d92ff15
commit 0e08db5c34
10 changed files with 280 additions and 20 deletions

Binary file not shown.

View File

@ -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));

View File

@ -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 (
<html lang="zh-Hant">
<body className={`${inter.className} antialiased`}>
<body className={`${notoSansTc.variable} ${notoSansMono.variable} ${hackNerdMono.variable} antialiased`}>
<div className="min-h-screen">
<Navbar />
{children}

View File

@ -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 (
<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>
<FirstView />
<AboutMe />
<Motto />
</div>
);
}

View File

@ -0,0 +1,9 @@
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

@ -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<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

@ -0,0 +1,17 @@
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

@ -0,0 +1,18 @@
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

@ -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<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={`bg-true-gray-700 border-true-gray-800 flex w-full flex-col gap-y-1.5 rounded-2xl border-4 p-4 pb-28 font-mono font-medium text-gray-50 transition-opacity duration-300 md:gap-y-2.5 md:rounded-3xl md:border-8 md:p-8 md:pb-32 md:text-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,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: [],