frontend refact

This commit is contained in:
2026-05-05 22:31:31 +02:00
parent d544b4da76
commit dd1603027c
37 changed files with 465 additions and 466 deletions
+1 -1
View File
@@ -5,6 +5,6 @@
</template>
<script setup lang="ts">
import AppLayout from './components/AppLayout.vue'
import AppLayout from './layout/AppLayout.vue'
import { RouterView } from 'vue-router'
</script>
+9
View File
@@ -0,0 +1,9 @@
import type { Event } from '../lib/interfaces/event.interface'
const index = async (): Promise<Event[]> => {
const res = await fetch('/api/events')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
export default { index }
+37
View File
@@ -0,0 +1,37 @@
import type { GiteaRepo, Commit } from '../lib/interfaces/git.interface'
const BASE = import.meta.env.VITE_GIT_BASE || 'https://git.teletype.hu/api/v1'
const TOKEN = import.meta.env.WEBAPP_GITEA_TOKEN
function buildHeaders() {
return { 'Authorization': `token ${TOKEN}`, 'Accept': 'application/json' }
}
const repos = async (): Promise<GiteaRepo[]> => {
if (!TOKEN) throw new Error('Gitea API token (WEBAPP_GITEA_TOKEN) is not configured.')
const res = await fetch(`${BASE}/repos/search?q=&private=false&limit=50`, { headers: buildHeaders() })
if (!res.ok) throw new Error(`Gitea API responded with status ${res.status}`)
const data = await res.json()
return data.data || []
}
const commits = async (owner: string, name: string, htmlUrl: string): Promise<Commit[]> => {
try {
const res = await fetch(`${BASE}/repos/${owner}/${name}/commits?limit=10&page=1`, { headers: buildHeaders() })
if (!res.ok) return []
const data = await res.json()
if (!Array.isArray(data)) return []
return data.map((c: any): Commit => ({
sha: c.sha,
message: c.commit?.message ?? '',
author: { name: c.commit?.author?.name ?? 'Unknown', email: c.commit?.author?.email ?? '' },
date: c.commit?.author?.date ?? c.created,
url: c.html_url,
repo: { owner, name, html_url: htmlUrl },
}))
} catch {
return []
}
}
export default { repos, commits }
+9
View File
@@ -0,0 +1,9 @@
import type { Member } from '../lib/interfaces/member.interface'
const index = async (): Promise<Member[]> => {
const res = await fetch('/api/members')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
return res.json()
}
export default { index }
+16
View File
@@ -0,0 +1,16 @@
import type { SoftwareEntry } from '../lib/interfaces/software.interface'
const index = async (): Promise<SoftwareEntry[]> => {
const res = await fetch('/api/software')
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const json = await res.json()
return json.softwares
}
const highlighted = async (): Promise<SoftwareEntry | null> => {
const res = await fetch('/api/software/highlighted')
if (!res.ok) return null
return res.json()
}
export default { index, highlighted }
+102
View File
@@ -0,0 +1,102 @@
import type { WikiPage, WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface'
const BASE = import.meta.env.VITE_WIKI_BASE || 'https://wiki.teletype.hu'
const TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN
function buildHeaders(): Record<string, string> {
const h: Record<string, string> = { 'Content-Type': 'application/json', 'Accept': 'application/json' }
if (TOKEN) h['Authorization'] = `Bearer ${TOKEN}`
return h
}
async function gql(query: string): Promise<any> {
const res = await fetch(`${BASE}/graphql`, {
method: 'POST',
headers: buildHeaders(),
body: JSON.stringify({ query }),
})
const json = await res.json()
if (json.errors) throw new Error(json.errors.map((e: any) => e.message).join(', '))
return json.data
}
const listBlogPages = async (): Promise<WikiPageWithContent[]> => {
const data = await gql(`{
pages {
list(orderBy: CREATED, orderByDirection: DESC, tags: ["blog"]) {
id path title description updatedAt createdAt locale
}
}
}`)
const pages: any[] = data?.pages?.list ?? []
const pagesWithContent = await Promise.all(pages.map(async (p: any) => {
try {
const contentData = await gql(`{ pages { single(id: ${p.id}) { content } } }`)
return { ...p, content: contentData?.pages?.single?.content ?? '' }
} catch {
return { ...p, content: '' }
}
}))
return pagesWithContent.map((p: any): WikiPageWithContent => ({
id: p.id,
path: p.path,
title: p.title || p.path,
description: p.description ?? '',
content: p.content ?? '',
updatedAt: p.updatedAt,
createdAt: p.createdAt,
locale: p.locale,
}))
}
const getBlogPage = async (slug: string): Promise<WikiPageContent | null> => {
const data = await gql(`{
pages {
list(orderBy: CREATED, orderByDirection: DESC, tags: ["blog"]) {
id path
}
}
}`)
const pages: any[] = data?.pages?.list ?? []
const matched = pages.find((p: any) => {
const pageSlug = p.path.startsWith('blog/') ? p.path.replace('blog/', '') : p.path
return pageSlug === slug
})
if (!matched) return null
const singleData = await gql(`{
pages {
single(id: ${matched.id}) {
id path title description render updatedAt createdAt locale
}
}
}`)
return singleData?.pages?.single ?? null
}
const listHowtoPages = async (): Promise<WikiPage[]> => {
const data = await gql(`{
pages {
list(orderBy: UPDATED, orderByDirection: DESC, tags: ["howto"]) {
id path title description updatedAt createdAt locale
}
}
}`)
const pages: any[] = data?.pages?.list ?? []
return pages.map((p: any): WikiPage => ({
id: p.id,
path: p.path,
title: p.title || p.path,
description: p.description ?? '',
updatedAt: p.updatedAt,
createdAt: p.createdAt,
locale: p.locale,
})).slice(0, 30)
}
export { BASE as WIKI_BASE }
export default { listBlogPages, getBlogPage, listHowtoPages }
+34
View File
@@ -0,0 +1,34 @@
import type { YoutubeVideo } from '../lib/interfaces/youtube.interface'
const API_KEY = import.meta.env.YOUTUBE_API_KEY
const CHANNEL_ID = import.meta.env.YOUTUBE_CHANNEL_ID
const latestVideo = async (): Promise<YoutubeVideo | null> => {
if (!API_KEY || !CHANNEL_ID) return null
const searchRes = await fetch(
`https://www.googleapis.com/youtube/v3/search?key=${API_KEY}&channelId=${CHANNEL_ID}&part=snippet&order=date&maxResults=1&type=video`
)
if (!searchRes.ok) throw new Error(`API error: ${searchRes.status}`)
const searchData = await searchRes.json()
if (!searchData.items?.length) throw new Error('No videos found on this channel.')
const item = searchData.items[0]
const videoId = item.id.videoId
const statsRes = await fetch(
`https://www.googleapis.com/youtube/v3/videos?key=${API_KEY}&id=${videoId}&part=statistics`
)
const statsData = await statsRes.json()
const views = statsData.items?.[0]?.statistics?.viewCount ?? null
return {
id: videoId,
title: item.snippet.title,
publishDate: item.snippet.publishedAt,
viewCount: views ? views.toString() : '',
}
}
export default { latestVideo }
View File
-3
View File
@@ -1,3 +0,0 @@
export const GAMES_BASE = ''
export const WIKI_BASE = import.meta.env.VITE_WIKI_BASE || 'https://wiki.teletype.hu'
export const GIT_BASE = import.meta.env.VITE_GIT_BASE || 'https://git.teletype.hu/api/v1'
+20
View File
@@ -0,0 +1,20 @@
export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm'
export const DATE_FORMAT = 'YYYY-MM-DD'
export function formatDateTime(dateStr: string | Date): string {
const d = new Date(dateStr as string)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
export function formatDate(dateStr: string | Date): string {
const d = new Date(dateStr as string)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
@@ -0,0 +1,4 @@
export interface Event {
name: string
date: string
}
@@ -0,0 +1,17 @@
export interface GiteaRepo {
owner: { login: string }
name: string
description: string
language?: string
html_url: string
updated_at: string
}
export interface Commit {
sha: string
message: string
author: { name: string; email: string }
date: string
url: string
repo: { owner: string; name: string; html_url: string }
}
@@ -0,0 +1,6 @@
export interface Member {
nick: string
real_nick: string
motto: string
avatar_filename: string
}
@@ -0,0 +1,25 @@
export interface Release {
version: string
htmlFolderPath?: string
cartridgePath?: string
sourcePath?: string
docsFolderPath?: string
UpdatedAt: string
}
export interface Software {
name: string
title: string
desc: string
status: string
author: string
platform: string
license?: string
story?: string
externalLinks?: { label: string; url: string }[]
}
export interface SoftwareEntry {
software: Software
releases: Release[]
}
@@ -0,0 +1,17 @@
export interface WikiPage {
id: number
path: string
title: string
description: string
updatedAt: string
createdAt: string
locale: string
}
export interface WikiPageWithContent extends WikiPage {
content: string
}
export interface WikiPageContent extends WikiPage {
render: string
}
@@ -0,0 +1,6 @@
export interface YoutubeVideo {
id: string
title: string
publishDate: string
viewCount: string
}
+1 -1
View File
@@ -1,5 +1,5 @@
import { createApp } from 'vue'
import { router } from './router'
import { router } from './router/index.router'
import App from './App.vue'
import './styles/global.css'
@@ -76,24 +76,11 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import { formatDateTime } from '../utils/dateFormat'
import { WIKI_BASE } from '../config'
import { formatDateTime } from '../../lib/dateFormat'
import wikiApi from '../../api/wiki.api'
import type { WikiPageWithContent } from '../../lib/interfaces/wiki.interface'
const WIKIJS_BASE_URL = WIKI_BASE
const WIKIJS_TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN
interface WikiPage {
id: number
path: string
title: string
description: string
content: string
updatedAt: string
createdAt: string
locale: string
}
const blogPages = ref<WikiPage[]>([])
const blogPages = ref<WikiPageWithContent[]>([])
const error = ref<string | null>(null)
const loading = ref(true)
@@ -112,60 +99,8 @@ function getCleanPreview(content: string): string {
}
onMounted(async () => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
if (WIKIJS_TOKEN) {
headers['Authorization'] = `Bearer ${WIKIJS_TOKEN}`
}
const LIST_QUERY = `
{
pages {
list(orderBy: CREATED, orderByDirection: DESC, tags: ["blog"]) {
id path title description updatedAt createdAt locale
}
}
}
`
try {
const listRes = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: LIST_QUERY }),
})
const listJson = await listRes.json()
if (listJson.errors) throw new Error(`GraphQL error: ${listJson.errors.map((e: any) => e.message).join(', ')}`)
const pages = listJson?.data?.pages?.list ?? []
const pagesWithContent = await Promise.all(pages.map(async (p: any) => {
try {
const CONTENT_QUERY = `{ pages { single(id: ${p.id}) { content } } }`
const contentRes = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: CONTENT_QUERY }),
})
const contentJson = await contentRes.json()
return { ...p, content: contentJson?.data?.pages?.single?.content ?? '' }
} catch {
return { ...p, content: '' }
}
}))
blogPages.value = pagesWithContent.map((p: any) => ({
id: p.id,
path: p.path,
title: p.title || p.path,
description: p.description ?? '',
content: p.content ?? '',
updatedAt: p.updatedAt,
createdAt: p.createdAt,
locale: p.locale,
}))
blogPages.value = await wikiApi.listBlogPages()
} catch (e: any) {
error.value = `Failed to fetch wiki data: ${e.message}`
} finally {
@@ -42,85 +42,22 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import { formatDateTime } from '../utils/dateFormat'
import { WIKI_BASE } from '../config'
const WIKIJS_BASE_URL = WIKI_BASE
const WIKIJS_TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN
interface PageContent {
id: number
path: string
title: string
description: string
render: string
createdAt: string
updatedAt: string
}
import { formatDateTime } from '../../lib/dateFormat'
import wikiApi from '../../api/wiki.api'
import type { WikiPageContent } from '../../lib/interfaces/wiki.interface'
const route = useRoute()
const pageContent = ref<PageContent | null>(null)
const pageContent = ref<WikiPageContent | null>(null)
const error = ref<string | null>(null)
const loading = ref(true)
onMounted(async () => {
const slug = route.params.slug as string
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
if (WIKIJS_TOKEN) {
headers['Authorization'] = `Bearer ${WIKIJS_TOKEN}`
}
try {
// Find page ID by fetching the list and matching the slug
const LIST_QUERY = `
{
pages {
list(orderBy: CREATED, orderByDirection: DESC, tags: ["blog"]) {
id path
}
}
}
`
const listRes = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: LIST_QUERY }),
})
const listJson = await listRes.json()
const pages: any[] = listJson?.data?.pages?.list ?? []
const matchedPage = pages.find((p: any) => {
const pageSlug = p.path.startsWith('blog/') ? p.path.replace('blog/', '') : p.path
return pageSlug === slug
})
if (!matchedPage) {
error.value = 'Post not found'
return
}
const SINGLE_QUERY = `
{
pages {
single(id: ${matchedPage.id}) {
id path title description render updatedAt createdAt
}
}
}
`
const singleRes = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: SINGLE_QUERY }),
})
const singleJson = await singleRes.json()
pageContent.value = singleJson?.data?.pages?.single ?? null
if (!pageContent.value) {
const result = await wikiApi.getBlogPage(slug)
if (result) {
pageContent.value = result
} else {
error.value = 'Post not found'
}
} catch (e: any) {
@@ -41,7 +41,7 @@
<div class="flex flex-wrap gap-2 items-center">
<a
v-if="getLatestStable(releases)?.htmlFolderPath"
:href="BASE + getLatestStable(releases).htmlFolderPath"
:href="getLatestStable(releases).htmlFolderPath"
target="_blank"
class="btn-play-sm"
> Play</a>
@@ -60,31 +60,12 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import { GAMES_BASE } from '../config'
import softwareApi from '../../api/software.api'
import type { Release, SoftwareEntry } from '../../lib/interfaces/software.interface'
const BASE = GAMES_BASE
const filters = ['all', 'released', 'demo', 'development', 'archived']
const activeFilter = ref('all')
interface Release {
version: string
htmlFolderPath?: string
cartridgePath?: string
sourcePath?: string
docsFolderPath?: string
UpdatedAt: string
}
interface Software {
name: string
title: string
desc: string
status: string
author: string
platform: string
}
const softwares = ref<{ software: Software; releases: Release[] }[]>([])
const softwares = ref<SoftwareEntry[]>([])
function getLatestStable(releases: Release[]): Release {
return releases
@@ -94,9 +75,7 @@ function getLatestStable(releases: Release[]): Release {
onMounted(async () => {
try {
const res = await fetch(`${BASE}/api/software`)
const json = await res.json()
softwares.value = json.softwares
softwares.value = await softwareApi.index()
} catch (e) {
console.error('Failed to load catalog:', e)
}
@@ -73,10 +73,10 @@
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-2 xl:grid-cols-4 gap-2 w-full md:w-auto">
<a v-if="latestStable.htmlFolderPath" :href="BASE + latestStable.htmlFolderPath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:bg-indigo-50 shadow-lg shadow-black/10 text-sm whitespace-nowrap"> Play Now</a>
<a v-if="latestStable.cartridgePath" :href="BASE + latestStable.cartridgePath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">💾 Download</a>
<a v-if="latestStable.sourcePath" :href="BASE + latestStable.sourcePath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">📄 Source</a>
<a v-if="latestStable.docsFolderPath" :href="BASE + latestStable.docsFolderPath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">📖 Docs</a>
<a v-if="latestStable.htmlFolderPath" :href="latestStable.htmlFolderPath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-white text-indigo-900 font-bold rounded-xl transition-all hover:bg-indigo-50 shadow-lg shadow-black/10 text-sm whitespace-nowrap"> Play Now</a>
<a v-if="latestStable.cartridgePath" :href="latestStable.cartridgePath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">💾 Download</a>
<a v-if="latestStable.sourcePath" :href="latestStable.sourcePath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">📄 Source</a>
<a v-if="latestStable.docsFolderPath" :href="latestStable.docsFolderPath" target="_blank" class="flex items-center justify-center py-3 px-4 bg-indigo-500/30 border border-indigo-400/30 text-white font-bold rounded-xl transition-all hover:bg-indigo-500/40 text-sm whitespace-nowrap">📖 Docs</a>
</div>
</div>
</div>
@@ -100,10 +100,10 @@
<td class="px-8 py-4">
<div class="font-bold text-gray-900 text-lg mb-2">{{ release.version }}</div>
<div class="flex flex-wrap gap-4">
<a v-if="release.htmlFolderPath" :href="BASE + release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm flex items-center gap-1"> Play</a>
<a v-if="release.cartridgePath" :href="BASE + release.cartridgePath" target="_blank" class="text-blue-600 hover:text-blue-900 font-bold text-sm flex items-center gap-1">💾 Download</a>
<a v-if="release.sourcePath" :href="BASE + release.sourcePath" target="_blank" class="text-green-600 hover:text-green-900 font-bold text-sm flex items-center gap-1">📄 Source</a>
<a v-if="release.docsFolderPath" :href="BASE + release.docsFolderPath" target="_blank" class="text-yellow-600 hover:text-yellow-900 font-bold text-sm flex items-center gap-1">📖 Docs</a>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm flex items-center gap-1"> Play</a>
<a v-if="release.cartridgePath" :href="release.cartridgePath" target="_blank" class="text-blue-600 hover:text-blue-900 font-bold text-sm flex items-center gap-1">💾 Download</a>
<a v-if="release.sourcePath" :href="release.sourcePath" target="_blank" class="text-green-600 hover:text-green-900 font-bold text-sm flex items-center gap-1">📄 Source</a>
<a v-if="release.docsFolderPath" :href="release.docsFolderPath" target="_blank" class="text-yellow-600 hover:text-yellow-900 font-bold text-sm flex items-center gap-1">📖 Docs</a>
</div>
</td>
<td class="px-8 py-4 text-gray-500 align-top pt-5">{{ formatDateTime(release.UpdatedAt) }}</td>
@@ -117,8 +117,8 @@
<td class="px-8 py-4">
<div class="font-bold text-yellow-800 text-lg mb-2">{{ release.version }}</div>
<div class="flex flex-wrap gap-4">
<a v-if="release.htmlFolderPath" :href="BASE + release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm flex items-center gap-1"> Play</a>
<a v-if="release.cartridgePath" :href="BASE + release.cartridgePath" target="_blank" class="text-blue-600 hover:text-blue-900 font-bold text-sm flex items-center gap-1">💾 Download</a>
<a v-if="release.htmlFolderPath" :href="release.htmlFolderPath" target="_blank" class="text-purple-600 hover:text-purple-900 font-bold text-sm flex items-center gap-1"> Play</a>
<a v-if="release.cartridgePath" :href="release.cartridgePath" target="_blank" class="text-blue-600 hover:text-blue-900 font-bold text-sm flex items-center gap-1">💾 Download</a>
</div>
</td>
<td class="px-8 py-4 text-gray-500 align-top pt-5">{{ formatDateTime(release.UpdatedAt) }}</td>
@@ -142,10 +142,10 @@
<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>
</div>
<div class="grid grid-cols-2 gap-2">
<a v-if="release.htmlFolderPath" :href="BASE + release.htmlFolderPath" target="_blank" class="flex items-center justify-center py-2 bg-purple-100 text-purple-700 font-bold rounded-lg text-sm">Play</a>
<a v-if="release.cartridgePath" :href="BASE + release.cartridgePath" target="_blank" class="flex items-center justify-center py-2 bg-blue-100 text-blue-700 font-bold rounded-lg text-sm">Download</a>
<a v-if="release.sourcePath" :href="BASE + release.sourcePath" target="_blank" class="flex items-center justify-center py-2 bg-green-100 text-green-700 font-bold rounded-lg text-sm">Source</a>
<a v-if="release.docsFolderPath" :href="BASE + release.docsFolderPath" target="_blank" class="flex items-center justify-center py-2 bg-yellow-100 text-yellow-700 font-bold rounded-lg text-sm">Docs</a>
<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">Play</a>
<a v-if="release.cartridgePath" :href="release.cartridgePath" target="_blank" class="flex items-center justify-center py-2 bg-blue-100 text-blue-700 font-bold rounded-lg text-sm">Download</a>
<a v-if="release.sourcePath" :href="release.sourcePath" target="_blank" class="flex items-center justify-center py-2 bg-green-100 text-green-700 font-bold rounded-lg text-sm">Source</a>
<a v-if="release.docsFolderPath" :href="release.docsFolderPath" target="_blank" class="flex items-center justify-center py-2 bg-yellow-100 text-yellow-700 font-bold rounded-lg text-sm">Docs</a>
</div>
</div>
</div>
@@ -166,34 +166,12 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import { formatDateTime } from '../utils/dateFormat'
import { GAMES_BASE } from '../config'
import { formatDateTime } from '../../lib/dateFormat'
import softwareApi from '../../api/software.api'
import type { Release, Software } from '../../lib/interfaces/software.interface'
const BASE = GAMES_BASE
const route = useRoute()
interface Release {
version: string
htmlFolderPath?: string
cartridgePath?: string
sourcePath?: string
docsFolderPath?: string
UpdatedAt: string
}
interface SoftwareItem {
name: string
title: string
desc: string
status: string
author: string
platform: string
license?: string
story?: string
externalLinks?: { label: string; url: string }[]
}
const software = ref<SoftwareItem | null>(null)
const software = ref<Software | null>(null)
const releases = ref<Release[]>([])
const error = ref<string | null>(null)
@@ -210,14 +188,12 @@ const devReleases = computed(() =>
)
const allReleases = computed(() => [...stableReleases.value, ...devReleases.value])
const latestStable = computed(() => stableReleases.value[0] ?? null)
onMounted(async () => {
try {
const res = await fetch(`${BASE}/api/software`)
const json = await res.json()
const item = json.softwares.find((s: any) => s.software.name === route.params.name)
const all = await softwareApi.index()
const item = all.find(s => s.software.name === route.params.name)
if (item) {
software.value = item.software
releases.value = item.releases
@@ -60,79 +60,27 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { formatDateTime } from '../utils/dateFormat'
import { GIT_BASE } from '../config'
const GITEA_API_BASE_URL = GIT_BASE
const GITEA_TOKEN = import.meta.env.WEBAPP_GITEA_TOKEN
interface GiteaRepo {
owner: { login: string }
name: string
description: string
language?: string
html_url: string
updated_at: string
}
interface Commit {
sha: string
message: string
author: { name: string; email: string }
date: string
url: string
repo: { owner: string; name: string; html_url: string }
}
import { formatDateTime } from '../../lib/dateFormat'
import gitApi from '../../api/git.api'
import type { GiteaRepo, Commit } from '../../lib/interfaces/git.interface'
const publicRepos = ref<GiteaRepo[]>([])
const recentCommits = ref<Commit[]>([])
const error = ref<string | null>(null)
onMounted(async () => {
if (!GITEA_TOKEN) {
error.value = 'Gitea API token (WEBAPP_GITEA_TOKEN) is not configured.'
return
}
const headers = {
'Authorization': `token ${GITEA_TOKEN}`,
'Accept': 'application/json',
}
try {
const reposRes = await fetch(`${GITEA_API_BASE_URL}/repos/search?q=&private=false&limit=50`, { headers })
if (!reposRes.ok) throw new Error(`Gitea API responded with status ${reposRes.status}`)
publicRepos.value = await gitApi.repos()
const reposData = await reposRes.json()
publicRepos.value = reposData.data || []
const commitPromises = publicRepos.value.map(async (repo) => {
try {
const res = await fetch(
`${GITEA_API_BASE_URL}/repos/${repo.owner.login}/${repo.name}/commits?limit=10&page=1`,
{ headers }
)
if (!res.ok) return []
const data = await res.json()
if (!Array.isArray(data)) return []
return data.map((c: any): Commit => ({
sha: c.sha,
message: c.commit?.message ?? '',
author: { name: c.commit?.author?.name ?? 'Unknown', email: c.commit?.author?.email ?? '' },
date: c.commit?.author?.date ?? c.created,
url: c.html_url,
repo: { owner: repo.owner.login, name: repo.name, html_url: repo.html_url },
}))
} catch {
return []
}
})
const commitPromises = publicRepos.value.map(repo =>
gitApi.commits(repo.owner.login, repo.name, repo.html_url)
)
const all = (await Promise.all(commitPromises)).flat()
all.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
recentCommits.value = all.slice(0, 20)
} catch (e: any) {
error.value = `Failed to fetch Gitea data: ${e.message}`
error.value = e.message
}
})
</script>
@@ -53,7 +53,7 @@
<div class="flex flex-wrap gap-4 mt-8">
<a
v-if="highlightedStableRelease?.htmlFolderPath"
:href="BASE + highlightedStableRelease.htmlFolderPath"
:href="highlightedStableRelease.htmlFolderPath"
target="_blank"
class="featured-btn-play"
> Play in Browser</a>
@@ -186,19 +186,17 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { RouterLink } from 'vue-router'
import { formatDateTime } from '../utils/dateFormat'
import { GAMES_BASE } from '../config'
const BASE = GAMES_BASE
const YOUTUBE_API_KEY = import.meta.env.YOUTUBE_API_KEY
const YOUTUBE_CHANNEL_ID = import.meta.env.YOUTUBE_CHANNEL_ID
interface Event { name: string; date: string }
import { formatDateTime } from '../../lib/dateFormat'
import eventApi from '../../api/event.api'
import softwareApi from '../../api/software.api'
import youtubeApi from '../../api/youtube.api'
import type { Event } from '../../lib/interfaces/event.interface'
import type { SoftwareEntry, Release } from '../../lib/interfaces/software.interface'
const nextEvent = ref<(Event & { dateISO: string; dateText: string }) | null>(null)
const followingEvents = ref<(Event & { dateText: string })[]>([])
const highlightedSoftware = ref<any>(null)
const highlightedStableRelease = ref<any>(null)
const highlightedSoftware = ref<SoftwareEntry | null>(null)
const highlightedStableRelease = ref<Release | null>(null)
const countdownText = ref('-- : -- : -- : --')
const ytState = ref<'loading' | 'loaded' | 'error' | 'no-config'>('loading')
@@ -242,89 +240,48 @@ function formatViews(n: number): string {
return n + ' views'
}
async function loadLatestVideo() {
if (!YOUTUBE_API_KEY || !YOUTUBE_CHANNEL_ID) {
ytState.value = 'no-config'
return
}
try {
const searchRes = await fetch(`https://www.googleapis.com/youtube/v3/search?key=${YOUTUBE_API_KEY}&channelId=${YOUTUBE_CHANNEL_ID}&part=snippet&order=date&maxResults=1&type=video`)
if (!searchRes.ok) throw new Error(`API error: ${searchRes.status}`)
const searchData = await searchRes.json()
if (!searchData.items?.length) {
ytError.value = 'No videos found on this channel.'
ytState.value = 'error'
return
}
const item = searchData.items[0]
const videoId = item.id.videoId
const title = item.snippet.title
const pubDate = item.snippet.publishedAt
const statsRes = await fetch(`https://www.googleapis.com/youtube/v3/videos?key=${YOUTUBE_API_KEY}&id=${videoId}&part=statistics`)
const statsData = await statsRes.json()
const views = statsData.items?.[0]?.statistics?.viewCount ?? null
ytVideoId.value = videoId
ytTitle.value = title
ytPublishDate.value = formatYtDate(pubDate)
ytViewCount.value = views ? formatViews(Number(views)) : ''
ytState.value = 'loaded'
} catch (err: any) {
ytError.value = 'Error loading video: ' + err.message
ytState.value = 'error'
}
}
onMounted(async () => {
// Load events
try {
const res = await fetch(BASE + '/api/events')
if (res.ok) {
const events: Event[] = await res.json()
if (events.length > 0) {
const first = events[0]
const firstDate = new Date(first.date)
nextEvent.value = {
name: first.name,
date: first.date,
dateISO: firstDate.toISOString(),
dateText: formatDateTime(firstDate),
}
followingEvents.value = events.slice(1).map(e => ({
name: e.name,
date: e.date,
dateText: formatDateTime(new Date(e.date)),
}))
startCountdown(firstDate.toISOString())
}
const events = await eventApi.index()
if (events.length > 0) {
const first = events[0]
const firstDate = new Date(first.date)
nextEvent.value = { name: first.name, date: first.date, dateISO: firstDate.toISOString(), dateText: formatDateTime(firstDate) }
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)
}
// Load highlighted software
try {
const res = await fetch(BASE + '/api/software/highlighted')
if (res.ok) {
const data = await res.json()
highlightedSoftware.value = data
if (data?.releases) {
const stable = data.releases
.filter((r: any) => !r.version.startsWith('dev-'))
.sort((a: any, b: any) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())
highlightedStableRelease.value = stable[0] ?? null
}
const data = await softwareApi.highlighted()
highlightedSoftware.value = data
if (data?.releases) {
const stable = data.releases
.filter((r: Release) => !r.version.startsWith('dev-'))
.sort((a: Release, b: Release) => new Date(b.UpdatedAt).getTime() - new Date(a.UpdatedAt).getTime())
highlightedStableRelease.value = stable[0] ?? null
}
} catch (e) {
console.error('Failed to fetch highlighted software:', e)
}
// Load YouTube
await loadLatestVideo()
try {
const video = await youtubeApi.latestVideo()
if (!video) {
ytState.value = 'no-config'
return
}
ytVideoId.value = video.id
ytTitle.value = video.title
ytPublishDate.value = formatYtDate(video.publishDate)
ytViewCount.value = video.viewCount ? formatViews(Number(video.viewCount)) : ''
ytState.value = 'loaded'
} catch (err: any) {
ytError.value = 'Error loading video: ' + err.message
ytState.value = 'error'
}
})
onUnmounted(() => {
@@ -14,7 +14,7 @@
<p class="hero-subtitle mb-10">
The latest documentation, technical guides, and development logs from our knowledge base.
</p>
<a :href="WIKIJS_BASE_URL" target="_blank" class="btn-hero-indigo">
<a :href="WIKI_BASE" target="_blank" class="btn-hero-indigo">
Knowledge Base
</a>
</div>
@@ -39,7 +39,7 @@
<a
v-for="page in recentPages"
:key="page.id"
:href="`${WIKIJS_BASE_URL}/${page.locale}/${page.path}`"
:href="`${WIKI_BASE}/${page.locale}/${page.path}`"
target="_blank"
class="group card-interactive flex flex-col"
>
@@ -66,7 +66,7 @@
</div>
<div v-if="!error && recentPages.length > 0" class="howtos-footer">
<a :href="WIKIJS_BASE_URL" target="_blank" class="browse-wiki-btn">
<a :href="WIKI_BASE" target="_blank" class="browse-wiki-btn">
Browse Wiki Home
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" />
@@ -79,21 +79,9 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { formatDateTime } from '../utils/dateFormat'
import { WIKI_BASE } from '../config'
const WIKIJS_BASE_URL = WIKI_BASE
const WIKIJS_TOKEN = import.meta.env.WEBAPP_WIKIJS_TOKEN
interface WikiPage {
id: number
path: string
title: string
description: string
updatedAt: string
createdAt: string
locale: string
}
import { formatDateTime } from '../../lib/dateFormat'
import wikiApi, { WIKI_BASE } from '../../api/wiki.api'
import type { WikiPage } from '../../lib/interfaces/wiki.interface'
const recentPages = ref<WikiPage[]>([])
const error = ref<string | null>(null)
@@ -104,46 +92,8 @@ function isNew(createdAt: string): boolean {
}
onMounted(async () => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
if (WIKIJS_TOKEN) {
headers['Authorization'] = `Bearer ${WIKIJS_TOKEN}`
}
const GRAPHQL_QUERY = `
{
pages {
list(orderBy: UPDATED, orderByDirection: DESC, tags: ["howto"]) {
id path title description updatedAt createdAt locale
}
}
}
`
try {
const response = await fetch(`${WIKIJS_BASE_URL}/graphql`, {
method: 'POST',
headers,
body: JSON.stringify({ query: GRAPHQL_QUERY }),
})
if (!response.ok) throw new Error(`WikiJS responded with status ${response.status}`)
const json = await response.json()
if (json.errors) throw new Error(`GraphQL error: ${json.errors.map((e: any) => e.message).join(', ')}`)
const pages: any[] = json?.data?.pages?.list ?? []
recentPages.value = pages.map((p: any) => ({
id: p.id,
path: p.path,
title: p.title || p.path,
description: p.description ?? '',
updatedAt: p.updatedAt,
createdAt: p.createdAt,
locale: p.locale,
})).slice(0, 30)
recentPages.value = await wikiApi.listHowtoPages()
} catch (e: any) {
error.value = `Failed to fetch wiki data: ${e.message}`
} finally {
@@ -25,23 +25,21 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
interface Member {
nick: string
real_nick: string
motto: string
avatar_filename: string
}
import memberApi from '../../api/member.api'
import type { Member } from '../../lib/interfaces/member.interface'
const members = ref<Member[]>([])
function avatarUrl(filename: string): string {
return new URL(`../assets/team/${filename}`, import.meta.url).href
return new URL(`../../assets/team/${filename}`, import.meta.url).href
}
onMounted(async () => {
const res = await fetch('/api/members')
if (res.ok) members.value = await res.json()
try {
members.value = await memberApi.index()
} catch (e) {
console.error('Failed to load team members:', e)
}
})
</script>
+6
View File
@@ -0,0 +1,6 @@
import type { RouteRecordRaw } from 'vue-router'
export const blogRouter: RouteRecordRaw[] = [
{ path: '/blog', name: 'blogIndex', component: () => import('../page/blog/BlogIndexPage.vue') },
{ path: '/blog/:slug(.*)', name: 'blogShow', component: () => import('../page/blog/BlogShowPage.vue') },
]
@@ -0,0 +1,6 @@
import type { RouteRecordRaw } from 'vue-router'
export const catalogRouter: RouteRecordRaw[] = [
{ path: '/catalog', name: 'catalogIndex', component: () => import('../page/catalog/CatalogIndexPage.vue') },
{ path: '/catalog/:name', name: 'catalogShow', component: () => import('../page/catalog/CatalogShowPage.vue') },
]
+5
View File
@@ -0,0 +1,5 @@
import type { RouteRecordRaw } from 'vue-router'
export const codeRouter: RouteRecordRaw[] = [
{ path: '/code', name: 'codeIndex', component: () => import('../page/code/CodeIndexPage.vue') },
]
@@ -0,0 +1,5 @@
import type { RouteRecordRaw } from 'vue-router'
export const contactRouter: RouteRecordRaw[] = [
{ path: '/contact', name: 'contact', component: () => import('../page/contact/ContactPage.vue') },
]
+6
View File
@@ -0,0 +1,6 @@
import type { RouteRecordRaw } from 'vue-router'
import HomePage from '../page/home/HomePage.vue'
export const homeRouter: RouteRecordRaw[] = [
{ path: '/', name: 'home', component: HomePage },
]
@@ -0,0 +1,5 @@
import type { RouteRecordRaw } from 'vue-router'
export const howtosRouter: RouteRecordRaw[] = [
{ path: '/howtos', name: 'howtosIndex', component: () => import('../page/howtos/HowtosIndexPage.vue') },
]
+24
View File
@@ -0,0 +1,24 @@
import { createRouter, createWebHistory } from 'vue-router'
import { homeRouter } from './home.router'
import { blogRouter } from './blog.router'
import { catalogRouter } from './catalog.router'
import { codeRouter } from './code.router'
import { contactRouter } from './contact.router'
import { howtosRouter } from './howtos.router'
import { teamRouter } from './team.router'
export const router = createRouter({
history: createWebHistory(),
routes: [
...homeRouter,
...blogRouter,
...catalogRouter,
...codeRouter,
...contactRouter,
...howtosRouter,
...teamRouter,
],
scrollBehavior() {
return { top: 0 }
},
})
-20
View File
@@ -1,20 +0,0 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: HomeView },
{ path: '/blog', component: () => import('../views/BlogView.vue') },
{ path: '/blog/:slug(.*)', component: () => import('../views/BlogPostView.vue') },
{ path: '/catalog', component: () => import('../views/CatalogView.vue') },
{ path: '/catalog/:name', component: () => import('../views/CatalogItemView.vue') },
{ path: '/code', component: () => import('../views/CodeView.vue') },
{ path: '/contact', component: () => import('../views/ContactView.vue') },
{ path: '/howtos', component: () => import('../views/HowtosView.vue') },
{ path: '/team', component: () => import('../views/TeamView.vue') },
],
scrollBehavior() {
return { top: 0 }
}
})
+5
View File
@@ -0,0 +1,5 @@
import type { RouteRecordRaw } from 'vue-router'
export const teamRouter: RouteRecordRaw[] = [
{ path: '/team', name: 'teamIndex', component: () => import('../page/team/TeamIndexPage.vue') },
]
-22
View File
@@ -1,22 +0,0 @@
// Központi dátum formátum konfiguráció
// A formátum megváltoztatásához csak itt kell módosítani.
export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm';
export const DATE_FORMAT = 'YYYY-MM-DD';
export function formatDateTime(dateStr: string | Date): string {
const d = new Date(dateStr as string);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const hours = String(d.getHours()).padStart(2, '0');
const minutes = String(d.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
export function formatDate(dateStr: string | Date): string {
const d = new Date(dateStr as string);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}