Serve engine-tagged wiki pages on a new /engines section
Frontend: /engines index + /engines/:slug detail routes, nav menu item and en/hu translations. Engine pages are few, so the index uses an emphasized poster-style design (dark slate, emerald accents, numbered full-width cards with content preview) instead of the blog/howtos layouts. Slugs are the last wiki path segment, since engine pages live scattered in the wiki tree. API: /api/rss/engines feed linking to the site's engine pages, and a WikiService#pages alias for #index — RssService called the alias-less name, so the blog and howtos feeds were raising NoMethodError.
This commit is contained in:
@@ -24,4 +24,11 @@ class Api::RssController < ApiController
|
||||
xml = RssService.new.howtos_feed
|
||||
render xml: xml, content_type: "application/rss+xml"
|
||||
end
|
||||
|
||||
api :GET, "/api/rss/engines", "Engines RSS feed"
|
||||
returns code: 200, desc: "RSS XML feed of in-house engines"
|
||||
def engines
|
||||
xml = RssService.new.engines_feed
|
||||
render xml: xml, content_type: "application/rss+xml"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ class Api::WikiController < ApiController
|
||||
end
|
||||
|
||||
api :GET, "/api/wiki/pages", "List wiki pages filtered by tag"
|
||||
param :tag, String, required: false, desc: "Filter by tag (blog, howto)"
|
||||
param :tag, String, required: false, desc: "Filter by tag (blog, howto, engine)"
|
||||
param :limit, :number, required: false, desc: "Limit number of results"
|
||||
param :body, String, required: false, desc: "Include body content (1 = yes)"
|
||||
returns code: 200, desc: "Wiki pages response" do
|
||||
|
||||
@@ -50,6 +50,28 @@ class RssService
|
||||
end.to_s
|
||||
end
|
||||
|
||||
def engines_feed
|
||||
pages = WikiService.new.pages(tag: "engine", limit: 30)
|
||||
items = pages.fetch("pages", [])
|
||||
|
||||
RSS::Maker.make("2.0") do |maker|
|
||||
maker.channel.title = "Teletype Games Engines"
|
||||
maker.channel.link = "#{SITE_URL}/engines"
|
||||
maker.channel.description = "In-house engines and frameworks from Teletype Games"
|
||||
maker.channel.language = "hu"
|
||||
|
||||
items.each do |page|
|
||||
maker.items.new_item do |item|
|
||||
slug = page["path"].to_s.split("/").last
|
||||
item.title = page["title"]
|
||||
item.link = "#{SITE_URL}/engines/#{slug}"
|
||||
item.description = page["description"].to_s
|
||||
item.pubDate = Time.parse(page["createdAt"]) rescue Time.current
|
||||
end
|
||||
end
|
||||
end.to_s
|
||||
end
|
||||
|
||||
def howtos_feed
|
||||
pages = WikiService.new.pages(tag: "howto", limit: 30)
|
||||
items = pages.fetch("pages", [])
|
||||
|
||||
@@ -43,4 +43,7 @@ class WikiService
|
||||
rescue StandardError => e
|
||||
{ "tag" => tag, "count" => 0, "pages" => [], "error" => e.message }
|
||||
end
|
||||
|
||||
# Az RSS feedek ezen a néven hívják.
|
||||
alias_method :pages, :index
|
||||
end
|
||||
|
||||
@@ -11,6 +11,7 @@ Rails.application.routes.draw do
|
||||
get "rss/blog", to: "rss#blog"
|
||||
get "rss/releases", to: "rss#releases"
|
||||
get "rss/howtos", to: "rss#howtos"
|
||||
get "rss/engines", to: "rss#engines"
|
||||
end
|
||||
|
||||
# Utolsó sor: a host route-jai nyernek, a katalógus-útvonalakat
|
||||
|
||||
@@ -67,6 +67,42 @@ const getBlogPage = async (slug: string): Promise<WikiPageContent | null> => {
|
||||
}
|
||||
}
|
||||
|
||||
// Engine pages live scattered in the wiki tree (infrastructure/, others/, …),
|
||||
// so the site slug is the last path segment: others/rubbs -> rubbs.
|
||||
const engineSlug = (path: string): string => path.split('/').filter(Boolean).pop() ?? path
|
||||
|
||||
const listEnginePages = async (): Promise<WikiPageWithContent[]> => {
|
||||
const pages = await fetchPages('engine', { body: true })
|
||||
return pages.map((p): WikiPageWithContent => ({
|
||||
id: p.id,
|
||||
path: p.path,
|
||||
title: p.title || p.path,
|
||||
description: p.description ?? '',
|
||||
content: p.content ?? '',
|
||||
updatedAt: p.updatedAt,
|
||||
createdAt: p.createdAt,
|
||||
locale: p.locale,
|
||||
}))
|
||||
}
|
||||
|
||||
const getEnginePage = async (slug: string): Promise<WikiPageContent | null> => {
|
||||
const pages = await fetchPages('engine', { body: true })
|
||||
|
||||
const matched = pages.find((p) => engineSlug(p.path) === slug)
|
||||
if (!matched) return null
|
||||
|
||||
return {
|
||||
id: matched.id,
|
||||
path: matched.path,
|
||||
title: matched.title || matched.path,
|
||||
description: matched.description ?? '',
|
||||
render: matched.render ?? '',
|
||||
updatedAt: matched.updatedAt,
|
||||
createdAt: matched.createdAt,
|
||||
locale: matched.locale,
|
||||
}
|
||||
}
|
||||
|
||||
const listHowtoPages = async (): Promise<WikiPage[]> => {
|
||||
const pages = await fetchPages('howto', { limit: 30 })
|
||||
return pages.map((p): WikiPage => ({
|
||||
@@ -80,5 +116,5 @@ const listHowtoPages = async (): Promise<WikiPage[]> => {
|
||||
}))
|
||||
}
|
||||
|
||||
export { WIKI_BASE }
|
||||
export default { listBlogPages, getBlogPage, listHowtoPages }
|
||||
export { WIKI_BASE, engineSlug }
|
||||
export default { listBlogPages, getBlogPage, listHowtoPages, listEnginePages, getEnginePage }
|
||||
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
catalog: 'Catalog',
|
||||
blog: 'Blog',
|
||||
howtos: 'How-tos',
|
||||
engines: 'Engines',
|
||||
code: 'Code',
|
||||
team: 'Team',
|
||||
contact: 'Contact us',
|
||||
@@ -168,6 +169,20 @@ export default {
|
||||
title: 'Our Team',
|
||||
subtitle: 'Meet the brilliant minds behind Teletype Games.',
|
||||
},
|
||||
engines: {
|
||||
badge: 'In-house Tech',
|
||||
titleLead: 'Our',
|
||||
titleAccent: 'Engines',
|
||||
subtitle: 'The engines and frameworks we build, maintain and ship our games and services on.',
|
||||
errorTitle: 'Failed to connect to Wiki',
|
||||
noPagesTitle: 'No engines found',
|
||||
noPagesDesc: "It seems like there aren't any engine pages available on the wiki at the moment.",
|
||||
new: 'New',
|
||||
explore: 'Explore',
|
||||
openWiki: 'Open in Wiki',
|
||||
back: 'Back to Engines',
|
||||
noContent: 'No content available for this engine.',
|
||||
},
|
||||
howtos: {
|
||||
badge: 'Knowledge Base',
|
||||
title: 'Tech HowTo Center',
|
||||
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
catalog: 'Katalógus',
|
||||
blog: 'Blog',
|
||||
howtos: 'Hogyan csináld',
|
||||
engines: 'Engine-ek',
|
||||
code: 'Kód',
|
||||
team: 'Csapat',
|
||||
contact: 'Kapcsolat',
|
||||
@@ -168,6 +169,20 @@ export default {
|
||||
title: 'Csapatunk',
|
||||
subtitle: 'Ismerd meg a Teletype Games mögött álló zseniális elméket.',
|
||||
},
|
||||
engines: {
|
||||
badge: 'Saját technológia',
|
||||
titleLead: 'Saját',
|
||||
titleAccent: 'Engine-jeink',
|
||||
subtitle: 'Az általunk épített és karbantartott engine-ek és keretrendszerek, amelyekre a játékaink és szolgáltatásaink épülnek.',
|
||||
errorTitle: 'Nem sikerült csatlakozni a Wikihez',
|
||||
noPagesTitle: 'Nem találhatók engine-ek',
|
||||
noPagesDesc: 'Úgy tűnik, jelenleg nincsenek engine-oldalak a wikin.',
|
||||
new: 'Új',
|
||||
explore: 'Felfedezés',
|
||||
openWiki: 'Megnyitás a Wikiben',
|
||||
back: 'Vissza az Engine-ekhez',
|
||||
noContent: 'Ehhez az engine-hez nincs elérhető tartalom.',
|
||||
},
|
||||
howtos: {
|
||||
badge: 'Tudásbázis',
|
||||
title: 'Tech HowTo Központ',
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<RouterLink to="/catalog" class="nav-link"><i class="fa-solid fa-gamepad nav-icon"></i>{{ t('nav.catalog') }}</RouterLink>
|
||||
<RouterLink to="/blog" class="nav-link"><i class="fa-solid fa-newspaper nav-icon"></i>{{ t('nav.blog') }}</RouterLink>
|
||||
<RouterLink to="/howtos" class="nav-link"><i class="fa-solid fa-lightbulb nav-icon"></i>{{ t('nav.howtos') }}</RouterLink>
|
||||
<RouterLink to="/engines" class="nav-link"><i class="fa-solid fa-cubes nav-icon"></i>{{ t('nav.engines') }}</RouterLink>
|
||||
<RouterLink to="/code" class="nav-link"><i class="fa-solid fa-code nav-icon"></i>{{ t('nav.code') }}</RouterLink>
|
||||
<RouterLink to="/team" class="nav-link"><i class="fa-solid fa-users nav-icon"></i>{{ t('nav.team') }}</RouterLink>
|
||||
<RouterLink to="/contact" class="nav-link"><i class="fa-solid fa-envelope nav-icon"></i>{{ t('nav.contact') }}</RouterLink>
|
||||
@@ -33,6 +34,7 @@
|
||||
<RouterLink to="/catalog" class="nav-mobile-link"><i class="fa-solid fa-gamepad nav-icon"></i>{{ t('nav.catalog') }}</RouterLink>
|
||||
<RouterLink to="/blog" class="nav-mobile-link"><i class="fa-solid fa-newspaper nav-icon"></i>{{ t('nav.blog') }}</RouterLink>
|
||||
<RouterLink to="/howtos" class="nav-mobile-link"><i class="fa-solid fa-lightbulb nav-icon"></i>{{ t('nav.howtos') }}</RouterLink>
|
||||
<RouterLink to="/engines" class="nav-mobile-link"><i class="fa-solid fa-cubes nav-icon"></i>{{ t('nav.engines') }}</RouterLink>
|
||||
<RouterLink to="/code" class="nav-mobile-link"><i class="fa-solid fa-code nav-icon"></i>{{ t('nav.code') }}</RouterLink>
|
||||
<RouterLink to="/team" class="nav-mobile-link"><i class="fa-solid fa-users nav-icon"></i>{{ t('nav.team') }}</RouterLink>
|
||||
<RouterLink to="/contact" class="nav-mobile-link"><i class="fa-solid fa-envelope nav-icon"></i>{{ t('nav.contact') }}</RouterLink>
|
||||
@@ -66,6 +68,10 @@
|
||||
<a href="/api/rss/howtos" target="_blank" class="rss-link" title="HowTos RSS">
|
||||
<i class="fa-solid fa-rss"></i> HowTos
|
||||
</a>
|
||||
<span class="locale-separator">|</span>
|
||||
<a href="/api/rss/engines" target="_blank" class="rss-link" title="Engines RSS">
|
||||
<i class="fa-solid fa-rss"></i> Engines
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="engine-show-container">
|
||||
<header class="engine-show-header">
|
||||
<div class="engine-show-header-decor">
|
||||
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-emerald-600"></div>
|
||||
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-teal-600"></div>
|
||||
</div>
|
||||
|
||||
<div class="engine-hero-container">
|
||||
<RouterLink to="/engines" class="back-link"><i class="fa-solid fa-arrow-left mr-1"></i> {{ t('engines.back') }}</RouterLink>
|
||||
<template v-if="pageContent">
|
||||
<h1 class="hero-title mt-4">{{ pageContent.title }}</h1>
|
||||
<p class="hero-subtitle mb-6">{{ pageContent.description }}</p>
|
||||
<div class="engine-show-meta">
|
||||
<span><i class="fa-solid fa-clock-rotate-left mr-1"></i>{{ formatDateTime(pageContent.updatedAt) }}</span>
|
||||
<a :href="`${WIKI_BASE}/${pageContent.path}`" target="_blank" rel="noopener" class="engine-show-wiki-link">
|
||||
<i class="fa-solid fa-book mr-1"></i>{{ t('engines.openWiki') }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="engine-show-main">
|
||||
<div v-if="error" class="error-banner" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation text-2xl text-yellow-500"></i>
|
||||
<div>
|
||||
<h3 class="error-banner-title">{{ t('engines.errorTitle') }}</h3>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<article v-else-if="pageContent" class="engine-doc-card">
|
||||
<div class="engine-doc-content">
|
||||
<div v-if="pageContent.render" class="wiki-content max-w-none text-gray-700 leading-relaxed" v-html="sanitize(pageContent.render)" />
|
||||
<div v-else class="engine-doc-fallback">{{ t('engines.noContent') }}</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<SkeletonCard v-else-if="loading" :count="1" />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import { sanitize } from '../../lib/sanitize'
|
||||
import { WIKI_BASE } from '../../api/wiki.api'
|
||||
import { useEnginesStore } from '../../stores/engines.store'
|
||||
import SkeletonCard from '../../components/SkeletonCard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const route = useRoute()
|
||||
const store = useEnginesStore()
|
||||
const { currentPage: pageContent, pageLoading: loading, pageError: error } = storeToRefs(store)
|
||||
|
||||
onMounted(() => store.fetchPage(route.params.slug as string))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.engine-show-container {
|
||||
@apply bg-slate-950 min-h-screen pb-20;
|
||||
}
|
||||
.engine-show-header {
|
||||
@apply relative overflow-hidden py-24 text-white text-center bg-slate-900;
|
||||
}
|
||||
.engine-show-header-decor {
|
||||
@apply absolute inset-0 opacity-30;
|
||||
}
|
||||
.engine-hero-container {
|
||||
@apply relative z-10 max-w-4xl mx-auto px-4;
|
||||
}
|
||||
.back-link {
|
||||
@apply inline-block text-emerald-300 hover:text-white transition-colors font-medium;
|
||||
}
|
||||
.hero-title {
|
||||
@apply text-4xl md:text-5xl font-black mb-4 leading-tight;
|
||||
}
|
||||
.hero-subtitle {
|
||||
@apply text-xl text-emerald-100 font-medium;
|
||||
}
|
||||
.engine-show-meta {
|
||||
@apply flex items-center justify-center gap-6 text-sm font-semibold uppercase tracking-wider text-slate-400;
|
||||
}
|
||||
.engine-show-wiki-link {
|
||||
@apply text-emerald-400 hover:text-emerald-300 transition-colors normal-case tracking-normal;
|
||||
}
|
||||
.engine-show-main {
|
||||
@apply max-w-4xl mx-auto px-4 md:px-8 -mt-12 relative z-10;
|
||||
}
|
||||
.engine-doc-card {
|
||||
@apply bg-white rounded-2xl shadow-xl border border-slate-800 overflow-hidden;
|
||||
}
|
||||
.engine-doc-content {
|
||||
@apply p-6 md:p-12;
|
||||
}
|
||||
.engine-doc-fallback {
|
||||
@apply italic text-gray-400;
|
||||
}
|
||||
.error-banner {
|
||||
@apply max-w-7xl mx-auto bg-red-50 border-l-4 border-red-500 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4;
|
||||
}
|
||||
.error-banner-title { @apply text-red-800 font-bold text-lg; }
|
||||
.error-banner-desc { @apply text-red-700 mt-1; }
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="engines-container">
|
||||
<header class="hero-section-slate">
|
||||
<div class="engines-header-decor">
|
||||
<div class="hero-decor-blob -top-24 -left-24 h-96 w-96 bg-emerald-600"></div>
|
||||
<div class="hero-decor-blob -bottom-24 -right-24 h-96 w-96 bg-teal-600"></div>
|
||||
</div>
|
||||
|
||||
<div class="hero-container">
|
||||
<div class="hero-badge">{{ t('engines.badge') }}</div>
|
||||
<h1 class="hero-title">
|
||||
{{ t('engines.titleLead') }} <span class="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 to-teal-300">{{ t('engines.titleAccent') }}</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle mb-10">{{ t('engines.subtitle') }}</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="engines-main">
|
||||
<div v-if="error" class="error-banner" role="alert">
|
||||
<i class="fa-solid fa-triangle-exclamation text-2xl text-yellow-500"></i>
|
||||
<div>
|
||||
<h3 class="error-banner-title">{{ t('engines.errorTitle') }}</h3>
|
||||
<p class="error-banner-desc">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SkeletonCard v-else-if="loading" :count="3" />
|
||||
|
||||
<div v-else-if="enginePages.length === 0" class="empty-state">
|
||||
<div class="empty-state-icon"><i class="fa-solid fa-inbox"></i></div>
|
||||
<h2 class="empty-state-title">{{ t('engines.noPagesTitle') }}</h2>
|
||||
<p class="empty-state-desc">{{ t('engines.noPagesDesc') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="engine-list">
|
||||
<article v-for="(page, index) in enginePages" :key="page.id" class="engine-card group">
|
||||
<div class="engine-card-index">{{ String(index + 1).padStart(2, '0') }}</div>
|
||||
|
||||
<div class="engine-card-body">
|
||||
<div class="engine-card-meta">
|
||||
<span v-if="isNew(page.createdAt)" class="engine-new-badge">
|
||||
<span class="engine-new-dot"></span>
|
||||
{{ t('engines.new') }}
|
||||
</span>
|
||||
<span class="engine-card-updated">
|
||||
<i class="fa-solid fa-clock-rotate-left mr-1 opacity-60"></i>{{ formatDateTime(page.updatedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 class="engine-card-title">
|
||||
<RouterLink :to="getPermalink(page.path)">{{ page.title }}</RouterLink>
|
||||
</h2>
|
||||
|
||||
<p v-if="page.description" class="engine-card-desc">{{ page.description }}</p>
|
||||
<p v-if="page.content" class="engine-card-preview">{{ getCleanPreview(page.content) }}</p>
|
||||
|
||||
<div class="engine-card-actions">
|
||||
<RouterLink :to="getPermalink(page.path)" class="engine-explore-btn">
|
||||
{{ t('engines.explore') }}
|
||||
<i class="fa-solid fa-arrow-right text-sm"></i>
|
||||
</RouterLink>
|
||||
<a :href="`${WIKI_BASE}/${page.path}`" target="_blank" rel="noopener" class="engine-wiki-link">
|
||||
<i class="fa-solid fa-book mr-1"></i>{{ t('engines.openWiki') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import { WIKI_BASE } from '../../api/wiki.api'
|
||||
import { useEnginesStore } from '../../stores/engines.store'
|
||||
import SkeletonCard from '../../components/SkeletonCard.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const store = useEnginesStore()
|
||||
const { pages: enginePages, loading, error } = storeToRefs(store)
|
||||
const { isNew, getPermalink, getCleanPreview } = store
|
||||
|
||||
onMounted(() => store.fetch())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.engines-container {
|
||||
@apply bg-slate-950 min-h-screen pb-24;
|
||||
}
|
||||
.engines-header-decor {
|
||||
@apply absolute inset-0 opacity-30;
|
||||
}
|
||||
.engines-main {
|
||||
@apply max-w-5xl mx-auto px-4 md:px-8 mt-16 relative z-10;
|
||||
}
|
||||
.banner-base {
|
||||
@apply max-w-7xl mx-auto border-l-4 p-6 rounded-r-xl shadow-lg mb-12 flex items-start gap-4;
|
||||
}
|
||||
.error-banner { @apply banner-base bg-red-50 border-red-500; }
|
||||
.error-banner-title { @apply text-red-800 font-bold text-lg; }
|
||||
.error-banner-desc { @apply text-red-700 mt-1; }
|
||||
.empty-state {
|
||||
@apply max-w-7xl mx-auto bg-slate-900 rounded-2xl shadow-xl p-12 text-center border border-slate-800;
|
||||
}
|
||||
.empty-state-icon { @apply text-6xl mb-4; }
|
||||
.empty-state-title { @apply text-2xl font-bold text-white mb-2; }
|
||||
.empty-state-desc { @apply text-slate-400 max-w-md mx-auto; }
|
||||
|
||||
/* Few engines, so every one of them gets a full-width, poster-like card. */
|
||||
.engine-list {
|
||||
@apply flex flex-col gap-10;
|
||||
}
|
||||
.engine-card {
|
||||
@apply relative overflow-hidden bg-slate-900 rounded-3xl border border-slate-800 border-l-4 border-l-emerald-500 shadow-2xl transition-all hover:border-l-teal-300 hover:-translate-y-1;
|
||||
}
|
||||
.engine-card-index {
|
||||
@apply absolute -top-6 right-4 text-[9rem] leading-none font-black text-slate-800/60 select-none pointer-events-none transition-colors;
|
||||
}
|
||||
.engine-card:hover .engine-card-index {
|
||||
@apply text-slate-800;
|
||||
}
|
||||
.engine-card-body {
|
||||
@apply relative p-8 md:p-12;
|
||||
}
|
||||
.engine-card-meta {
|
||||
@apply flex items-center gap-3 mb-5 text-xs font-semibold uppercase tracking-wider text-slate-500;
|
||||
}
|
||||
.engine-new-badge {
|
||||
@apply flex items-center gap-1 text-emerald-400 bg-emerald-950 px-2 py-0.5 rounded-full border border-emerald-800;
|
||||
}
|
||||
.engine-new-dot {
|
||||
@apply w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse;
|
||||
}
|
||||
.engine-card-updated {
|
||||
@apply text-slate-500;
|
||||
}
|
||||
.engine-card-title {
|
||||
@apply text-3xl md:text-5xl font-black text-white mb-4 leading-tight;
|
||||
}
|
||||
.engine-card-title a {
|
||||
@apply hover:text-emerald-400 transition-colors;
|
||||
}
|
||||
.engine-card-desc {
|
||||
@apply text-lg md:text-xl text-emerald-100/90 font-semibold mb-3 leading-relaxed max-w-3xl;
|
||||
}
|
||||
.engine-card-preview {
|
||||
@apply text-base text-slate-400 leading-relaxed mb-8 max-w-3xl;
|
||||
}
|
||||
.engine-card-actions {
|
||||
@apply flex flex-wrap items-center gap-6;
|
||||
}
|
||||
.engine-explore-btn {
|
||||
@apply inline-flex items-center gap-2 bg-emerald-500 text-slate-950 px-6 py-3 rounded-xl font-bold hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-900/40 group-hover:translate-x-1;
|
||||
}
|
||||
.engine-wiki-link {
|
||||
@apply text-slate-400 font-semibold hover:text-white transition-colors;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const enginesRouter: RouteRecordRaw[] = [
|
||||
{ path: '/engines', name: 'enginesIndex', component: () => import('../page/engines/EnginesIndexPage.vue') },
|
||||
{ path: '/engines/:slug', name: 'engineShow', component: () => import('../page/engines/EngineShowPage.vue') },
|
||||
]
|
||||
@@ -4,6 +4,7 @@ import { blogRouter } from './blog.router'
|
||||
import { catalogRouter } from './catalog.router'
|
||||
import { codeRouter } from './code.router'
|
||||
import { contactRouter } from './contact.router'
|
||||
import { enginesRouter } from './engines.router'
|
||||
import { howtosRouter } from './howtos.router'
|
||||
import { teamRouter } from './team.router'
|
||||
import { buildsRouter } from './builds.router'
|
||||
@@ -16,6 +17,7 @@ export const router = createRouter({
|
||||
...catalogRouter,
|
||||
...codeRouter,
|
||||
...contactRouter,
|
||||
...enginesRouter,
|
||||
...howtosRouter,
|
||||
...teamRouter,
|
||||
...buildsRouter,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import wikiApi, { engineSlug } from '../api/wiki.api'
|
||||
import { isNew } from '../lib/softwareUtils'
|
||||
import { useLoadable } from '../composables/useLoadable'
|
||||
import type { WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
export const useEnginesStore = defineStore('engines', () => {
|
||||
const pages = ref<WikiPageWithContent[]>([])
|
||||
const { loading, error, withCache, invalidate } = useLoadable()
|
||||
|
||||
const currentPage = ref<WikiPageContent | null>(null)
|
||||
const { loading: pageLoading, error: pageError, withCache: withPageCache } = useLoadable(0)
|
||||
|
||||
async function fetch() {
|
||||
await withCache(async () => {
|
||||
pages.value = await wikiApi.listEnginePages()
|
||||
})
|
||||
}
|
||||
|
||||
async function fetchPage(slug: string) {
|
||||
currentPage.value = null
|
||||
await withPageCache(async () => {
|
||||
const result = await wikiApi.getEnginePage(slug)
|
||||
if (!result) throw new Error('Engine not found')
|
||||
currentPage.value = result
|
||||
})
|
||||
}
|
||||
|
||||
function getPermalink(path: string): string {
|
||||
return `/engines/${engineSlug(path)}`
|
||||
}
|
||||
|
||||
function getCleanPreview(content: string): string {
|
||||
if (!content) return ''
|
||||
return content.replace(/[#*`_[\]()>|-]/g, '').replace(/\s+/g, ' ').trim().slice(0, 260) + '...'
|
||||
}
|
||||
|
||||
return { pages, loading, error, currentPage, pageLoading, pageError, fetch, fetchPage, isNew, getPermalink, getCleanPreview, invalidate }
|
||||
})
|
||||
Reference in New Issue
Block a user