BLOG-58 feat: implement terminal UI components, refactor Navbar, and update styles
All checks were successful
Frontend CI / build (push) Successful in 1m31s
All checks were successful
Frontend CI / build (push) Successful in 1m31s
This commit is contained in:
parent
98802dc862
commit
2a7d0f9878
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -1,12 +1,3 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"actix",
|
||||
"chrono",
|
||||
"dotenv",
|
||||
"rustls",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"squidspirit"
|
||||
]
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,18 @@
|
||||
@theme {
|
||||
--font-sans: 'Noto Sans TC', sans-serif;
|
||||
--font-mono: 'HackNerdMono', 'Noto Sans Mono', monospace;
|
||||
|
||||
--color-true-gray-50: #fafafa;
|
||||
--color-true-gray-100: #f5f5f5;
|
||||
--color-true-gray-200: #e5e5e5;
|
||||
--color-true-gray-300: #d4d4d4;
|
||||
--color-true-gray-400: #a3a3a3;
|
||||
--color-true-gray-500: #737373;
|
||||
--color-true-gray-600: #525252;
|
||||
--color-true-gray-700: #404040;
|
||||
--color-true-gray-800: #262626;
|
||||
--color-true-gray-900: #171717;
|
||||
|
||||
--spacing-toolbar-height: 4rem;
|
||||
--spacing-content-height: calc(100vh - var(--spacing-toolbar-height));
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import Action from '$lib/common/framework/ui/Action.svelte';
|
||||
import NavbarAction from '$lib/common/framework/ui/NavbarAction.svelte';
|
||||
</script>
|
||||
|
||||
<div class="border-b border-gray-300">
|
||||
@ -12,7 +12,7 @@
|
||||
<span class="text-2xl font-black text-gray-800">魚之魷魂</span>
|
||||
</a>
|
||||
<div class="flex flex-row items-center gap-x-6">
|
||||
<Action label="首頁" link="/" isSelected={page.url.pathname === '/'} />
|
||||
<NavbarAction label="首頁" link="/" isSelected={page.url.pathname === '/'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,13 @@
|
||||
<script lang="ts">
|
||||
let { label, link, isSelected } = $props();
|
||||
let {
|
||||
label,
|
||||
link,
|
||||
isSelected
|
||||
}: {
|
||||
label: string;
|
||||
link: string;
|
||||
isSelected: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="rounded px-1.5 {isSelected ? 'bg-blue-600' : 'bg-transparent'}">
|
74
frontend-v2/src/lib/home/framework/ui/Terminal.svelte
Normal file
74
frontend-v2/src/lib/home/framework/ui/Terminal.svelte
Normal file
@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import TerminalLastLine from '$lib/home/framework/ui/TerminalLastLine.svelte';
|
||||
import TerminalNormalLine from '$lib/home/framework/ui/TerminalNormalLine.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
const lines = [
|
||||
'大家好,我是 Squid 魷魚',
|
||||
'身為一位軟體工程師',
|
||||
'平常最喜歡埋首於程式碼的世界',
|
||||
'鑽研各種新奇有趣的技術',
|
||||
'在這裡',
|
||||
'我會分享我的技術筆記、開發心得',
|
||||
'還有各式各樣實用工具的評測與介紹',
|
||||
'一起探索數位世界的無限可能吧!'
|
||||
];
|
||||
|
||||
let isReady: boolean = $state(false);
|
||||
let currentIndex: number = $state(0);
|
||||
|
||||
let element: HTMLDivElement | null = null;
|
||||
let observer: IntersectionObserver | null = null;
|
||||
|
||||
onMount(() => {
|
||||
if (element == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
isReady = true;
|
||||
observer?.disconnect();
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 1 }
|
||||
);
|
||||
|
||||
observer.observe(element);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
observer?.disconnect();
|
||||
observer = null;
|
||||
});
|
||||
|
||||
function onLastLineComplete() {
|
||||
if (currentIndex < lines.length - 1) {
|
||||
currentIndex++;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="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"
|
||||
>
|
||||
<div
|
||||
bind:this={element}
|
||||
class="border-true-gray-800 bg-true-gray-700 flex w-full flex-col gap-y-1.5 rounded-2xl border-4 p-4 pb-28 font-mono font-medium text-gray-50 shadow-lg transition-opacity duration-300 md:gap-y-2.5 md:rounded-3xl md:border-8 md:p-8 md:pb-32 md:text-xl md:shadow-xl {isReady
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'}"
|
||||
>
|
||||
{#each lines.slice(0, currentIndex) as line, index (index)}
|
||||
<TerminalNormalLine text={line} />
|
||||
{/each}
|
||||
|
||||
{#if isReady}
|
||||
{#key currentIndex}
|
||||
<TerminalLastLine text={lines[currentIndex]} onComplete={onLastLineComplete} />
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
27
frontend-v2/src/lib/home/framework/ui/TerminalCursor.svelte
Normal file
27
frontend-v2/src/lib/home/framework/ui/TerminalCursor.svelte
Normal file
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let isVisible: boolean = $state(true);
|
||||
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
onMount(() => {
|
||||
interval = setInterval(() => {
|
||||
toggleVisibility();
|
||||
setTimeout(toggleVisibility, 500);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (interval != null) {
|
||||
clearInterval(interval);
|
||||
interval = null;
|
||||
}
|
||||
});
|
||||
|
||||
function toggleVisibility() {
|
||||
isVisible = !isVisible;
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="transition-opacity duration-200 {isVisible ? 'opacity-100' : 'opacity-0'}">█</span>
|
@ -0,0 +1,66 @@
|
||||
<script lang="ts">
|
||||
import TerminalCursor from '$lib/home/framework/ui/TerminalCursor.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
|
||||
let { text, onComplete: onCompleted }: { text: string; onComplete: () => void } = $props();
|
||||
|
||||
let timeText: string = $state('');
|
||||
let showingText: string = $state('');
|
||||
|
||||
let textUpdateInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let timeUpdateInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
textUpdateInterval = setInterval(() => {
|
||||
if (showingText.length < text.length) {
|
||||
showingText += text[showingText.length];
|
||||
} else {
|
||||
clearInterval(textUpdateInterval!);
|
||||
setTimeout(onCompleted, 300);
|
||||
}
|
||||
}, 50);
|
||||
}, 300);
|
||||
|
||||
timeUpdateInterval = setInterval(() => {
|
||||
const now = new Date();
|
||||
timeText = now.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (textUpdateInterval != null) {
|
||||
clearInterval(textUpdateInterval);
|
||||
textUpdateInterval = null;
|
||||
}
|
||||
if (timeUpdateInterval != null) {
|
||||
clearInterval(timeUpdateInterval);
|
||||
timeUpdateInterval = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col pt-1.5 leading-5 md:pt-2.5 md:leading-7">
|
||||
<div class="flex w-full flex-row flex-nowrap items-center gap-x-1.5 text-nowrap md:gap-x-2">
|
||||
<span>
|
||||
╭─ squid{' '}
|
||||
<span class="text-blue-500">
|
||||
~<span class="max-md:hidden">/Documents/blog</span>
|
||||
</span>
|
||||
</span>
|
||||
<div class="h-0.5 w-full bg-gray-50"></div>
|
||||
<span> {timeText}</span>
|
||||
</div>
|
||||
<div class="flex w-full flex-row gap-x-1.5 md:gap-x-2">
|
||||
<span>
|
||||
╰─<span class="text-green-400">❯</span>
|
||||
</span>
|
||||
<span>{showingText}</span>
|
||||
<TerminalCursor />
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
const { text }: { text: string } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-row gap-x-1.5 md:gap-x-2">
|
||||
<span class="text-green-400">❯</span>
|
||||
<span>{text}</span>
|
||||
</div>
|
@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import SelfTags from '$lib/home/framework/ui/SelfTags.svelte';
|
||||
import TitleScreenAnimatedTags from '$lib/home/framework/ui/TitleScreenAnimatedTags.svelte';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="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"
|
||||
class="min-h-content-height mx-auto flex max-w-screen-xl flex-col justify-center gap-y-2.5 px-4 md:gap-y-8 md:px-6"
|
||||
>
|
||||
<h2 class="text-3xl font-bold text-gray-800 md:text-6xl">Hello 大家好!</h2>
|
||||
<h1 class="flex flex-row items-center gap-x-2 text-4xl font-extrabold text-gray-800 md:text-7xl">
|
||||
@ -13,5 +13,5 @@
|
||||
</div>
|
||||
<span>魷魚</span>
|
||||
</h1>
|
||||
<SelfTags />
|
||||
<TitleScreenAnimatedTags />
|
||||
</div>
|
@ -1,7 +1,9 @@
|
||||
<script>
|
||||
import FirstView from '$lib/home/framework/ui/FirstView.svelte';
|
||||
import Terminal from '$lib/home/framework/ui/Terminal.svelte';
|
||||
import TitleScreen from '$lib/home/framework/ui/TitleScreen.svelte';
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<FirstView />
|
||||
<TitleScreen />
|
||||
<Terminal />
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user