custom css classes
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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">×</button>
|
||||
<button class="lightbox-close-btn" @click="close">×</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"
|
||||
>
|
||||
‹
|
||||
@@ -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"
|
||||
>
|
||||
›
|
||||
</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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user