refact round

This commit is contained in:
2026-08-04 15:45:13 +02:00
parent 24ba77a0f2
commit 724e5bc748
38 changed files with 129 additions and 112 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ const index = async (): Promise<SoftwareEntry[]> => {
const highlighted = async (): Promise<SoftwareEntry | null> => {
const res = await fetch('/api/software/highlighted')
if (!res.ok) return null
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
@@ -1,7 +1,7 @@
export interface Member {
nick: string
real_nick: string
realNick: string
motto: string
avatar_filename: string
image_url?: string | null
avatarFilename: string
imageUrl?: string | null
}
@@ -11,7 +11,7 @@ export interface Release {
docsFolderPath?: string
assets?: ReleaseAsset[]
downloadCount?: number
UpdatedAt: string
updatedAt: string
}
export interface SoftwareImage {
+2 -2
View File
@@ -17,13 +17,13 @@ export function getDefaultImageUrl(sw: Software): string | null {
export function filterStableReleases(releases: Release[]): Release[] {
return releases
.filter(r => !r.version.startsWith('dev-'))
.sort((a, b) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
}
export function filterDevReleases(releases: Release[]): Release[] {
return releases
.filter(r => r.version.startsWith('dev-'))
.sort((a, b) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
}
export function getLatestStable(releases: Release[]): Release | undefined {
@@ -86,7 +86,7 @@ const store = useBlogStore()
const { pages: blogPages, loading, error } = storeToRefs(store)
const { isNew, getPermalink, getCleanPreview } = store
onMounted(() => store.fetchPages())
onMounted(() => store.fetch())
</script>
<style scoped>
+3 -4
View File
@@ -32,9 +32,7 @@
</div>
</article>
<div v-else-if="loading" class="blog-post-card">
<div class="blog-post-content text-center text-gray-400 py-12">Loading...</div>
</div>
<SkeletonCard v-else-if="loading" :count="1" />
</main>
</div>
</template>
@@ -46,10 +44,11 @@ import { RouterLink, useRoute } from 'vue-router'
import { formatDateTime } from '../../lib/dateFormat'
import { sanitize } from '../../lib/sanitize'
import { useBlogStore } from '../../stores/blog.store'
import SkeletonCard from '../../components/SkeletonCard.vue'
const route = useRoute()
const store = useBlogStore()
const { currentPage: pageContent, currentPageLoading: loading, currentPageError: error } = storeToRefs(store)
const { currentPage: pageContent, pageLoading: loading, pageError: error } = storeToRefs(store)
onMounted(() => store.fetchPage(route.params.slug as string))
</script>
@@ -9,7 +9,7 @@
<main class="main-container py-12 px-4 mt-0">
<div v-if="error" class="builds-error">{{ error }}</div>
<div v-else-if="!data" class="builds-loading">{{ t('common.loading') }}</div>
<SkeletonCard v-else-if="!data" :count="3" />
<!-- Desktop table -->
<div v-else class="builds-desktop">
@@ -86,6 +86,7 @@ import { storeToRefs } from 'pinia'
import { RouterLink } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useBuildsStore } from '../../stores/builds.store'
import SkeletonCard from '../../components/SkeletonCard.vue'
const { t } = useI18n()
@@ -18,7 +18,9 @@
</button>
</div>
<div class="software-list">
<SkeletonCard v-if="loading" :count="4" />
<div v-else 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">
@@ -76,6 +78,7 @@ import { useI18n } from 'vue-i18n'
import { useSoftwareStore } from '../../stores/software.store'
import { getDefaultImageUrl, getLatestStable } from '../../lib/softwareUtils'
import PlatformBadge from '../../components/PlatformBadge.vue'
import SkeletonCard from '../../components/SkeletonCard.vue'
import type { SoftwareEntry } from '../../lib/interfaces/software.interface'
const { t } = useI18n()
@@ -84,7 +87,7 @@ const allFilters = ['all', 'released', 'demo', 'development', 'archived']
const activeFilter = ref('released')
const store = useSoftwareStore()
const { items: softwares } = storeToRefs(store)
const { items: softwares, loading } = storeToRefs(store)
const filters = computed(() => {
const present = new Set(softwares.value.map((e: SoftwareEntry) => e.software.status))
@@ -100,7 +103,7 @@ function getTotalDownloads(name: string): number | null {
return entry?.totalDownloads || null
}
onMounted(() => store.fetchAll())
onMounted(() => store.fetch())
</script>
<style scoped>
@@ -336,7 +336,7 @@ const assetKindIcon = (kind: string) =>
onMounted(async () => {
try {
await store.fetchAll()
await store.fetch()
const item = store.findByName(route.params.name as string)
if (item) {
software.value = item.software
@@ -71,7 +71,7 @@ const { t } = useI18n()
const store = useGitStore()
const { repos: publicRepos, commits: recentCommits, error } = storeToRefs(store)
onMounted(() => store.fetchAll())
onMounted(() => store.fetch())
</script>
<style scoped>
+5 -5
View File
@@ -86,14 +86,14 @@
<div class="yt-content-grid">
<div class="yt-player-container">
<div class="yt-player-wrap">
<div v-if="ytState === 'loading'" class="yt-state-overlay">
<div v-if="ytLoading" class="yt-state-overlay">
<div class="yt-loading-spinner"></div>
</div>
<div v-else-if="ytState === 'error'" class="yt-state-overlay flex-col gap-2">
<div v-else-if="ytError" 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="yt-state-overlay">
<div v-else-if="ytNoConfig" class="yt-state-overlay">
<div class="yt-noconfig-text">{{ t('common.missingApiConfig') }}</div>
</div>
<iframe
@@ -244,12 +244,12 @@ const highlightedImageUrl = computed(() => {
})
const ytStore = useYoutubeStore()
const { state: ytState, videoId: ytVideoId, title: ytTitle, publishDate: ytPublishDate, viewCount: ytViewCount, error: ytError } = storeToRefs(ytStore)
const { loading: ytLoading, error: ytError, noConfig: ytNoConfig, videoId: ytVideoId, title: ytTitle, publishDate: ytPublishDate, viewCount: ytViewCount } = storeToRefs(ytStore)
onMounted(() => {
eventStore.fetch()
softwareStore.fetchHighlighted()
ytStore.fetchLatest()
ytStore.fetch()
})
onUnmounted(() => eventStore.cleanup())
@@ -27,7 +27,9 @@
</div>
</div>
<div v-else-if="!loading && recentPages.length === 0" class="empty-state">
<SkeletonCard v-else-if="loading" :count="4" />
<div v-else-if="recentPages.length === 0" class="empty-state">
<div class="empty-state-icon"><i class="fa-solid fa-inbox"></i></div>
<h2 class="empty-state-title">{{ t('howtos.noPagesTitle') }}</h2>
<p class="empty-state-desc">{{ t('howtos.noPagesDesc') }}</p>
@@ -80,6 +82,7 @@ import { useI18n } from 'vue-i18n'
import { formatDateTime } from '../../lib/dateFormat'
import { WIKI_BASE } from '../../api/wiki.api'
import { useHowtosStore } from '../../stores/howtos.store'
import SkeletonCard from '../../components/SkeletonCard.vue'
const { t } = useI18n()
@@ -87,7 +90,7 @@ const store = useHowtosStore()
const { pages: recentPages, loading, error } = storeToRefs(store)
const { isNew } = store
onMounted(() => store.fetchPages())
onMounted(() => store.fetch())
</script>
<style scoped>
@@ -7,13 +7,15 @@
</header>
<main class="main-container py-16 px-4">
<div class="team-grid">
<SkeletonCard v-if="loading" :count="4" />
<div v-else class="team-grid">
<div v-for="member in members" :key="member.nick" class="team-card">
<div class="team-card-image-container">
<img :src="resolvedAvatarUrl(member)" :alt="member.nick" class="team-card-image" />
</div>
<div class="team-card-content">
<h2 class="team-card-title">{{ member.nick }}: {{ member.real_nick }}</h2>
<h2 class="team-card-title">{{ member.nick }}: {{ member.realNick }}</h2>
<p class="team-card-desc">{{ member.motto }}</p>
</div>
</div>
@@ -26,11 +28,12 @@ import { onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { useMemberStore } from '../../stores/member.store'
import SkeletonCard from '../../components/SkeletonCard.vue'
const { t } = useI18n()
const store = useMemberStore()
const { members } = storeToRefs(store)
const { members, loading } = storeToRefs(store)
const { resolvedAvatarUrl } = store
onMounted(() => store.fetch())
@@ -21,7 +21,7 @@ describe('useSoftwareStore', () => {
it('fetches software items', async () => {
const store = useSoftwareStore()
await store.fetchAll()
await store.fetch()
expect(store.items).toHaveLength(1)
expect(store.items[0].software.name).toBe('test-game')
expect(store.loading).toBe(false)
@@ -29,14 +29,14 @@ describe('useSoftwareStore', () => {
it('caches after first fetch', async () => {
const store = useSoftwareStore()
await store.fetchAll()
await store.fetchAll()
await store.fetch()
await store.fetch()
expect(mockIndex).toHaveBeenCalledOnce()
})
it('findByName returns correct entry', async () => {
const store = useSoftwareStore()
await store.fetchAll()
await store.fetch()
expect(store.findByName('test-game')).toBeDefined()
expect(store.findByName('nonexistent')).toBeUndefined()
})
+7 -17
View File
@@ -10,10 +10,9 @@ export const useBlogStore = defineStore('blog', () => {
const { loading, error, withCache, invalidate } = useLoadable()
const currentPage = ref<WikiPageContent | null>(null)
const currentPageLoading = ref(false)
const currentPageError = ref<string | null>(null)
const { loading: pageLoading, error: pageError, withCache: withPageCache } = useLoadable(0)
async function fetchPages() {
async function fetch() {
await withCache(async () => {
pages.value = await wikiApi.listBlogPages()
})
@@ -21,20 +20,11 @@ export const useBlogStore = defineStore('blog', () => {
async function fetchPage(slug: string) {
currentPage.value = null
currentPageError.value = null
currentPageLoading.value = true
try {
await withPageCache(async () => {
const result = await wikiApi.getBlogPage(slug)
if (result) {
currentPage.value = result
} else {
currentPageError.value = 'Post not found'
}
} catch (e) {
currentPageError.value = e instanceof Error ? e.message : String(e)
} finally {
currentPageLoading.value = false
}
if (!result) throw new Error('Post not found')
currentPage.value = result
})
}
function getPermalink(path: string): string {
@@ -47,5 +37,5 @@ export const useBlogStore = defineStore('blog', () => {
return content.replace(/[#*`_[\]()]/g, '').trim().slice(0, 300) + '...'
}
return { pages, loading, error, currentPage, currentPageLoading, currentPageError, fetchPages, fetchPage, isNew, getPermalink, getCleanPreview, invalidate }
return { pages, loading, error, currentPage, pageLoading, pageError, fetch, fetchPage, isNew, getPermalink, getCleanPreview, invalidate }
})
+2 -2
View File
@@ -9,7 +9,7 @@ export const useGitStore = defineStore('git', () => {
const commits = ref<Commit[]>([])
const { loading, error, withCache, invalidate } = useLoadable()
async function fetchAll() {
async function fetch() {
await withCache(async () => {
repos.value = await gitApi.repos()
@@ -23,5 +23,5 @@ export const useGitStore = defineStore('git', () => {
})
}
return { repos, commits, loading, error, fetchAll, invalidate }
return { repos, commits, loading, error, fetch, invalidate }
})
+2 -2
View File
@@ -9,11 +9,11 @@ export const useHowtosStore = defineStore('howtos', () => {
const pages = ref<WikiPage[]>([])
const { loading, error, withCache, invalidate } = useLoadable()
async function fetchPages() {
async function fetch() {
await withCache(async () => {
pages.value = await wikiApi.listHowtoPages()
})
}
return { pages, loading, error, fetchPages, isNew, invalidate }
return { pages, loading, error, fetch, isNew, invalidate }
})
+2 -2
View File
@@ -15,8 +15,8 @@ export const useMemberStore = defineStore('member', () => {
}
function resolvedAvatarUrl(member: Member): string {
if (member.image_url) return member.image_url
return new URL(`../assets/team/${member.avatar_filename}`, import.meta.url).href
if (member.imageUrl) return member.imageUrl
return new URL(`../assets/team/${member.avatarFilename}`, import.meta.url).href
}
return { members, loading, error, fetch, resolvedAvatarUrl, invalidate }
+5 -6
View File
@@ -9,29 +9,28 @@ export const useSoftwareStore = defineStore('software', () => {
const items = ref<SoftwareEntry[]>([])
const highlighted = ref<SoftwareEntry | null>(null)
const { loading, error, withCache, invalidate } = useLoadable()
const { loading: highlightedLoading, error: highlightedError, withCache: withHighlightedCache } = useLoadable(0)
const highlightedStableRelease = computed((): Release | null => {
if (!highlighted.value?.releases) return null
return getLatestStable(highlighted.value.releases) ?? null
})
async function fetchAll() {
async function fetch() {
await withCache(async () => {
items.value = await softwareApi.index()
})
}
async function fetchHighlighted() {
try {
await withHighlightedCache(async () => {
highlighted.value = await softwareApi.highlighted()
} catch (e) {
error.value = e instanceof Error ? e.message : String(e)
}
})
}
function findByName(name: string): SoftwareEntry | undefined {
return items.value.find(s => s.software.name === name)
}
return { items, highlighted, loading, error, highlightedStableRelease, fetchAll, fetchHighlighted, findByName, invalidate }
return { items, highlighted, loading, error, highlightedLoading, highlightedError, highlightedStableRelease, fetch, fetchHighlighted, findByName, invalidate }
})
+9 -12
View File
@@ -2,14 +2,15 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
import youtubeApi from '../api/youtube.api'
import { formatDateTime } from '../lib/dateFormat'
import { useLoadable } from '../composables/useLoadable'
export const useYoutubeStore = defineStore('youtube', () => {
const state = ref<'loading' | 'loaded' | 'error' | 'no-config'>('loading')
const { loading, error, withCache } = useLoadable(0)
const noConfig = ref(false)
const videoId = ref('')
const title = ref('')
const publishDate = ref('')
const viewCount = ref('')
const error = ref('')
function formatViews(n: number): string {
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M views'
@@ -17,24 +18,20 @@ export const useYoutubeStore = defineStore('youtube', () => {
return n + ' views'
}
async function fetchLatest() {
state.value = 'loading'
try {
async function fetch() {
noConfig.value = false
await withCache(async () => {
const video = await youtubeApi.latestVideo()
if (!video) {
state.value = 'no-config'
noConfig.value = true
return
}
videoId.value = video.id
title.value = video.title
publishDate.value = formatDateTime(video.publishDate)
viewCount.value = video.viewCount ? formatViews(Number(video.viewCount)) : ''
state.value = 'loaded'
} catch (err) {
error.value = 'Error loading video: ' + (err instanceof Error ? err.message : String(err))
state.value = 'error'
}
})
}
return { state, videoId, title, publishDate, viewCount, error, fetchLatest }
return { loading, error, noConfig, videoId, title, publishDate, viewCount, fetch }
})