feat: ship blog platform admin and deploy stack
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
---
|
||||
import { API_BASE_URL } from '../lib/api/client';
|
||||
import { resolvePublicApiBaseUrl } from '../lib/api/client';
|
||||
import { terminalConfig } from '../lib/config/terminal';
|
||||
import { getI18n, SUPPORTED_LOCALES } from '../lib/i18n';
|
||||
import ThemeToggle from './interactive/ThemeToggle.svelte';
|
||||
import type { SiteSettings } from '../lib/types';
|
||||
|
||||
interface Props {
|
||||
@@ -19,6 +20,7 @@ const musicPlaylist = (Astro.props.siteSettings?.musicPlaylist || []).filter(
|
||||
(item) => item?.title?.trim() && item?.url?.trim()
|
||||
);
|
||||
const musicPlaylistPayload = JSON.stringify(musicPlaylist);
|
||||
const publicApiBaseUrl = resolvePublicApiBaseUrl(Astro.url);
|
||||
const hasMusicPlaylist = musicPlaylist.length > 0;
|
||||
const currentMusicTrack = hasMusicPlaylist ? musicPlaylist[0] : null;
|
||||
const navItems = [
|
||||
@@ -89,7 +91,7 @@ const currentNavLabel =
|
||||
placeholder={t('header.searchPlaceholderKeyword')}
|
||||
class="terminal-console-input"
|
||||
/>
|
||||
<button id="search-btn" class="terminal-toolbar-iconbtn h-8 w-8">
|
||||
<button id="search-btn" class="terminal-toolbar-iconbtn h-8 w-8" aria-label="Search">
|
||||
<i id="search-btn-icon" class="fas fa-search text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -126,13 +128,13 @@ const currentNavLabel =
|
||||
{currentMusicTrack?.title || '未配置曲目'}
|
||||
</p>
|
||||
<div class="mt-1 flex items-center gap-1">
|
||||
<button id="desktop-music-prev" class="terminal-toolbar-iconbtn h-7 w-7" disabled={!hasMusicPlaylist}>
|
||||
<button id="desktop-music-prev" class="terminal-toolbar-iconbtn h-7 w-7" aria-label="Previous track" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-step-backward text-[11px]"></i>
|
||||
</button>
|
||||
<button id="desktop-music-play" class="terminal-toolbar-iconbtn h-7 w-7 bg-[var(--primary)]/12 text-[var(--primary)]" style="border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));" disabled={!hasMusicPlaylist}>
|
||||
<button id="desktop-music-play" class="terminal-toolbar-iconbtn h-7 w-7 bg-[var(--primary)]/12 text-[var(--primary)]" style="border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));" aria-label="Play or pause" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-play text-[11px]" id="desktop-music-play-icon"></i>
|
||||
</button>
|
||||
<button id="desktop-music-next" class="terminal-toolbar-iconbtn h-7 w-7" disabled={!hasMusicPlaylist}>
|
||||
<button id="desktop-music-next" class="terminal-toolbar-iconbtn h-7 w-7" aria-label="Next track" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-step-forward text-[11px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -169,17 +171,15 @@ const currentNavLabel =
|
||||
</div>
|
||||
|
||||
<div class="relative shrink-0">
|
||||
<button
|
||||
id="theme-toggle"
|
||||
class="theme-toggle terminal-toolbar-iconbtn h-8 w-8 shrink-0"
|
||||
aria-label={t('header.themeToggle')}
|
||||
title={t('header.themeToggle')}
|
||||
>
|
||||
<i id="theme-icon" class="fas fa-desktop text-sm text-[var(--text-secondary)]"></i>
|
||||
<span id="theme-toggle-label" class="sr-only">
|
||||
{t('header.themeSystem')}
|
||||
</span>
|
||||
</button>
|
||||
<ThemeToggle
|
||||
client:load
|
||||
labels={{
|
||||
toggle: t('header.themeToggle'),
|
||||
system: t('header.themeSystem'),
|
||||
light: t('header.themeLight'),
|
||||
dark: t('header.themeDark'),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -233,7 +233,7 @@ const currentNavLabel =
|
||||
placeholder={t('header.searchPlaceholderKeyword')}
|
||||
class="terminal-console-input"
|
||||
/>
|
||||
<button id="mobile-search-btn" class="terminal-toolbar-iconbtn">
|
||||
<button id="mobile-search-btn" class="terminal-toolbar-iconbtn" aria-label="Search">
|
||||
<i id="mobile-search-btn-icon" class="fas fa-search text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -284,16 +284,16 @@ const currentNavLabel =
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="terminal-toolbar-label">{t('header.musicPanel')}</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<button id="music-prev" class="terminal-toolbar-iconbtn" disabled={!hasMusicPlaylist}>
|
||||
<button id="music-prev" class="terminal-toolbar-iconbtn" aria-label="Previous track" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-step-backward text-xs"></i>
|
||||
</button>
|
||||
<button id="music-play" class="terminal-toolbar-iconbtn bg-[var(--primary)]/12 text-[var(--primary)]" style="border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));" disabled={!hasMusicPlaylist}>
|
||||
<button id="music-play" class="terminal-toolbar-iconbtn bg-[var(--primary)]/12 text-[var(--primary)]" style="border-color: color-mix(in oklab, var(--primary) 28%, var(--border-color));" aria-label="Play or pause" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-play text-xs" id="music-play-icon"></i>
|
||||
</button>
|
||||
<button id="music-next" class="terminal-toolbar-iconbtn" disabled={!hasMusicPlaylist}>
|
||||
<button id="music-next" class="terminal-toolbar-iconbtn" aria-label="Next track" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-step-forward text-xs"></i>
|
||||
</button>
|
||||
<button id="music-volume" class="terminal-toolbar-iconbtn" disabled={!hasMusicPlaylist}>
|
||||
<button id="music-volume" class="terminal-toolbar-iconbtn" aria-label="Mute or unmute" disabled={!hasMusicPlaylist}>
|
||||
<i class="fas fa-volume-up text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -337,88 +337,9 @@ const currentNavLabel =
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<script is:inline define:vars={{ apiBase: API_BASE_URL, musicPlaylistPayload }}>
|
||||
<script is:inline define:vars={{ apiBase: publicApiBaseUrl, musicPlaylistPayload }}>
|
||||
const t = window.__termiTranslate;
|
||||
|
||||
// Theme selection
|
||||
function initThemeToggle() {
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const themeIcon = document.getElementById('theme-icon');
|
||||
const themeToggleLabel = document.getElementById('theme-toggle-label');
|
||||
const themeApi = window.__termiTheme;
|
||||
|
||||
if (!themeToggle || !themeIcon || !themeApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeToggle.dataset.bound === 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
themeToggle.dataset.bound = 'true';
|
||||
|
||||
const themeMeta = {
|
||||
light: {
|
||||
iconClass: 'fas fa-sun text-sm',
|
||||
color: 'var(--secondary)',
|
||||
label: t('header.themeLight'),
|
||||
},
|
||||
dark: {
|
||||
iconClass: 'fas fa-moon text-sm',
|
||||
color: 'var(--primary)',
|
||||
label: t('header.themeDark'),
|
||||
},
|
||||
system: {
|
||||
iconClass: 'fas fa-desktop text-sm',
|
||||
color: 'var(--text-secondary)',
|
||||
label: t('header.themeSystem'),
|
||||
},
|
||||
};
|
||||
|
||||
function updateThemeUI(detail = null) {
|
||||
const mode = detail?.mode || themeApi.getMode();
|
||||
const resolved = detail?.resolved || themeApi.resolveTheme(mode);
|
||||
const modeMeta = themeMeta[mode] || themeMeta.system;
|
||||
const modeLabel = themeMeta[mode]?.label || themeMeta.system.label;
|
||||
const resolvedLabel = resolved === 'dark' ? t('header.themeDark') : t('header.themeLight');
|
||||
|
||||
themeIcon.className = modeMeta.iconClass;
|
||||
themeIcon.style.color = modeMeta.color;
|
||||
|
||||
if (themeToggleLabel) {
|
||||
themeToggleLabel.textContent = `${modeLabel} / ${resolvedLabel}`;
|
||||
}
|
||||
|
||||
const toggleTitle = `${t('header.themeToggle')} · ${modeLabel} / ${resolvedLabel}`;
|
||||
themeToggle.setAttribute('aria-label', toggleTitle);
|
||||
themeToggle.setAttribute('title', toggleTitle);
|
||||
}
|
||||
|
||||
themeToggle.addEventListener('click', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const currentMode = themeApi.getMode();
|
||||
const nextMode =
|
||||
currentMode === 'system' ? 'light' :
|
||||
currentMode === 'light' ? 'dark' :
|
||||
'system';
|
||||
themeApi.applyTheme(nextMode);
|
||||
});
|
||||
|
||||
window.addEventListener('termi:theme-change', function(event) {
|
||||
updateThemeUI(event.detail);
|
||||
});
|
||||
|
||||
updateThemeUI(themeApi.syncTheme());
|
||||
}
|
||||
|
||||
// Run immediately if DOM is ready, otherwise wait
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initThemeToggle);
|
||||
} else {
|
||||
initThemeToggle();
|
||||
}
|
||||
|
||||
// Site Menu
|
||||
const mobileMenuBtn = document.getElementById('mobile-menu-btn');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
@@ -689,7 +610,7 @@ const currentNavLabel =
|
||||
return buildLocalizedUrl(
|
||||
currentSearchMode === 'ai'
|
||||
? `/ask?q=${encodeURIComponent(query)}`
|
||||
: `/articles?search=${encodeURIComponent(query)}`
|
||||
: `/search?q=${encodeURIComponent(query)}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -845,7 +766,7 @@ const currentNavLabel =
|
||||
${escapeHtml(t('header.liveResults'))}
|
||||
</div>
|
||||
${itemsHtml}
|
||||
<a href="${buildLocalizedUrl(`/articles?search=${encodeURIComponent(query)}`)}" class="block px-4 py-3 text-sm font-medium text-[var(--primary)] transition-colors hover:bg-[var(--header-bg)]">
|
||||
<a href="${buildLocalizedUrl(`/search?q=${encodeURIComponent(query)}`)}" class="block px-4 py-3 text-sm font-medium text-[var(--primary)] transition-colors hover:bg-[var(--header-bg)]">
|
||||
${escapeHtml(t('header.searchAllResults'))}
|
||||
</a>
|
||||
${aiFooter}
|
||||
|
||||
Reference in New Issue
Block a user