custom css classes

This commit is contained in:
2026-08-04 07:08:08 +02:00
parent e5f714c34e
commit f943357ebd
7 changed files with 420 additions and 168 deletions
+21 -6
View File
@@ -1,11 +1,11 @@
<template>
<div class="animate-pulse">
<div v-for="n in count" :key="n" class="bg-white rounded-2xl shadow-xl border border-gray-100 overflow-hidden mb-6">
<div class="p-6 md:p-8">
<div class="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
<div class="h-6 bg-gray-200 rounded w-3/4 mb-3"></div>
<div class="h-4 bg-gray-200 rounded w-full mb-2"></div>
<div class="h-4 bg-gray-200 rounded w-5/6"></div>
<div v-for="n in count" :key="n" class="skeleton-card">
<div class="skeleton-card-inner">
<div class="skeleton-line w-1/4 mb-4"></div>
<div class="skeleton-line-lg w-3/4 mb-3"></div>
<div class="skeleton-line w-full mb-2"></div>
<div class="skeleton-line w-5/6"></div>
</div>
</div>
</div>
@@ -14,3 +14,18 @@
<script setup lang="ts">
withDefaults(defineProps<{ count?: number }>(), { count: 3 })
</script>
<style scoped>
.skeleton-card {
@apply bg-white rounded-2xl shadow-xl border border-gray-100 overflow-hidden mb-6;
}
.skeleton-card-inner {
@apply p-6 md:p-8;
}
.skeleton-line {
@apply h-4 bg-gray-200 rounded;
}
.skeleton-line-lg {
@apply h-6 bg-gray-200 rounded;
}
</style>
+84 -37
View File
@@ -1,47 +1,47 @@
<template>
<div :class="{ 'retro-mode': isRetroMode }" class="app-root">
<header class="bg-gray-800 text-white p-4 flex justify-between items-center relative">
<RouterLink to="/" class="text-xl font-bold">Teletype Games</RouterLink>
<header class="app-header">
<RouterLink to="/" class="app-logo">Teletype Games</RouterLink>
<!-- Desktop Navigation -->
<nav class="hidden md:flex items-center space-x-4">
<RouterLink to="/" class="p-2 hover:text-purple-300 transition-colors duration-200">{{ t('nav.home') }}</RouterLink>
<RouterLink to="/catalog" class="p-2 hover:text-purple-300 transition-colors duration-200">{{ t('nav.catalog') }}</RouterLink>
<RouterLink to="/blog" class="p-2 hover:text-purple-300 transition-colors duration-200">{{ t('nav.blog') }}</RouterLink>
<RouterLink to="/howtos" class="p-2 hover:text-purple-300 transition-colors duration-200">{{ t('nav.howtos') }}</RouterLink>
<RouterLink to="/code" class="p-2 hover:text-purple-300 transition-colors duration-200">{{ t('nav.code') }}</RouterLink>
<RouterLink to="/team" class="p-2 hover:text-purple-300 transition-colors duration-200">{{ t('nav.team') }}</RouterLink>
<RouterLink to="/contact" class="p-2 hover:text-purple-300 transition-colors duration-200">{{ t('nav.contact') }}</RouterLink>
<a v-if="isAdmin" href="/admin" class="p-2 hover:text-yellow-300 transition-colors duration-200">Admin</a>
<nav class="nav-desktop">
<RouterLink to="/" class="nav-link">{{ t('nav.home') }}</RouterLink>
<RouterLink to="/catalog" class="nav-link">{{ t('nav.catalog') }}</RouterLink>
<RouterLink to="/blog" class="nav-link">{{ t('nav.blog') }}</RouterLink>
<RouterLink to="/howtos" class="nav-link">{{ t('nav.howtos') }}</RouterLink>
<RouterLink to="/code" class="nav-link">{{ t('nav.code') }}</RouterLink>
<RouterLink to="/team" class="nav-link">{{ t('nav.team') }}</RouterLink>
<RouterLink to="/contact" class="nav-link">{{ t('nav.contact') }}</RouterLink>
<a v-if="isAdmin" href="/admin" class="nav-link-admin">Admin</a>
<div class="flex items-center gap-1 text-sm font-bold ml-2 border-l border-gray-600 pl-4">
<button :class="locale === 'en' ? 'text-white' : 'text-gray-400 hover:text-white'" class="transition-colors px-1" @click="switchLocale('en')">EN</button>
<span class="text-gray-600">|</span>
<button :class="locale === 'hu' ? 'text-white' : 'text-gray-400 hover:text-white'" class="transition-colors px-1" @click="switchLocale('hu')">HU</button>
<div class="locale-switcher">
<button :class="locale === 'en' ? 'text-white' : 'text-gray-400 hover:text-white'" class="locale-btn" @click="switchLocale('en')">EN</button>
<span class="locale-separator">|</span>
<button :class="locale === 'hu' ? 'text-white' : 'text-gray-400 hover:text-white'" class="locale-btn" @click="switchLocale('hu')">HU</button>
</div>
</nav>
<!-- Hamburger Button (Mobile only) -->
<button class="md:hidden p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 rounded-md" @click="toggleMenu">
<button class="hamburger-btn" @click="toggleMenu">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
<!-- Mobile Navigation -->
<nav v-show="menuOpen" class="md:hidden absolute top-full left-0 w-full bg-gray-800 flex flex-col items-center py-4 space-y-2 z-50">
<RouterLink to="/" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">{{ t('nav.home') }}</RouterLink>
<RouterLink to="/catalog" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">{{ t('nav.catalog') }}</RouterLink>
<RouterLink to="/blog" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">{{ t('nav.blog') }}</RouterLink>
<RouterLink to="/howtos" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">{{ t('nav.howtos') }}</RouterLink>
<RouterLink to="/code" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">{{ t('nav.code') }}</RouterLink>
<RouterLink to="/team" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">{{ t('nav.team') }}</RouterLink>
<RouterLink to="/contact" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200">{{ t('nav.contact') }}</RouterLink>
<a v-if="isAdmin" href="/admin" class="block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200"><i class="fa-solid fa-wrench"></i> Admin</a>
<div class="flex items-center gap-2 pt-2 border-t border-gray-700 w-full justify-center text-sm font-bold">
<button :class="locale === 'en' ? 'text-white' : 'text-gray-400'" class="transition-colors px-2 py-1" @click="switchLocale('en')">EN</button>
<span class="text-gray-600">|</span>
<button :class="locale === 'hu' ? 'text-white' : 'text-gray-400'" class="transition-colors px-2 py-1" @click="switchLocale('hu')">HU</button>
<nav v-show="menuOpen" class="nav-mobile">
<RouterLink to="/" class="nav-mobile-link">{{ t('nav.home') }}</RouterLink>
<RouterLink to="/catalog" class="nav-mobile-link">{{ t('nav.catalog') }}</RouterLink>
<RouterLink to="/blog" class="nav-mobile-link">{{ t('nav.blog') }}</RouterLink>
<RouterLink to="/howtos" class="nav-mobile-link">{{ t('nav.howtos') }}</RouterLink>
<RouterLink to="/code" class="nav-mobile-link">{{ t('nav.code') }}</RouterLink>
<RouterLink to="/team" class="nav-mobile-link">{{ t('nav.team') }}</RouterLink>
<RouterLink to="/contact" class="nav-mobile-link">{{ t('nav.contact') }}</RouterLink>
<a v-if="isAdmin" href="/admin" class="nav-mobile-link"><i class="fa-solid fa-wrench"></i> Admin</a>
<div class="locale-switcher-mobile">
<button :class="locale === 'en' ? 'text-white' : 'text-gray-400'" class="locale-btn-mobile" @click="switchLocale('en')">EN</button>
<span class="locale-separator">|</span>
<button :class="locale === 'hu' ? 'text-white' : 'text-gray-400'" class="locale-btn-mobile" @click="switchLocale('hu')">HU</button>
</div>
</nav>
</header>
@@ -51,20 +51,20 @@
</main>
<div class="retro-toggle-bar">
<div class="flex items-center justify-center gap-4">
<div class="retro-toggle-content">
<button class="retro-toggle-btn" @click="toggleRetroMode">
<i class="fa-solid fa-desktop"></i> {{ isRetroMode ? t('nav.exitRetro') : t('nav.retro') }}
</button>
<div class="flex items-center gap-2 text-xs text-gray-400">
<a href="/api/rss/blog" target="_blank" class="hover:text-orange-400 transition-colors" title="Blog RSS">
<div class="rss-links">
<a href="/api/rss/blog" target="_blank" class="rss-link" title="Blog RSS">
<i class="fa-solid fa-rss"></i> Blog
</a>
<span class="text-gray-600">|</span>
<a href="/api/rss/releases" target="_blank" class="hover:text-orange-400 transition-colors" title="Releases RSS">
<span class="locale-separator">|</span>
<a href="/api/rss/releases" target="_blank" class="rss-link" title="Releases RSS">
<i class="fa-solid fa-rss"></i> Releases
</a>
<span class="text-gray-600">|</span>
<a href="/api/rss/howtos" target="_blank" class="hover:text-orange-400 transition-colors" title="HowTos RSS">
<span class="locale-separator">|</span>
<a href="/api/rss/howtos" target="_blank" class="rss-link" title="HowTos RSS">
<i class="fa-solid fa-rss"></i> HowTos
</a>
</div>
@@ -105,8 +105,55 @@ function switchLocale(lang: string) {
display: flex;
flex-direction: column;
}
.app-root > main {
flex: 1;
}
.app-header {
@apply bg-gray-800 text-white p-4 flex justify-between items-center relative;
}
.app-logo {
@apply text-xl font-bold;
}
.nav-desktop {
@apply hidden md:flex items-center space-x-4;
}
.nav-link {
@apply p-2 hover:text-purple-300 transition-colors duration-200;
}
.nav-link-admin {
@apply p-2 hover:text-yellow-300 transition-colors duration-200;
}
.locale-switcher {
@apply flex items-center gap-1 text-sm font-bold ml-2 border-l border-gray-600 pl-4;
}
.locale-btn {
@apply transition-colors px-1;
}
.locale-separator {
@apply text-gray-600;
}
.hamburger-btn {
@apply md:hidden p-2 focus:outline-none focus:ring-2 focus:ring-purple-500 rounded-md;
}
.nav-mobile {
@apply md:hidden absolute top-full left-0 w-full bg-gray-800 flex flex-col items-center py-4 space-y-2 z-50;
}
.nav-mobile-link {
@apply block p-2 w-full text-center hover:bg-gray-700 transition-colors duration-200;
}
.locale-switcher-mobile {
@apply flex items-center gap-2 pt-2 border-t border-gray-700 w-full justify-center text-sm font-bold;
}
.locale-btn-mobile {
@apply transition-colors px-2 py-1;
}
.retro-toggle-content {
@apply flex items-center justify-center gap-4;
}
.rss-links {
@apply flex items-center gap-2 text-xs text-gray-400;
}
.rss-link {
@apply hover:text-orange-400 transition-colors;
}
</style>
@@ -7,7 +7,7 @@
</header>
<main class="main-container py-12 px-4 mt-0">
<div class="flex flex-wrap gap-4 mb-8 justify-center filter-buttons pb-4">
<div class="filter-buttons">
<button
v-for="f in filters"
:key="f"
@@ -18,11 +18,11 @@
</button>
</div>
<div class="grid grid-cols-1 gap-8">
<div class="software-list">
<template v-for="{ software, releases } in softwares" :key="software.name">
<div v-show="activeFilter === 'all' || software.status === activeFilter" class="software-card group">
<div class="software-card-content">
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div class="software-card-layout">
<div v-if="getDefaultImageUrl(software)" class="flex-shrink-0">
<img :src="getDefaultImageUrl(software)!" :alt="software.title" class="software-thumb" />
</div>
@@ -30,7 +30,7 @@
<RouterLink :to="`/catalog/${software.name}`" class="hover:text-purple-600 transition-colors">
<h2 class="software-title mb-0">{{ software.title }}</h2>
</RouterLink>
<div class="flex items-center gap-2 mt-1 mb-2">
<div class="software-badges">
<span :class="`status-badge status-${software.status}`">{{ t(`common.status.${software.status}`) }}</span>
<PlatformBadge :platform="software.platform" />
</div>
@@ -40,7 +40,7 @@
</div>
</div>
<div class="flex flex-wrap gap-2 items-center">
<div class="software-actions">
<a
v-if="getLatestStable(releases)?.htmlFolderPath"
:href="getLatestStable(releases)?.htmlFolderPath"
@@ -50,7 +50,7 @@
<RouterLink :to="`/catalog/${software.name}`" class="btn-info-sm">
<i class="fa-solid fa-circle-info"></i> {{ t('catalog.moreInfo') }}
</RouterLink>
<span v-if="getTotalDownloads(software.name)" class="text-xs text-gray-400 font-medium ml-1">
<span v-if="getTotalDownloads(software.name)" class="software-download-count">
{{ getTotalDownloads(software.name) }} {{ t('catalog.downloads') }}
</span>
</div>
@@ -98,6 +98,9 @@ onMounted(() => store.fetchAll())
</script>
<style scoped>
.filter-buttons {
@apply flex flex-wrap gap-4 mb-8 justify-center pb-4;
}
.filter-btn {
@apply px-4 py-2 rounded-full border-2 border-purple-600 text-purple-600 font-bold transition-all hover:bg-purple-600 hover:text-white;
}
@@ -134,4 +137,19 @@ onMounted(() => store.fetchAll())
.software-thumb {
@apply w-24 h-24 object-cover rounded-xl border border-gray-100 shadow-sm;
}
.software-list {
@apply grid grid-cols-1 gap-8;
}
.software-card-layout {
@apply flex flex-col md:flex-row md:items-center justify-between gap-4;
}
.software-badges {
@apply flex items-center gap-2 mt-1 mb-2;
}
.software-actions {
@apply flex flex-wrap gap-2 items-center;
}
.software-download-count {
@apply text-xs text-gray-400 font-medium ml-1;
}
</style>
@@ -1,72 +1,71 @@
<template>
<div class="bg-gray-50 min-h-screen pb-20">
<div class="catalog-show-page">
<header class="hero-section-gradient from-purple-700 to-indigo-800 py-12 md:py-20">
<div class="hero-container">
<nav class="mb-8">
<RouterLink to="/catalog" class="text-purple-200 hover:text-white transition-colors flex items-center gap-2 font-medium">
<RouterLink to="/catalog" class="catalog-back-link">
{{ t('catalogShow.back') }}
</RouterLink>
</nav>
<div v-if="software" class="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div v-if="software" class="catalog-header-content">
<div class="flex-grow">
<div class="flex items-center justify-center gap-3 mb-4">
<div class="catalog-badges-group">
<span :class="`status-badge status-${software.status} px-3 py-1`">{{ t(`common.status.${software.status}`) }}</span>
<PlatformBadge :platform="software.platform" />
</div>
<h1 class="text-4xl md:text-6xl font-black text-white mb-4 leading-tight">{{ software.title }}</h1>
<p class="text-xl text-purple-100 max-w-3xl mx-auto leading-relaxed">{{ software.desc }}</p>
<h1 class="catalog-title">{{ software.title }}</h1>
<p class="catalog-subtitle">{{ software.desc }}</p>
</div>
</div>
</div>
</header>
<main v-if="software" class="main-container -mt-10">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div class="catalog-layout">
<!-- Sidebar Info -->
<div class="lg:col-span-1 space-y-6">
<div class="bg-white rounded-3xl shadow-xl p-8 border border-gray-100">
<div class="catalog-sidebar">
<div class="catalog-card">
<div v-if="defaultImageUrl" class="mb-6">
<div class="flex justify-center">
<img
:src="defaultImageUrl"
:alt="software.title"
:class="['max-h-48 w-auto object-contain border-4 border-gray-100 shadow-sm', softwareImages.length > 0 ? 'cursor-pointer hover:opacity-80 transition-opacity' : '']"
:class="['catalog-featured-image', softwareImages.length > 0 ? 'cursor-pointer hover:opacity-80 transition-opacity' : '']"
@click="openLightbox(defaultImageIndex)"
/>
</div>
<div v-if="softwareImages.length > 1" class="flex gap-2 mt-3 justify-center">
<div v-if="softwareImages.length > 1" class="catalog-thumbs">
<img
v-for="(img, idx) in softwareImages"
:key="idx"
:src="img.url"
:alt="`${software.title} ${idx + 1}`"
:class="['w-12 h-12 object-cover rounded border-2 cursor-pointer transition-all',
img.isDefault ? 'border-purple-400' : 'border-gray-200 hover:border-purple-300']"
:class="['catalog-thumb', img.isDefault ? 'border-purple-400' : 'border-gray-200 hover:border-purple-300']"
@click="openLightbox(idx)"
/>
</div>
</div>
<h2 class="text-xl font-bold text-gray-900 mb-6 flex items-center gap-2">
<h2 class="catalog-section-heading">
<i class="fa-solid fa-circle-info text-purple-600"></i> {{ t('catalogShow.projectInfo') }}
</h2>
<dl class="space-y-4">
<div>
<dt class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">{{ t('catalogShow.author') }}</dt>
<dd class="text-gray-900 font-medium">{{ software.author }}</dd>
<dt class="catalog-info-label">{{ t('catalogShow.author') }}</dt>
<dd class="catalog-info-value">{{ software.author }}</dd>
</div>
<div>
<dt class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">{{ t('catalogShow.platform') }}</dt>
<dt class="catalog-info-label">{{ t('catalogShow.platform') }}</dt>
<dd><PlatformBadge :platform="software.platform" /></dd>
</div>
<div v-if="software.license">
<dt class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">{{ t('catalogShow.license') }}</dt>
<dd class="text-gray-900 font-medium">{{ software.license }}</dd>
<dt class="catalog-info-label">{{ t('catalogShow.license') }}</dt>
<dd class="catalog-info-value">{{ software.license }}</dd>
</div>
<div v-if="software.externalLinks?.length">
<dt class="text-xs font-bold text-gray-400 uppercase tracking-widest mb-1">{{ t('catalogShow.links') }}</dt>
<dt class="catalog-info-label">{{ t('catalogShow.links') }}</dt>
<dd class="flex flex-col gap-1.5">
<a v-for="link in software.externalLinks" :key="link.url" :href="link.url" target="_blank" class="text-purple-600 hover:text-purple-800 font-bold inline-flex items-center gap-1">
<a v-for="link in software.externalLinks" :key="link.url" :href="link.url" target="_blank" class="catalog-external-link">
{{ link.label }} <span class="text-xs"></span>
</a>
</dd>
@@ -76,63 +75,63 @@
</div>
<!-- Main Content Area -->
<div class="lg:col-span-2 space-y-8">
<div v-if="software.story" class="bg-white rounded-3xl shadow-xl p-8 border border-gray-100">
<h2 class="text-xl font-bold text-gray-900 mb-4">{{ t('catalogShow.about') }}</h2>
<p class="text-gray-700 leading-relaxed whitespace-pre-line">{{ software.story }}</p>
<div class="catalog-main">
<div v-if="software.story" class="catalog-card">
<h2 class="catalog-about-title">{{ t('catalogShow.about') }}</h2>
<p class="catalog-about-text">{{ software.story }}</p>
</div>
<div v-if="latestStable" class="bg-gradient-to-br from-indigo-600 to-purple-700 rounded-3xl shadow-xl p-8 text-white">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center gap-6">
<div v-if="latestStable" class="latest-release-card">
<div class="latest-release-header">
<div>
<div class="flex items-center gap-2 mb-2">
<span class="px-2 py-0.5 bg-white/20 text-white text-[10px] font-black uppercase rounded tracking-widest">{{ t('catalogShow.latestStable') }}</span>
<div class="latest-release-label-row">
<span class="latest-release-badge">{{ t('catalogShow.latestStable') }}</span>
</div>
<h2 class="text-4xl font-black mb-1">{{ latestStable.version }}</h2>
<p class="text-indigo-100 text-sm">{{ t('catalogShow.released') }} {{ formatDateTime(latestStable.UpdatedAt) }}</p>
<h2 class="latest-release-version">{{ latestStable.version }}</h2>
<p class="latest-release-date">{{ t('catalogShow.released') }} {{ formatDateTime(latestStable.UpdatedAt) }}</p>
</div>
<a v-if="latestStable.htmlFolderPath" :href="latestStable.htmlFolderPath" target="_blank" class="flex items-center justify-center py-3 px-6 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:bg-indigo-50 shadow-lg shadow-black/10 text-sm whitespace-nowrap">{{ t('catalogShow.playNow') }}</a>
<a v-if="latestStable.htmlFolderPath" :href="latestStable.htmlFolderPath" target="_blank" class="latest-release-play-btn">{{ t('catalogShow.playNow') }}</a>
</div>
<div v-if="tableAssets(latestStable).length" class="mt-6 pt-6 border-t border-white/15">
<div class="text-[10px] font-black uppercase tracking-widest text-indigo-200 mb-3">{{ t('catalogShow.assetsTitle') }}</div>
<div v-if="tableAssets(latestStable).length" class="latest-release-assets">
<div class="assets-section-label">{{ t('catalogShow.assetsTitle') }}</div>
<!-- Desktop asset table -->
<table class="hidden md:table w-full text-sm">
<table class="asset-table-desktop">
<tbody>
<tr v-for="asset in tableAssets(latestStable)" :key="asset.kind" class="border-b border-white/10 last:border-0">
<td class="py-1.5 pr-6 text-indigo-100 whitespace-nowrap"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="py-1.5 pr-6 w-full font-mono text-xs text-indigo-200/80 break-all">{{ assetFileName(asset.path) }}</td>
<td class="py-1.5 text-right whitespace-nowrap">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-white font-bold hover:underline">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-white font-bold hover:underline">{{ t('catalogShow.assetDownload') }}</a>
<tr v-for="asset in tableAssets(latestStable)" :key="asset.kind" class="asset-table-row-light">
<td class="asset-kind-col-light"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="asset-path-col-light">{{ assetFileName(asset.path) }}</td>
<td class="asset-action-col">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="asset-link-white">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="asset-link-white">{{ t('catalogShow.assetDownload') }}</a>
</td>
</tr>
</tbody>
</table>
<!-- Mobile asset list -->
<div class="md:hidden space-y-2">
<div v-for="asset in tableAssets(latestStable)" :key="asset.kind" class="flex flex-col gap-0.5 py-2 border-b border-white/10 last:border-0">
<div class="flex justify-between items-center">
<span class="text-indigo-100 text-sm whitespace-nowrap"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</span>
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-white font-bold text-sm hover:underline ml-4 shrink-0">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-white font-bold text-sm hover:underline ml-4 shrink-0">{{ t('catalogShow.assetDownload') }}</a>
<div class="asset-mobile-list">
<div v-for="asset in tableAssets(latestStable)" :key="asset.kind" class="asset-mobile-item-light">
<div class="asset-mobile-row">
<span class="asset-mobile-kind-light"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</span>
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="asset-mobile-link-white">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="asset-mobile-link-white">{{ t('catalogShow.assetDownload') }}</a>
</div>
<div class="font-mono text-xs text-indigo-200/80 truncate">{{ assetFileName(asset.path) }}</div>
<div class="asset-mobile-path-light">{{ assetFileName(asset.path) }}</div>
</div>
</div>
</div>
</div>
<div class="bg-white rounded-3xl shadow-xl overflow-hidden border border-gray-100">
<div class="p-8 border-b border-gray-100">
<h2 class="text-2xl font-bold text-gray-900">{{ t('catalogShow.allReleases') }}</h2>
<div class="releases-card">
<div class="releases-header">
<h2 class="releases-title">{{ t('catalogShow.allReleases') }}</h2>
</div>
<div class="p-0">
<!-- Desktop Table -->
<div class="hidden md:block">
<table class="w-full text-left">
<thead class="bg-gray-50 text-gray-500 text-xs font-bold uppercase tracking-widest">
<thead class="release-table-head">
<tr>
<th class="px-8 py-4">{{ t('catalogShow.version') }}</th>
<th class="px-8 py-4">{{ t('catalogShow.releaseDate') }}</th>
@@ -140,26 +139,26 @@
</thead>
<tbody>
<template v-for="release in stableReleases" :key="release.version">
<tr class="border-t border-gray-100 first:border-t-0 hover:bg-gray-50/50 transition-colors">
<tr class="release-table-row">
<td class="px-8 py-4">
<div class="flex items-center gap-2 mb-2">
<span class="font-bold text-gray-900 text-lg">{{ release.version }}</span>
<span v-if="release.downloadCount" class="text-xs text-gray-400 font-medium"> {{ release.downloadCount }}</span>
<div class="release-version-row">
<span class="release-version">{{ release.version }}</span>
<span v-if="release.downloadCount" class="release-download-count"> {{ release.downloadCount }}</span>
</div>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm">{{ t('catalogShow.play') }}</a>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="release-play-link">{{ t('catalogShow.play') }}</a>
</td>
<td class="px-8 py-4 text-gray-500 align-top pt-5">{{ formatDateTime(release.UpdatedAt) }}</td>
<td class="release-date-col">{{ formatDateTime(release.UpdatedAt) }}</td>
</tr>
<tr v-if="tableAssets(release).length">
<td colspan="2" class="px-8 pb-4 pt-0">
<table class="w-full text-sm">
<tbody>
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="border-b border-gray-100 last:border-0">
<td class="py-1 pr-6 text-gray-500 whitespace-nowrap"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="py-1 pr-6 w-full font-mono text-xs text-gray-400 break-all">{{ assetFileName(asset.path) }}</td>
<td class="py-1 text-right whitespace-nowrap">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-blue-600 hover:text-blue-900 font-bold">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-blue-600 hover:text-blue-900 font-bold">{{ t('catalogShow.assetDownload') }}</a>
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="release-asset-row">
<td class="release-asset-kind"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="release-asset-path">{{ assetFileName(asset.path) }}</td>
<td class="release-asset-action">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="release-asset-link">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="release-asset-link">{{ t('catalogShow.assetDownload') }}</a>
</td>
</tr>
</tbody>
@@ -168,28 +167,28 @@
</tr>
</template>
<tr v-if="devReleases.length > 0" class="border-t border-gray-100 bg-yellow-50/30">
<td colspan="2" class="px-8 py-3 text-[10px] font-black text-yellow-700 uppercase tracking-tighter">{{ t('catalogShow.devVersions') }}</td>
<tr v-if="devReleases.length > 0" class="dev-versions-header">
<td colspan="2" class="dev-versions-label">{{ t('catalogShow.devVersions') }}</td>
</tr>
<template v-for="release in devReleases" :key="release.version">
<tr class="border-t border-gray-100 bg-yellow-50/10 hover:bg-yellow-50/30 transition-colors">
<tr class="dev-release-row">
<td class="px-8 py-4">
<div class="font-bold text-yellow-800 text-lg mb-2">{{ release.version }}</div>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm">{{ t('catalogShow.play') }}</a>
<div class="dev-release-version">{{ release.version }}</div>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="release-play-link">{{ t('catalogShow.play') }}</a>
</td>
<td class="px-8 py-4 text-gray-500 align-top pt-5">{{ formatDateTime(release.UpdatedAt) }}</td>
<td class="release-date-col">{{ formatDateTime(release.UpdatedAt) }}</td>
</tr>
<tr v-if="tableAssets(release).length" class="bg-yellow-50/10">
<td colspan="2" class="px-8 pb-4 pt-0">
<table class="w-full text-sm">
<tbody>
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="border-b border-gray-100 last:border-0">
<td class="py-1 pr-6 text-gray-500 whitespace-nowrap"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="py-1 pr-6 w-full font-mono text-xs text-gray-400 break-all">{{ assetFileName(asset.path) }}</td>
<td class="py-1 text-right whitespace-nowrap">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-blue-600 hover:text-blue-900 font-bold">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-blue-600 hover:text-blue-900 font-bold">{{ t('catalogShow.assetDownload') }}</a>
<tr v-for="asset in tableAssets(release)" :key="asset.kind" class="release-asset-row">
<td class="release-asset-kind"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</td>
<td class="release-asset-path">{{ assetFileName(asset.path) }}</td>
<td class="release-asset-action">
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="release-asset-link">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="release-asset-link">{{ t('catalogShow.assetDownload') }}</a>
</td>
</tr>
</tbody>
@@ -208,22 +207,22 @@
:key="release.version"
:class="['p-6', release.version.startsWith('dev-') ? 'bg-yellow-50/20' : '']"
>
<div class="flex justify-between items-start mb-4">
<div class="mobile-release-header">
<div>
<div class="font-bold text-gray-900 text-lg">{{ release.version }}</div>
<div class="text-gray-500 text-xs mt-1">{{ formatDateTime(release.UpdatedAt) }}</div>
<div class="mobile-release-version">{{ release.version }}</div>
<div class="mobile-release-date">{{ formatDateTime(release.UpdatedAt) }}</div>
</div>
<span v-if="release.version.startsWith('dev-')" class="px-2 py-0.5 bg-yellow-100 text-yellow-800 text-[10px] font-black uppercase rounded">Dev</span>
<span v-if="release.version.startsWith('dev-')" class="dev-badge">Dev</span>
</div>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="flex items-center justify-center py-2 bg-purple-100 text-purple-700 font-bold rounded-lg text-sm">{{ t('catalogShow.play') }}</a>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="mobile-play-btn">{{ t('catalogShow.play') }}</a>
<div v-if="tableAssets(release).length" class="mt-3 space-y-1">
<div v-for="asset in tableAssets(release)" :key="asset.kind" class="flex flex-col gap-0.5 py-1.5 border-b border-gray-100 last:border-0">
<div class="flex justify-between items-center">
<span class="text-gray-500 text-sm whitespace-nowrap"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</span>
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="text-blue-600 font-bold text-sm ml-4 shrink-0">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="text-blue-600 font-bold text-sm ml-4 shrink-0">{{ t('catalogShow.assetDownload') }}</a>
<div v-for="asset in tableAssets(release)" :key="asset.kind" class="asset-mobile-item">
<div class="asset-mobile-row">
<span class="asset-mobile-kind"><i :class="assetKindIcon(asset.kind)" class="w-4 text-center"></i> {{ t('catalogShow.assetKind.' + asset.kind) }}</span>
<a v-if="asset.kind === 'docs'" :href="asset.path" target="_blank" class="asset-mobile-link">{{ t('catalogShow.assetOpen') }}</a>
<a v-else :href="downloadUrl(asset.path)" class="asset-mobile-link">{{ t('catalogShow.assetDownload') }}</a>
</div>
<div class="font-mono text-xs text-gray-400 truncate">{{ assetFileName(asset.path) }}</div>
<div class="asset-mobile-path">{{ assetFileName(asset.path) }}</div>
</div>
</div>
</div>
@@ -235,7 +234,7 @@
</main>
<main v-else class="main-container -mt-10">
<div class="bg-white rounded-3xl shadow-xl p-12 text-center text-gray-400">
<div class="loading-card">
{{ error || t('catalogShow.loading') }}
</div>
</main>
@@ -344,11 +343,105 @@ onMounted(async () => {
</script>
<style scoped>
.status-badge {
@apply rounded text-[10px] font-black uppercase tracking-wider;
}
/* Page */
.catalog-show-page { @apply bg-gray-50 min-h-screen pb-20; }
.catalog-back-link { @apply text-purple-200 hover:text-white transition-colors flex items-center gap-2 font-medium; }
.catalog-header-content { @apply flex flex-col md:flex-row md:items-end justify-between gap-6; }
.catalog-badges-group { @apply flex items-center justify-center gap-3 mb-4; }
.catalog-title { @apply text-4xl md:text-6xl font-black text-white mb-4 leading-tight; }
.catalog-subtitle { @apply text-xl text-purple-100 max-w-3xl mx-auto leading-relaxed; }
/* Layout */
.catalog-layout { @apply grid grid-cols-1 lg:grid-cols-3 gap-8; }
.catalog-sidebar { @apply lg:col-span-1 space-y-6; }
.catalog-main { @apply lg:col-span-2 space-y-8; }
.catalog-card { @apply bg-white rounded-3xl shadow-xl p-8 border border-gray-100; }
/* Sidebar */
.catalog-featured-image { @apply max-h-48 w-auto object-contain border-4 border-gray-100 shadow-sm; }
.catalog-thumbs { @apply flex gap-2 mt-3 justify-center; }
.catalog-thumb { @apply w-12 h-12 object-cover rounded border-2 cursor-pointer transition-all; }
.catalog-section-heading { @apply text-xl font-bold text-gray-900 mb-6 flex items-center gap-2; }
.catalog-info-label { @apply text-xs font-bold text-gray-400 uppercase tracking-widest mb-1; }
.catalog-info-value { @apply text-gray-900 font-medium; }
.catalog-external-link { @apply text-purple-600 hover:text-purple-800 font-bold inline-flex items-center gap-1; }
/* About */
.catalog-about-title { @apply text-xl font-bold text-gray-900 mb-4; }
.catalog-about-text { @apply text-gray-700 leading-relaxed whitespace-pre-line; }
/* Latest Release */
.latest-release-card { @apply bg-gradient-to-br from-indigo-600 to-purple-700 rounded-3xl shadow-xl p-8 text-white; }
.latest-release-header { @apply flex flex-col md:flex-row justify-between items-start md:items-center gap-6; }
.latest-release-label-row { @apply flex items-center gap-2 mb-2; }
.latest-release-badge { @apply px-2 py-0.5 bg-white/20 text-white text-[10px] font-black uppercase rounded tracking-widest; }
.latest-release-version { @apply text-4xl font-black mb-1; }
.latest-release-date { @apply text-indigo-100 text-sm; }
.latest-release-play-btn { @apply flex items-center justify-center py-3 px-6 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:bg-indigo-50 shadow-lg shadow-black/10 text-sm whitespace-nowrap; }
.latest-release-assets { @apply mt-6 pt-6 border-t border-white/15; }
.assets-section-label { @apply text-[10px] font-black uppercase tracking-widest text-indigo-200 mb-3; }
/* Asset table - light (hero) */
.asset-table-desktop { @apply hidden md:table w-full text-sm; }
.asset-table-row-light { @apply border-b border-white/10 last:border-0; }
.asset-kind-col-light { @apply py-1.5 pr-6 text-indigo-100 whitespace-nowrap; }
.asset-path-col-light { @apply py-1.5 pr-6 w-full font-mono text-xs text-indigo-200/80 break-all; }
.asset-action-col { @apply py-1.5 text-right whitespace-nowrap; }
.asset-link-white { @apply text-white font-bold hover:underline; }
/* Asset mobile - light (hero) */
.asset-mobile-list { @apply md:hidden space-y-2; }
.asset-mobile-item-light { @apply flex flex-col gap-0.5 py-2 border-b border-white/10 last:border-0; }
.asset-mobile-row { @apply flex justify-between items-center; }
.asset-mobile-kind-light { @apply text-indigo-100 text-sm whitespace-nowrap; }
.asset-mobile-link-white { @apply text-white font-bold text-sm hover:underline ml-4 shrink-0; }
.asset-mobile-path-light { @apply font-mono text-xs text-indigo-200/80 truncate; }
/* Releases section */
.releases-card { @apply bg-white rounded-3xl shadow-xl overflow-hidden border border-gray-100; }
.releases-header { @apply p-8 border-b border-gray-100; }
.releases-title { @apply text-2xl font-bold text-gray-900; }
.release-table-head { @apply bg-gray-50 text-gray-500 text-xs font-bold uppercase tracking-widest; }
.release-table-row { @apply border-t border-gray-100 first:border-t-0 hover:bg-gray-50/50 transition-colors; }
.release-version-row { @apply flex items-center gap-2 mb-2; }
.release-version { @apply font-bold text-gray-900 text-lg; }
.release-download-count { @apply text-xs text-gray-400 font-medium; }
.release-play-link { @apply text-purple-600 hover:text-purple-900 font-bold text-sm; }
.release-date-col { @apply px-8 py-4 text-gray-500 align-top pt-5; }
/* Release asset table */
.release-asset-row { @apply border-b border-gray-100 last:border-0; }
.release-asset-kind { @apply py-1 pr-6 text-gray-500 whitespace-nowrap; }
.release-asset-path { @apply py-1 pr-6 w-full font-mono text-xs text-gray-400 break-all; }
.release-asset-action { @apply py-1 text-right whitespace-nowrap; }
.release-asset-link { @apply text-blue-600 hover:text-blue-900 font-bold; }
/* Dev versions */
.dev-versions-header { @apply border-t border-gray-100 bg-yellow-50/30; }
.dev-versions-label { @apply px-8 py-3 text-[10px] font-black text-yellow-700 uppercase tracking-tighter; }
.dev-release-row { @apply border-t border-gray-100 bg-yellow-50/10 hover:bg-yellow-50/30 transition-colors; }
.dev-release-version { @apply font-bold text-yellow-800 text-lg mb-2; }
/* Mobile releases */
.mobile-release-header { @apply flex justify-between items-start mb-4; }
.mobile-release-version { @apply font-bold text-gray-900 text-lg; }
.mobile-release-date { @apply text-gray-500 text-xs mt-1; }
.dev-badge { @apply px-2 py-0.5 bg-yellow-100 text-yellow-800 text-[10px] font-black uppercase rounded; }
.mobile-play-btn { @apply flex items-center justify-center py-2 bg-purple-100 text-purple-700 font-bold rounded-lg text-sm; }
/* Asset mobile - dark (releases) */
.asset-mobile-item { @apply flex flex-col gap-0.5 py-1.5 border-b border-gray-100 last:border-0; }
.asset-mobile-kind { @apply text-gray-500 text-sm whitespace-nowrap; }
.asset-mobile-link { @apply text-blue-600 font-bold text-sm ml-4 shrink-0; }
.asset-mobile-path { @apply font-mono text-xs text-gray-400 truncate; }
/* Status badges */
.status-badge { @apply rounded text-[10px] font-black uppercase tracking-wider; }
.status-released { @apply bg-green-500 text-white shadow-lg shadow-green-500/20; }
.status-demo { @apply bg-blue-500 text-white shadow-lg shadow-blue-500/20; }
.status-development { @apply bg-yellow-500 text-white shadow-lg shadow-yellow-500/20; }
.status-archived { @apply bg-gray-500 text-white shadow-lg shadow-gray-500/20; }
/* Loading */
.loading-card { @apply bg-white rounded-3xl shadow-xl p-12 text-center text-gray-400; }
</style>
@@ -3,17 +3,17 @@
<div
v-if="isOpen"
ref="overlay"
class="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm"
class="lightbox-overlay"
tabindex="0"
@click.self="close"
@keydown.escape="close"
@keydown.left="prev"
@keydown.right="next"
>
<button class="absolute top-4 right-4 text-white/70 hover:text-white text-3xl font-bold z-10" @click="close">&times;</button>
<button class="lightbox-close-btn" @click="close">&times;</button>
<button
v-if="images.length > 1" class="absolute left-4 text-white/70 hover:text-white text-5xl font-bold z-10 select-none"
v-if="images.length > 1" class="lightbox-nav-btn left-4"
@click="prev"
>
&lsaquo;
@@ -22,17 +22,17 @@
<img
:src="images[currentIndex].url"
:alt="`Image ${currentIndex + 1}`"
class="max-h-[85vh] max-w-[90vw] object-contain rounded-lg shadow-2xl"
class="lightbox-image"
/>
<button
v-if="images.length > 1" class="absolute right-4 text-white/70 hover:text-white text-5xl font-bold z-10 select-none"
v-if="images.length > 1" class="lightbox-nav-btn right-4"
@click="next"
>
&rsaquo;
</button>
<div v-if="images.length > 1" class="absolute bottom-6 text-white/60 text-sm font-mono">
<div v-if="images.length > 1" class="lightbox-counter">
{{ currentIndex + 1 }} / {{ images.length }}
</div>
</div>
@@ -71,3 +71,21 @@ function close() { emit('close') }
function next() { currentIndex.value = (currentIndex.value + 1) % props.images.length }
function prev() { currentIndex.value = (currentIndex.value - 1 + props.images.length) % props.images.length }
</script>
<style scoped>
.lightbox-overlay {
@apply fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm;
}
.lightbox-close-btn {
@apply absolute top-4 right-4 text-white/70 hover:text-white text-3xl font-bold z-10;
}
.lightbox-nav-btn {
@apply absolute text-white/70 hover:text-white text-5xl font-bold z-10 select-none;
}
.lightbox-image {
@apply max-h-[85vh] max-w-[90vw] object-contain rounded-lg shadow-2xl;
}
.lightbox-counter {
@apply absolute bottom-6 text-white/60 text-sm font-mono;
}
</style>
+17 -5
View File
@@ -30,7 +30,7 @@
</h2>
<p class="contact-card-desc">{{ t('contact.communityDesc') }}</p>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="community-grid-3">
<div class="community-section border-[#5865F2]">
<h3 class="community-section-title text-[#5865F2]">Discord</h3>
<p class="community-section-desc">{{ t('contact.discord.desc') }}</p>
@@ -41,7 +41,7 @@
<div class="community-section border-green-500">
<h3 class="community-section-title text-green-700">IRC</h3>
<p class="text-sm text-gray-600 mb-3">{{ t('contact.irc.desc') }}</p>
<p class="community-section-desc">{{ t('contact.irc.desc') }}</p>
<div class="irc-box">
<span class="irc-network">Network: Libera.Chat</span>
<code class="irc-channel">#teletypegames</code>
@@ -65,7 +65,7 @@
</h2>
<p class="contact-card-desc">{{ t('contact.socialDesc') }}</p>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div class="social-grid">
<div class="community-section border-red-600">
<h3 class="community-section-title text-red-600">YouTube</h3>
<p class="community-section-desc">{{ t('contact.youtube.desc') }}</p>
@@ -107,8 +107,8 @@
</svg>
</div>
<div>
<h2 class="text-2xl font-black text-white">Teletype Games Party</h2>
<p class="text-[#1DB954] text-sm font-bold">Spotify Playlist</p>
<h2 class="spotify-card-title">Teletype Games Party</h2>
<p class="spotify-card-subtitle">Spotify Playlist</p>
</div>
</div>
<div class="spotify-embed-wrap">
@@ -193,9 +193,21 @@ function openEmail() {
.spotify-icon-wrap {
@apply w-14 h-14 rounded-2xl bg-[#1DB954]/10 border border-[#1DB954]/20 flex items-center justify-center;
}
.spotify-card-title {
@apply text-2xl font-black text-white;
}
.spotify-card-subtitle {
@apply text-[#1DB954] text-sm font-bold;
}
.spotify-embed-wrap {
@apply rounded-xl overflow-hidden;
}
.community-grid-3 {
@apply grid grid-cols-1 sm:grid-cols-3 gap-4;
}
.social-grid {
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4;
}
.irc-box {
@apply bg-green-100 p-4 rounded-xl border border-green-200 mt-3 inline-block;
}
+66 -17
View File
@@ -43,7 +43,7 @@
<img :src="highlightedImageUrl" :alt="highlightedSoftware.software.title" class="featured-image" />
</div>
<div class="featured-content">
<div class="flex items-center gap-2 mb-4">
<div class="featured-badges-group">
<span class="featured-badge">{{ t('home.featuredGame') }}</span>
<span :class="`status-badge status-${highlightedSoftware.software.status}`">{{ t(`common.status.${highlightedSoftware.software.status}`) }}</span>
<span v-if="highlightedStableRelease" class="featured-version-badge">{{ highlightedStableRelease.version }}</span>
@@ -51,7 +51,7 @@
<h2 class="featured-title">{{ highlightedSoftware.software.title }}</h2>
<p class="featured-desc">{{ highlightedSoftware.software.desc }}</p>
<div class="flex flex-wrap gap-4 mt-8">
<div class="featured-actions">
<a
v-if="highlightedStableRelease?.htmlFolderPath"
:href="highlightedStableRelease.htmlFolderPath"
@@ -72,7 +72,7 @@
<div class="yt-glow"></div>
<div class="yt-header-area">
<div class="flex items-center gap-3">
<div class="yt-header-inner">
<div class="yt-status-blob">
<div class="yt-status-dot"></div>
</div>
@@ -85,23 +85,23 @@
<div class="yt-content-grid">
<div class="yt-player-container">
<div class="player-wrap relative w-full aspect-video">
<div v-if="ytState === 'loading'" class="state-overlay absolute inset-0 flex items-center justify-center bg-slate-950">
<div class="w-10 h-10 border-4 border-white/5 border-t-indigo-500 rounded-full animate-spin"></div>
<div class="yt-player-wrap">
<div v-if="ytState === 'loading'" class="yt-state-overlay">
<div class="yt-loading-spinner"></div>
</div>
<div v-else-if="ytState === 'error'" class="state-overlay absolute inset-0 flex items-center justify-center bg-slate-950 flex-col gap-2">
<div class="text-xl text-yellow-500"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="text-[11px] text-slate-500 text-center px-4 font-bold uppercase tracking-tighter">{{ ytError }}</div>
<div v-else-if="ytState === 'error'" class="yt-state-overlay flex-col gap-2">
<div class="yt-error-icon"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="yt-error-text">{{ ytError }}</div>
</div>
<div v-else-if="ytState === 'no-config'" class="state-overlay absolute inset-0 flex items-center justify-center bg-slate-950">
<div class="text-slate-500 text-xs">{{ t('common.missingApiConfig') }}</div>
<div v-else-if="ytState === 'no-config'" class="yt-state-overlay">
<div class="yt-noconfig-text">{{ t('common.missingApiConfig') }}</div>
</div>
<iframe
v-else-if="ytVideoId"
:src="`https://www.youtube.com/embed/${ytVideoId}?autoplay=0&rel=0&modestbranding=1`"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
class="w-full h-full border-none block"
class="yt-iframe"
></iframe>
</div>
</div>
@@ -132,7 +132,7 @@
<div class="spotify-glow"></div>
<div class="spotify-header">
<div class="flex items-center gap-3">
<div class="spotify-header-inner">
<div class="spotify-pulse-wrap">
<div class="spotify-pulse-bar"></div>
<div class="spotify-pulse-bar bar-2"></div>
@@ -162,7 +162,7 @@
</section>
<!-- Navigation Grid -->
<section class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<section class="nav-grid">
<div class="nav-card group card-interactive">
<div class="nav-card-icon bg-purple-50 group-hover:bg-purple-100">
<i class="fa-solid fa-gamepad text-3xl text-purple-600"></i>
@@ -209,9 +209,9 @@
</section>
<!-- AI Content Notice -->
<div class="mb-12 mt-12 bg-white rounded-2xl border border-indigo-100 p-6 shadow-sm flex items-center gap-4">
<div class="w-12 h-12 rounded-full bg-indigo-50 flex items-center justify-center text-2xl flex-shrink-0"><i class="fa-solid fa-robot text-indigo-500"></i></div>
<div class="flex-grow text-slate-600 text-sm md:text-base italic">
<div class="ai-notice-card">
<div class="ai-notice-icon"><i class="fa-solid fa-robot text-indigo-500"></i></div>
<div class="ai-notice-text">
<strong>Note:</strong> {{ t('home.note') }}
</div>
</div>
@@ -270,6 +270,12 @@ onUnmounted(() => eventStore.cleanup())
.featured-content {
@apply p-8 md:p-12 flex flex-col justify-center flex-1 z-10;
}
.featured-badges-group {
@apply flex items-center gap-2 mb-4;
}
.featured-actions {
@apply flex flex-wrap gap-4 mt-8;
}
.featured-badge {
@apply px-3 py-1 bg-indigo-500/20 text-indigo-300 border border-indigo-400/30 rounded-full text-xs font-bold uppercase tracking-wider;
}
@@ -369,6 +375,30 @@ onUnmounted(() => eventStore.cleanup())
.yt-player-container {
@apply flex-[1.6] overflow-hidden rounded-2xl border border-white/5 shadow-inner bg-black;
}
.yt-player-wrap {
@apply relative w-full aspect-video;
}
.yt-state-overlay {
@apply absolute inset-0 flex items-center justify-center bg-slate-950;
}
.yt-loading-spinner {
@apply w-10 h-10 border-4 border-white/5 border-t-indigo-500 rounded-full animate-spin;
}
.yt-error-icon {
@apply text-xl text-yellow-500;
}
.yt-error-text {
@apply text-[11px] text-slate-500 text-center px-4 font-bold uppercase tracking-tighter;
}
.yt-noconfig-text {
@apply text-slate-500 text-xs;
}
.yt-iframe {
@apply w-full h-full border-none block;
}
.yt-header-inner {
@apply flex items-center gap-3;
}
.yt-info-sidebar {
@apply flex-1 flex flex-col justify-center;
}
@@ -420,7 +450,26 @@ onUnmounted(() => eventStore.cleanup())
.spotify-open-link {
@apply text-xs font-semibold text-[#1DB954] hover:text-[#1ed760] transition-colors;
}
.spotify-header-inner {
@apply flex items-center gap-3;
}
.spotify-embed-container {
@apply relative z-10 rounded-xl overflow-hidden;
}
/* Nav Grid */
.nav-grid {
@apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6;
}
/* AI Notice */
.ai-notice-card {
@apply mb-12 mt-12 bg-white rounded-2xl border border-indigo-100 p-6 shadow-sm flex items-center gap-4;
}
.ai-notice-icon {
@apply w-12 h-12 rounded-full bg-indigo-50 flex items-center justify-center text-2xl flex-shrink-0;
}
.ai-notice-text {
@apply flex-grow text-slate-600 text-sm md:text-base italic;
}
</style>