feat: Refactor service management scripts to use a unified dev script
- Added package.json to manage development scripts. - Updated restart-services.ps1 to call the new dev script for starting services. - Refactored start-admin.ps1, start-backend.ps1, start-frontend.ps1, and start-mcp.ps1 to utilize the dev script for starting respective services. - Enhanced stop-services.ps1 to improve process termination logic by matching command patterns.
This commit is contained in:
@@ -7,6 +7,7 @@ import PostCard from '../../components/PostCard.astro';
|
||||
import { api } from '../../lib/api/client';
|
||||
import { getI18n } from '../../lib/i18n';
|
||||
import type { Category, Post, Tag } from '../../lib/types';
|
||||
import { getAccentVars, getCategoryTheme, getPostTypeTheme, getTagTheme } from '../../lib/utils';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
@@ -60,9 +61,22 @@ const postTypeFilters = [
|
||||
{ id: 'tweet', name: t('common.tweet'), icon: 'fa-comment-dots' }
|
||||
];
|
||||
|
||||
const typePromptCommand = `./filter --type ${selectedType || 'all'}`;
|
||||
const categoryPromptCommand = `./filter --category ${selectedCategory ? `"${selectedCategory}"` : 'all'}`;
|
||||
const tagPromptCommand = `./filter --tag ${selectedTag ? `"${selectedTag}"` : 'all'}`;
|
||||
const typePromptCommand =
|
||||
selectedType === 'all'
|
||||
? `grep -E "^type: (article|tweet)$" ./posts/*.md`
|
||||
: `grep -E "^type: ${selectedType}$" ./posts/*.md`;
|
||||
const categoryPromptCommand = selectedCategory
|
||||
? `grep -El "^category: ${selectedCategory}$" ./posts/*.md`
|
||||
: `cut -d: -f2 ./categories.index | sort -u`;
|
||||
const tagPromptCommand = selectedTag
|
||||
? `grep -Ril "#${selectedTag}" ./posts`
|
||||
: `cut -d: -f2 ./tags.index | sort -u`;
|
||||
const categoryAccentMap = Object.fromEntries(
|
||||
allCategories.map((category) => [category.name.toLowerCase(), getAccentVars(getCategoryTheme(category.name))])
|
||||
);
|
||||
const tagAccentMap = Object.fromEntries(
|
||||
allTags.map((tag) => [String(tag.slug || tag.name).toLowerCase(), getAccentVars(getTagTheme(tag.name))])
|
||||
);
|
||||
|
||||
const buildArticlesUrl = ({
|
||||
type = selectedType,
|
||||
@@ -94,7 +108,7 @@ const buildArticlesUrl = ({
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<TerminalWindow title="~/articles/index" class="w-full">
|
||||
<div class="px-4 pb-2">
|
||||
<CommandPrompt command="fd . ./content/posts --full-path" />
|
||||
<CommandPrompt command="find ./posts -type f -name '*.md' | sort" />
|
||||
|
||||
<div class="ml-4 mt-4 space-y-3">
|
||||
<h1 class="text-3xl font-bold tracking-tight text-[var(--title-color)]">{t('articlesPage.title')}</h1>
|
||||
@@ -104,7 +118,7 @@ const buildArticlesUrl = ({
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-file-lines text-[var(--primary)]"></i>
|
||||
{t('articlesPage.totalPosts', { count: filteredPosts.length })}
|
||||
<span id="articles-total-posts">{t('articlesPage.totalPosts', { count: filteredPosts.length })}</span>
|
||||
</span>
|
||||
{selectedSearch && (
|
||||
<span class="terminal-stat-pill">
|
||||
@@ -112,31 +126,43 @@ const buildArticlesUrl = ({
|
||||
grep: {selectedSearch}
|
||||
</span>
|
||||
)}
|
||||
{selectedCategory && (
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-folder-open text-[var(--primary)]"></i>
|
||||
{selectedCategory}
|
||||
</span>
|
||||
)}
|
||||
{selectedTag && (
|
||||
<span class="terminal-stat-pill">
|
||||
<i class="fas fa-hashtag text-[var(--primary)]"></i>
|
||||
{selectedTag}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
id="articles-current-category-pill"
|
||||
class:list={[
|
||||
'terminal-stat-pill terminal-stat-pill--accent',
|
||||
!selectedCategory && 'hidden'
|
||||
]}
|
||||
style={selectedCategory ? getAccentVars(getCategoryTheme(selectedCategory)) : undefined}
|
||||
>
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<span id="articles-current-category">{selectedCategory}</span>
|
||||
</span>
|
||||
<span
|
||||
id="articles-current-tag-pill"
|
||||
class:list={[
|
||||
'terminal-stat-pill terminal-stat-pill--accent',
|
||||
!selectedTag && 'hidden'
|
||||
]}
|
||||
style={selectedTag ? getAccentVars(getTagTheme(selectedTag)) : undefined}
|
||||
>
|
||||
<i class="fas fa-hashtag"></i>
|
||||
<span id="articles-current-tag">{selectedTag}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pb-2 space-y-4">
|
||||
<div class="ml-4">
|
||||
<CommandPrompt command={typePromptCommand} typing={false} />
|
||||
<CommandPrompt promptId="articles-type-prompt" command={typePromptCommand} typing={false} />
|
||||
<div class="mt-3 flex flex-wrap gap-3">
|
||||
{postTypeFilters.map(filter => (
|
||||
<FilterPill
|
||||
href={buildArticlesUrl({ type: filter.id, page: 1 })}
|
||||
tone="blue"
|
||||
data-articles-type={filter.id}
|
||||
tone={filter.id === 'all' ? 'neutral' : 'accent'}
|
||||
active={selectedType === filter.id}
|
||||
style={filter.id === 'all' ? undefined : getAccentVars(getPostTypeTheme(filter.id))}
|
||||
>
|
||||
<i class={`fas ${filter.icon}`}></i>
|
||||
<span class="font-medium">{filter.name}</span>
|
||||
@@ -147,10 +173,11 @@ const buildArticlesUrl = ({
|
||||
|
||||
{allCategories.length > 0 && (
|
||||
<div class="ml-4">
|
||||
<CommandPrompt command={categoryPromptCommand} typing={false} />
|
||||
<CommandPrompt promptId="articles-category-prompt" command={categoryPromptCommand} typing={false} />
|
||||
<div class="mt-3 flex flex-wrap gap-3">
|
||||
<FilterPill
|
||||
href={buildArticlesUrl({ category: '', page: 1 })}
|
||||
data-articles-category=""
|
||||
tone="amber"
|
||||
active={!selectedCategory}
|
||||
>
|
||||
@@ -160,8 +187,10 @@ const buildArticlesUrl = ({
|
||||
{allCategories.map(category => (
|
||||
<FilterPill
|
||||
href={buildArticlesUrl({ category: category.name, page: 1 })}
|
||||
tone="amber"
|
||||
data-articles-category={category.name}
|
||||
tone="accent"
|
||||
active={selectedCategory.toLowerCase() === category.name.toLowerCase()}
|
||||
style={getAccentVars(getCategoryTheme(category.name))}
|
||||
>
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<span class="font-medium">{category.name}</span>
|
||||
@@ -174,10 +203,11 @@ const buildArticlesUrl = ({
|
||||
|
||||
{allTags.length > 0 && (
|
||||
<div class="ml-4">
|
||||
<CommandPrompt command={tagPromptCommand} typing={false} />
|
||||
<CommandPrompt promptId="articles-tag-prompt" command={tagPromptCommand} typing={false} />
|
||||
<div class="mt-3 flex flex-wrap gap-3">
|
||||
<FilterPill
|
||||
href={buildArticlesUrl({ tag: '', page: 1 })}
|
||||
data-articles-tag=""
|
||||
tone="teal"
|
||||
active={!selectedTag}
|
||||
>
|
||||
@@ -187,8 +217,10 @@ const buildArticlesUrl = ({
|
||||
{allTags.map(tag => (
|
||||
<FilterPill
|
||||
href={buildArticlesUrl({ tag: tag.slug || tag.name, page: 1 })}
|
||||
tone="teal"
|
||||
data-articles-tag={tag.slug || tag.name}
|
||||
tone="accent"
|
||||
active={isSelectedTag(tag)}
|
||||
style={getAccentVars(getTagTheme(tag.name))}
|
||||
>
|
||||
<i class="fas fa-hashtag"></i>
|
||||
<span class="font-medium">{tag.name}</span>
|
||||
@@ -200,14 +232,34 @@ const buildArticlesUrl = ({
|
||||
</div>
|
||||
|
||||
<div class="px-4">
|
||||
{paginatedPosts.length > 0 ? (
|
||||
{allPosts.length > 0 ? (
|
||||
<div class="ml-4 mt-4 space-y-4">
|
||||
{paginatedPosts.map(post => (
|
||||
<PostCard post={post} selectedTag={selectedTag} highlightTerm={selectedSearch} />
|
||||
))}
|
||||
{allPosts.map((post, index) => {
|
||||
const matchesCurrentFilter =
|
||||
(selectedType === 'all' || post.type === selectedType) &&
|
||||
(!selectedTag || post.tags?.some(isMatchingTag)) &&
|
||||
(!selectedCategory || post.category?.toLowerCase() === selectedCategory.toLowerCase());
|
||||
const filteredIndex = matchesCurrentFilter
|
||||
? filteredPosts.findIndex((item) => item.slug === post.slug)
|
||||
: -1;
|
||||
const isVisible = matchesCurrentFilter && filteredIndex >= startIndex && filteredIndex < startIndex + postsPerPage;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-article-card
|
||||
data-article-type={post.type}
|
||||
data-article-category={post.category?.toLowerCase() || ''}
|
||||
data-article-tags={post.tags.map((tag) => tag.trim().toLowerCase()).join('|')}
|
||||
data-article-index={index}
|
||||
class:list={[!isVisible && 'hidden']}
|
||||
>
|
||||
<PostCard post={post} selectedTag={selectedTag} highlightTerm={selectedSearch} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div class="terminal-empty ml-4 mt-4">
|
||||
) : null}
|
||||
<div id="articles-empty-state" class:list={['terminal-empty ml-4 mt-4', paginatedPosts.length > 0 && 'hidden']}>
|
||||
<div class="mx-auto flex max-w-md flex-col items-center gap-3">
|
||||
<span class="terminal-section-icon">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
@@ -222,38 +274,237 @@ const buildArticlesUrl = ({
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div class="px-4 py-6">
|
||||
<div class="terminal-panel-muted ml-4 mt-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="px-4 py-6">
|
||||
<div id="articles-pagination" class:list={['terminal-panel-muted ml-4 mt-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between', totalPages <= 1 && 'hidden']}>
|
||||
<span class="text-sm text-[var(--text-secondary)]">
|
||||
{t('articlesPage.pageSummary', { current: currentPage, total: totalPages, count: totalPosts })}
|
||||
<span id="articles-page-summary">{t('articlesPage.pageSummary', { current: currentPage, total: totalPages, count: totalPosts })}</span>
|
||||
</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{currentPage > 1 && (
|
||||
<a
|
||||
href={buildArticlesUrl({ page: currentPage - 1 })}
|
||||
class="terminal-action-button"
|
||||
<button
|
||||
id="articles-prev-btn"
|
||||
type="button"
|
||||
class:list={['terminal-action-button', currentPage <= 1 && 'hidden']}
|
||||
>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
<span>{t('articlesPage.previous')}</span>
|
||||
</a>
|
||||
)}
|
||||
{currentPage < totalPages && (
|
||||
<a
|
||||
href={buildArticlesUrl({ page: currentPage + 1 })}
|
||||
class="terminal-action-button terminal-action-button-primary"
|
||||
</button>
|
||||
<button
|
||||
id="articles-next-btn"
|
||||
type="button"
|
||||
class:list={['terminal-action-button terminal-action-button-primary', currentPage >= totalPages && 'hidden']}
|
||||
>
|
||||
<span>{t('articlesPage.next')}</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TerminalWindow>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script
|
||||
is:inline
|
||||
define:vars={{
|
||||
postsPerPage,
|
||||
selectedSearch,
|
||||
categoryAccentMap,
|
||||
tagAccentMap,
|
||||
initialArticlesState: {
|
||||
type: selectedType,
|
||||
category: selectedCategory,
|
||||
tag: selectedTag,
|
||||
page: currentPage,
|
||||
},
|
||||
}}
|
||||
>
|
||||
(function() {
|
||||
/** @type {Window['__termiCommandPrompt']} */
|
||||
let promptApi;
|
||||
|
||||
const articleCards = Array.from(document.querySelectorAll('[data-article-card]'));
|
||||
const typeFilters = Array.from(document.querySelectorAll('[data-articles-type]'));
|
||||
const categoryFilters = Array.from(document.querySelectorAll('[data-articles-category]'));
|
||||
const tagFilters = Array.from(document.querySelectorAll('[data-articles-tag]'));
|
||||
const totalPostsEl = document.getElementById('articles-total-posts');
|
||||
const categoryPill = document.getElementById('articles-current-category-pill');
|
||||
const categoryText = document.getElementById('articles-current-category');
|
||||
const tagPill = document.getElementById('articles-current-tag-pill');
|
||||
const tagText = document.getElementById('articles-current-tag');
|
||||
const emptyState = document.getElementById('articles-empty-state');
|
||||
const pagination = document.getElementById('articles-pagination');
|
||||
const pageSummary = document.getElementById('articles-page-summary');
|
||||
const prevBtn = document.getElementById('articles-prev-btn');
|
||||
const nextBtn = document.getElementById('articles-next-btn');
|
||||
const t = window.__termiTranslate;
|
||||
|
||||
promptApi = window.__termiCommandPrompt;
|
||||
|
||||
const state = {
|
||||
type: initialArticlesState.type || 'all',
|
||||
category: initialArticlesState.category || '',
|
||||
tag: initialArticlesState.tag || '',
|
||||
page: Number(initialArticlesState.page || 1),
|
||||
};
|
||||
|
||||
function updateArticlePrompts() {
|
||||
const typeCommand = state.type === 'all'
|
||||
? 'grep -E "^type: (article|tweet)$" ./posts/*.md'
|
||||
: `grep -E "^type: ${state.type}$" ./posts/*.md`;
|
||||
const categoryCommand = state.category
|
||||
? `grep -El "^category: ${state.category}$" ./posts/*.md`
|
||||
: 'cut -d: -f2 ./categories.index | sort -u';
|
||||
const tagCommand = state.tag
|
||||
? `grep -Ril "#${state.tag}" ./posts`
|
||||
: 'cut -d: -f2 ./tags.index | sort -u';
|
||||
|
||||
promptApi?.set?.('articles-type-prompt', typeCommand, { typing: false });
|
||||
promptApi?.set?.('articles-category-prompt', categoryCommand, { typing: false });
|
||||
promptApi?.set?.('articles-tag-prompt', tagCommand, { typing: false });
|
||||
}
|
||||
|
||||
function syncActiveFilters(elements, key, emptyValue = '') {
|
||||
elements.forEach((element) => {
|
||||
const value = (element.getAttribute(key) || '').trim();
|
||||
const activeValue =
|
||||
key === 'data-articles-type'
|
||||
? state.type
|
||||
: key === 'data-articles-category'
|
||||
? state.category
|
||||
: state.tag;
|
||||
element.classList.toggle('is-active', value === (activeValue || emptyValue));
|
||||
});
|
||||
}
|
||||
|
||||
function updateSummaryPills() {
|
||||
if (categoryPill && categoryText) {
|
||||
if (state.category) {
|
||||
categoryPill.classList.remove('hidden');
|
||||
categoryText.textContent = state.category;
|
||||
categoryPill.setAttribute('style', categoryAccentMap[String(state.category).toLowerCase()] || '');
|
||||
} else {
|
||||
categoryPill.classList.add('hidden');
|
||||
categoryPill.removeAttribute('style');
|
||||
}
|
||||
}
|
||||
|
||||
if (tagPill && tagText) {
|
||||
if (state.tag) {
|
||||
tagPill.classList.remove('hidden');
|
||||
tagText.textContent = state.tag;
|
||||
tagPill.setAttribute('style', tagAccentMap[String(state.tag).toLowerCase()] || '');
|
||||
} else {
|
||||
tagPill.classList.add('hidden');
|
||||
tagPill.removeAttribute('style');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateUrl(totalPages) {
|
||||
const params = new URLSearchParams();
|
||||
if (state.type && state.type !== 'all') params.set('type', state.type);
|
||||
if (selectedSearch) params.set('search', selectedSearch);
|
||||
if (state.tag) params.set('tag', state.tag);
|
||||
if (state.category) params.set('category', state.category);
|
||||
if (state.page > 1 && totalPages > 1) params.set('page', String(state.page));
|
||||
const nextUrl = params.toString() ? `/articles?${params.toString()}` : '/articles';
|
||||
window.history.replaceState({}, '', nextUrl);
|
||||
}
|
||||
|
||||
function applyArticleFilters(pushHistory = true) {
|
||||
const filtered = articleCards.filter((card) => {
|
||||
const type = card.getAttribute('data-article-type') || '';
|
||||
const category = (card.getAttribute('data-article-category') || '').toLowerCase();
|
||||
const tags = `|${(card.getAttribute('data-article-tags') || '').toLowerCase()}|`;
|
||||
const typeMatch = state.type === 'all' || type === state.type;
|
||||
const categoryMatch = !state.category || category === state.category.toLowerCase();
|
||||
const tagMatch = !state.tag || tags.includes(`|${state.tag.toLowerCase()}|`);
|
||||
return typeMatch && categoryMatch && tagMatch;
|
||||
});
|
||||
|
||||
const total = filtered.length;
|
||||
const totalPages = Math.max(Math.ceil(total / postsPerPage), 1);
|
||||
|
||||
if (state.page > totalPages) state.page = totalPages;
|
||||
if (state.page < 1) state.page = 1;
|
||||
|
||||
const startIndex = (state.page - 1) * postsPerPage;
|
||||
const endIndex = startIndex + postsPerPage;
|
||||
|
||||
articleCards.forEach((card) => card.classList.add('hidden'));
|
||||
filtered.slice(startIndex, endIndex).forEach((card) => card.classList.remove('hidden'));
|
||||
|
||||
syncActiveFilters(typeFilters, 'data-articles-type', 'all');
|
||||
syncActiveFilters(categoryFilters, 'data-articles-category', '');
|
||||
syncActiveFilters(tagFilters, 'data-articles-tag', '');
|
||||
updateSummaryPills();
|
||||
updateArticlePrompts();
|
||||
|
||||
if (totalPostsEl) {
|
||||
totalPostsEl.textContent = t('articlesPage.totalPosts', { count: total });
|
||||
}
|
||||
|
||||
if (emptyState) {
|
||||
emptyState.classList.toggle('hidden', total > 0);
|
||||
}
|
||||
|
||||
if (pagination) {
|
||||
pagination.classList.toggle('hidden', totalPages <= 1);
|
||||
}
|
||||
if (pageSummary) {
|
||||
pageSummary.textContent = t('articlesPage.pageSummary', { current: state.page, total: totalPages, count: total });
|
||||
}
|
||||
if (prevBtn) {
|
||||
prevBtn.classList.toggle('hidden', state.page <= 1 || totalPages <= 1);
|
||||
}
|
||||
if (nextBtn) {
|
||||
nextBtn.classList.toggle('hidden', state.page >= totalPages || totalPages <= 1);
|
||||
}
|
||||
|
||||
if (pushHistory) {
|
||||
updateUrl(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
typeFilters.forEach((filter) => {
|
||||
filter.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
state.type = filter.getAttribute('data-articles-type') || 'all';
|
||||
state.page = 1;
|
||||
applyArticleFilters();
|
||||
});
|
||||
});
|
||||
|
||||
categoryFilters.forEach((filter) => {
|
||||
filter.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
state.category = filter.getAttribute('data-articles-category') || '';
|
||||
state.page = 1;
|
||||
applyArticleFilters();
|
||||
});
|
||||
});
|
||||
|
||||
tagFilters.forEach((filter) => {
|
||||
filter.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
state.tag = filter.getAttribute('data-articles-tag') || '';
|
||||
state.page = 1;
|
||||
applyArticleFilters();
|
||||
});
|
||||
});
|
||||
|
||||
prevBtn?.addEventListener('click', () => {
|
||||
state.page -= 1;
|
||||
applyArticleFilters();
|
||||
});
|
||||
|
||||
nextBtn?.addEventListener('click', () => {
|
||||
state.page += 1;
|
||||
applyArticleFilters();
|
||||
});
|
||||
|
||||
applyArticleFilters(false);
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user