BLOG-7_home_page_about_me (#26)
All checks were successful
Frontend CI / build (push) Successful in 1m22s
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:
parent
741d92ff15
commit
0e08db5c34
BIN
frontend/src/app/HackNerdMono.woff2
Normal file
BIN
frontend/src/app/HackNerdMono.woff2
Normal file
Binary file not shown.
@ -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));
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
9
frontend/src/lib/home/framework/ui/AboutMe.tsx
Normal file
9
frontend/src/lib/home/framework/ui/AboutMe.tsx
Normal 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>
|
||||
);
|
||||
}
|
41
frontend/src/lib/home/framework/ui/AnimatedMark.tsx
Normal file
41
frontend/src/lib/home/framework/ui/AnimatedMark.tsx
Normal 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>
|
||||
);
|
||||
}
|
17
frontend/src/lib/home/framework/ui/FirstView.tsx
Normal file
17
frontend/src/lib/home/framework/ui/FirstView.tsx
Normal 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>
|
||||
);
|
||||
}
|
18
frontend/src/lib/home/framework/ui/Motto.tsx
Normal file
18
frontend/src/lib/home/framework/ui/Motto.tsx
Normal 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>
|
||||
);
|
||||
}
|
153
frontend/src/lib/home/framework/ui/Terminal.tsx
Normal file
153
frontend/src/lib/home/framework/ui/Terminal.tsx
Normal 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 魷魚",
|
||||
"身為一位軟體工程師",
|
||||
"平常最喜歡埋首於程式碼的世界",
|
||||
"鑽研各種新奇有趣的技術",
|
||||
"在這裡",
|
||||
"我會分享我的技術筆記、開發心得",
|
||||
"還有各式各樣實用工具的評測與介紹",
|
||||
"一起探索數位世界的無限可能吧!",
|
||||
];
|
@ -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: [],
|
||||
|
Loading…
x
Reference in New Issue
Block a user