tweaks
This commit is contained in:
@@ -21,7 +21,7 @@ const commits = async (owner: string, name: string, htmlUrl: string): Promise<Co
|
||||
if (!res.ok) return []
|
||||
const data = await res.json()
|
||||
if (!Array.isArray(data)) return []
|
||||
return data.map((c: any): Commit => ({
|
||||
return data.map((c: Record<string, any>): Commit => ({
|
||||
sha: c.sha,
|
||||
message: c.commit?.message ?? '',
|
||||
author: { name: c.commit?.author?.name ?? 'Unknown', email: c.commit?.author?.email ?? '' },
|
||||
|
||||
@@ -4,25 +4,35 @@ import type { WikiPage, WikiPageWithContent, WikiPageContent } from '../lib/inte
|
||||
// Points at Grav (which is taking over from wiki.teletypegames.org).
|
||||
const WIKI_BASE = import.meta.env.VITE_WIKI_BASE || 'https://wiki.teletypegames.org'
|
||||
|
||||
// Pages come from our own Rails API (same-origin), which proxies the Grav
|
||||
// content backend: GET /api/wiki/pages?tag=<tag>[&limit=<n>][&body=1]
|
||||
interface RawWikiPage {
|
||||
id: number
|
||||
path: string
|
||||
title?: string
|
||||
description?: string
|
||||
content?: string
|
||||
render?: string
|
||||
updatedAt: string
|
||||
createdAt: string
|
||||
locale: string
|
||||
}
|
||||
|
||||
async function fetchPages(
|
||||
tag: string,
|
||||
opts: { body?: boolean; limit?: number } = {},
|
||||
): Promise<any[]> {
|
||||
): Promise<RawWikiPage[]> {
|
||||
const params = new URLSearchParams({ tag })
|
||||
if (opts.body) params.set('body', '1')
|
||||
if (opts.limit) params.set('limit', String(opts.limit))
|
||||
|
||||
const res = await fetch(`/api/wiki/pages?${params.toString()}`)
|
||||
if (!res.ok) return []
|
||||
if (!res.ok) throw new Error(`Wiki API error: ${res.status}`)
|
||||
const json = await res.json()
|
||||
return json?.pages ?? []
|
||||
}
|
||||
|
||||
const listBlogPages = async (): Promise<WikiPageWithContent[]> => {
|
||||
const pages = await fetchPages('blog', { body: true })
|
||||
return pages.map((p: any): WikiPageWithContent => ({
|
||||
return pages.map((p): WikiPageWithContent => ({
|
||||
id: p.id,
|
||||
path: p.path,
|
||||
title: p.title || p.path,
|
||||
@@ -37,7 +47,7 @@ const listBlogPages = async (): Promise<WikiPageWithContent[]> => {
|
||||
const getBlogPage = async (slug: string): Promise<WikiPageContent | null> => {
|
||||
const pages = await fetchPages('blog', { body: true })
|
||||
|
||||
const matched = pages.find((p: any) => {
|
||||
const matched = pages.find((p) => {
|
||||
const pageSlug = p.path.startsWith('blog/') ? p.path.replace('blog/', '') : p.path
|
||||
return pageSlug === slug
|
||||
})
|
||||
@@ -58,7 +68,7 @@ const getBlogPage = async (slug: string): Promise<WikiPageContent | null> => {
|
||||
|
||||
const listHowtoPages = async (): Promise<WikiPage[]> => {
|
||||
const pages = await fetchPages('howto', { limit: 30 })
|
||||
return pages.map((p: any): WikiPage => ({
|
||||
return pages.map((p): WikiPage => ({
|
||||
id: p.id,
|
||||
path: p.path,
|
||||
title: p.title || p.path,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Release, Software } from './interfaces/software.interface'
|
||||
|
||||
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000
|
||||
|
||||
export function isNew(createdAt: string): boolean {
|
||||
return (Date.now() - new Date(createdAt).getTime()) < SEVEN_DAYS_MS
|
||||
}
|
||||
|
||||
export function getDefaultImageUrl(sw: Software): string | null {
|
||||
if (sw.images?.length) {
|
||||
const def = sw.images.find(i => i.isDefault)
|
||||
return def?.url ?? sw.images[0]?.url ?? null
|
||||
}
|
||||
return sw.imageUrl ?? 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())
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
export function getLatestStable(releases: Release[]): Release | undefined {
|
||||
return filterStableReleases(releases)[0]
|
||||
}
|
||||
@@ -69,6 +69,7 @@ import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
import { getDefaultImageUrl, getLatestStable } from '../../lib/softwareUtils'
|
||||
import type { Software, SoftwareEntry } from '../../lib/interfaces/software.interface'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -78,21 +79,12 @@ const activeFilter = ref('released')
|
||||
|
||||
const store = useSoftwareStore()
|
||||
const { items: softwares } = storeToRefs(store)
|
||||
const { getLatestStable } = store
|
||||
|
||||
function getTotalDownloads(name: string): number | null {
|
||||
const entry = softwares.value.find((e: SoftwareEntry) => e.software.name === name)
|
||||
return entry?.totalDownloads || null
|
||||
}
|
||||
|
||||
function getDefaultImageUrl(sw: Software): string | null {
|
||||
if (sw.images?.length) {
|
||||
const def = sw.images.find(i => i.isDefault)
|
||||
return def?.url ?? sw.images[0]?.url ?? null
|
||||
}
|
||||
return sw.imageUrl ?? null
|
||||
}
|
||||
|
||||
onMounted(() => store.fetchAll())
|
||||
</script>
|
||||
|
||||
|
||||
@@ -200,6 +200,7 @@ import { RouterLink, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
import { filterStableReleases, filterDevReleases } from '../../lib/softwareUtils'
|
||||
import type { Release, Software, SoftwareImage } from '../../lib/interfaces/software.interface'
|
||||
import ImageLightbox from './ImageLightbox.vue'
|
||||
|
||||
@@ -238,17 +239,8 @@ function openLightbox(index: number) {
|
||||
lightboxOpen.value = true
|
||||
}
|
||||
|
||||
const stableReleases = computed(() =>
|
||||
releases.value
|
||||
.filter(r => !r.version.startsWith('dev-'))
|
||||
.sort((a, b) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())
|
||||
)
|
||||
|
||||
const devReleases = computed(() =>
|
||||
releases.value
|
||||
.filter(r => r.version.startsWith('dev-'))
|
||||
.sort((a, b) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())
|
||||
)
|
||||
const stableReleases = computed(() => filterStableReleases(releases.value))
|
||||
const devReleases = computed(() => filterDevReleases(releases.value))
|
||||
|
||||
const allReleases = computed(() => [...stableReleases.value, ...devReleases.value])
|
||||
const latestStable = computed(() => stableReleases.value[0] ?? null)
|
||||
|
||||
@@ -227,6 +227,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useEventStore } from '../../stores/event.store'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
import { useYoutubeStore } from '../../stores/youtube.store'
|
||||
import { getDefaultImageUrl } from '../../lib/softwareUtils'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -239,11 +240,7 @@ const { highlighted: highlightedSoftware, highlightedStableRelease } = storeToRe
|
||||
const highlightedImageUrl = computed(() => {
|
||||
const sw = highlightedSoftware.value?.software
|
||||
if (!sw) return null
|
||||
if (sw.images?.length) {
|
||||
const def = sw.images.find(i => i.isDefault)
|
||||
return def?.url ?? sw.images[0]?.url ?? null
|
||||
}
|
||||
return sw.imageUrl ?? null
|
||||
return getDefaultImageUrl(sw)
|
||||
})
|
||||
|
||||
const ytStore = useYoutubeStore()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import wikiApi from '../api/wiki.api'
|
||||
import { isNew } from '../lib/softwareUtils'
|
||||
import type { WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
export const useBlogStore = defineStore('blog', () => {
|
||||
@@ -45,10 +46,6 @@ export const useBlogStore = defineStore('blog', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function isNew(createdAt: string): boolean {
|
||||
return (new Date().getTime() - new Date(createdAt).getTime()) < 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
function getPermalink(path: string): string {
|
||||
const slug = path.startsWith('blog/') ? path.replace('blog/', '') : path
|
||||
return `/blog/${slug}`
|
||||
|
||||
@@ -11,10 +11,15 @@ export const useEventStore = defineStore('event', () => {
|
||||
const nextEvent = ref<EventWithText | null>(null)
|
||||
const followingEvents = ref<FollowingEvent[]>([])
|
||||
const countdownText = ref('-- : -- : -- : --')
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
let countdownInterval: ReturnType<typeof setInterval> | null = null
|
||||
let _loaded = false
|
||||
|
||||
function startCountdown(targetISO: string) {
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
|
||||
const targetDate = new Date(targetISO).getTime()
|
||||
|
||||
function update() {
|
||||
@@ -22,6 +27,7 @@ export const useEventStore = defineStore('event', () => {
|
||||
if (distance < 0) {
|
||||
countdownText.value = 'LIVE NOW!'
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
countdownInterval = null
|
||||
return
|
||||
}
|
||||
const d = String(Math.floor(distance / 86400000)).padStart(2, '0')
|
||||
@@ -36,6 +42,9 @@ export const useEventStore = defineStore('event', () => {
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
if (_loaded) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const events = await eventApi.index()
|
||||
if (events.length > 0) {
|
||||
@@ -45,8 +54,11 @@ export const useEventStore = defineStore('event', () => {
|
||||
followingEvents.value = events.slice(1).map(e => ({ name: e.name, date: e.date, dateText: formatDateTime(new Date(e.date)) }))
|
||||
startCountdown(firstDate.toISOString())
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load events:', e)
|
||||
_loaded = true
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,5 +69,5 @@ export const useEventStore = defineStore('event', () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { nextEvent, followingEvents, countdownText, fetch, cleanup }
|
||||
return { nextEvent, followingEvents, countdownText, loading, error, fetch, cleanup }
|
||||
})
|
||||
|
||||
@@ -6,9 +6,14 @@ import type { GiteaRepo, Commit } from '../lib/interfaces/git.interface'
|
||||
export const useGitStore = defineStore('git', () => {
|
||||
const repos = ref<GiteaRepo[]>([])
|
||||
const commits = ref<Commit[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
let _loaded = false
|
||||
|
||||
async function fetchAll() {
|
||||
if (_loaded) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
repos.value = await gitApi.repos()
|
||||
|
||||
@@ -19,10 +24,13 @@ export const useGitStore = defineStore('git', () => {
|
||||
const all = (await Promise.all(commitPromises)).flat()
|
||||
all.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
commits.value = all.slice(0, 20)
|
||||
_loaded = true
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return { repos, commits, error, fetchAll }
|
||||
return { repos, commits, loading, error, fetchAll }
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import wikiApi from '../api/wiki.api'
|
||||
import { isNew } from '../lib/softwareUtils'
|
||||
import type { WikiPage } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
export const useHowtosStore = defineStore('howtos', () => {
|
||||
@@ -22,9 +23,5 @@ export const useHowtosStore = defineStore('howtos', () => {
|
||||
}
|
||||
}
|
||||
|
||||
function isNew(createdAt: string): boolean {
|
||||
return (new Date().getTime() - new Date(createdAt).getTime()) < 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
return { pages, loading, error, fetchPages, isNew }
|
||||
})
|
||||
|
||||
@@ -5,15 +5,21 @@ import type { Member } from '../lib/interfaces/member.interface'
|
||||
|
||||
export const useMemberStore = defineStore('member', () => {
|
||||
const members = ref<Member[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
let _loaded = false
|
||||
|
||||
async function fetch() {
|
||||
if (_loaded) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
members.value = await memberApi.index()
|
||||
_loaded = true
|
||||
} catch (e) {
|
||||
console.error('Failed to load team members:', e)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,5 +28,5 @@ export const useMemberStore = defineStore('member', () => {
|
||||
return new URL(`../assets/team/${member.avatar_filename}`, import.meta.url).href
|
||||
}
|
||||
|
||||
return { members, fetch, resolvedAvatarUrl }
|
||||
return { members, loading, error, fetch, resolvedAvatarUrl }
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import softwareApi from '../api/software.api'
|
||||
import { getLatestStable } from '../lib/softwareUtils'
|
||||
import type { SoftwareEntry, Release } from '../lib/interfaces/software.interface'
|
||||
|
||||
export const useSoftwareStore = defineStore('software', () => {
|
||||
@@ -10,17 +11,15 @@ export const useSoftwareStore = defineStore('software', () => {
|
||||
const error = ref<string | null>(null)
|
||||
let _loaded = false
|
||||
|
||||
const highlightedStableRelease = computed(() => {
|
||||
const highlightedStableRelease = computed((): Release | null => {
|
||||
if (!highlighted.value?.releases) return null
|
||||
const stable = highlighted.value.releases
|
||||
.filter((r: Release) => !r.version.startsWith('dev-'))
|
||||
.sort((a: Release, b: Release) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())
|
||||
return stable[0] ?? null
|
||||
return getLatestStable(highlighted.value.releases) ?? null
|
||||
})
|
||||
|
||||
async function fetchAll() {
|
||||
if (_loaded) return
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
items.value = await softwareApi.index()
|
||||
_loaded = true
|
||||
@@ -34,8 +33,8 @@ export const useSoftwareStore = defineStore('software', () => {
|
||||
async function fetchHighlighted() {
|
||||
try {
|
||||
highlighted.value = await softwareApi.highlighted()
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch highlighted software:', e)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +42,5 @@ export const useSoftwareStore = defineStore('software', () => {
|
||||
return items.value.find(s => s.software.name === name)
|
||||
}
|
||||
|
||||
function getLatestStable(releases: Release[]): Release {
|
||||
return releases
|
||||
.filter(r => !r.version.startsWith('dev-'))
|
||||
.sort((a, b) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())[0]
|
||||
}
|
||||
|
||||
return { items, highlighted, loading, error, highlightedStableRelease, fetchAll, fetchHighlighted, findByName, getLatestStable }
|
||||
return { items, highlighted, loading, error, highlightedStableRelease, fetchAll, fetchHighlighted, findByName }
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import youtubeApi from '../api/youtube.api'
|
||||
import { formatDateTime } from '../lib/dateFormat'
|
||||
|
||||
export const useYoutubeStore = defineStore('youtube', () => {
|
||||
const state = ref<'loading' | 'loaded' | 'error' | 'no-config'>('loading')
|
||||
@@ -10,11 +11,6 @@ export const useYoutubeStore = defineStore('youtube', () => {
|
||||
const viewCount = ref('')
|
||||
const error = ref('')
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function formatViews(n: number): string {
|
||||
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M views'
|
||||
if (n >= 1_000) return Math.round(n / 1_000) + 'K views'
|
||||
@@ -31,7 +27,7 @@ export const useYoutubeStore = defineStore('youtube', () => {
|
||||
}
|
||||
videoId.value = video.id
|
||||
title.value = video.title
|
||||
publishDate.value = formatDate(video.publishDate)
|
||||
publishDate.value = formatDateTime(video.publishDate)
|
||||
viewCount.value = video.viewCount ? formatViews(Number(video.viewCount)) : ''
|
||||
state.value = 'loaded'
|
||||
} catch (err: any) {
|
||||
|
||||
Reference in New Issue
Block a user