BLOG-7 feat: about me terminal
All checks were successful
Frontend CI / build (push) Successful in 1m22s

This commit is contained in:
SquidSpirit 2025-01-20 03:29:21 +08:00
parent 5517766f3f
commit 293d557c1b
8 changed files with 199 additions and 13 deletions

Binary file not shown.

View File

@ -3,7 +3,7 @@
@tailwind utilities; @tailwind utilities;
body { 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; --tool-bar-height: 4rem;
--content-height: calc(100vh - var(--tool-bar-height)); --content-height: calc(100vh - var(--tool-bar-height));

View File

@ -1,15 +1,27 @@
import type { Metadata } from "next"; 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 Footer from "@/lib/common/presenter/ui/Footer";
import Navbar from "@/lib/common/presenter/ui/Navbar"; import Navbar from "@/lib/common/presenter/ui/Navbar";
import "./globals.css"; import "./globals.css";
const inter = Inter({ const notoSansTc = Noto_Sans_TC({
variable: "--font-noto-sans-tc",
subsets: ["latin"], 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 = { export const metadata: Metadata = {
title: "魚之魷魂 SquidSpirit", title: "魚之魷魂 SquidSpirit",
description: "程式、科技、教學、分享", description: "程式、科技、教學、分享",
@ -22,7 +34,7 @@ export default function RootLayout({
}>) { }>) {
return ( return (
<html lang="zh-Hant"> <html lang="zh-Hant">
<body className={`${inter.className} antialiased`}> <body className={`${notoSansTc.variable} ${notoSansMono.variable} ${hackNerdMono.variable} antialiased`}>
<div className="min-h-screen"> <div className="min-h-screen">
<Navbar /> <Navbar />
{children} {children}

View File

@ -1,3 +1,9 @@
import Terminal from "@/lib/home/framework/ui/Terminal";
export default function AboutMe() { export default function AboutMe() {
return <></>; 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

@ -19,7 +19,6 @@ export default function AnimatedMark(props: { text: string; direction: "left" |
entries.forEach((entry) => { entries.forEach((entry) => {
if (entry.isIntersecting) { if (entry.isIntersecting) {
setIsVisible(true); setIsVisible(true);
console.log(element.current);
observer.disconnect(); observer.disconnect();
} }
}); });
@ -27,6 +26,8 @@ export default function AnimatedMark(props: { text: string; direction: "left" |
{ threshold: 1 }, { threshold: 1 },
); );
observer.observe(element.current); observer.observe(element.current);
return () => observer.disconnect();
}, []); }, []);
return ( return (

View File

@ -1,8 +1,8 @@
import AnimatedMark from "./AnimatedMark"; import AnimatedMark from "@/lib/home/framework/ui/AnimatedMark";
export default function Motto() { export default function Motto() {
return ( return (
<div className="mx-auto flex min-h-[--content-height] 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="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-[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"> <div className="flex w-full flex-row items-center justify-start gap-x-2.5">
<span>Keep</span> <span>Keep</span>

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"; import type { Config } from "tailwindcss";
export default { export default {
content: [ content: ["./src/lib/**/*.{js,ts,jsx,tsx,mdx}", "./src/app/**/*.{js,ts,jsx,tsx,mdx}"],
"./src/lib/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: { theme: {
extend: { 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: [], plugins: [],