BLOG-7 feat: about me terminal
All checks were successful
Frontend CI / build (push) Successful in 1m22s
All checks were successful
Frontend CI / build (push) Successful in 1m22s
This commit is contained in:
parent
5517766f3f
commit
293d557c1b
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;
|
@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));
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
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";
|
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: [],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user