frontend refact
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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 }
|
||||
@@ -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'
|
||||
@@ -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,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'
|
||||
|
||||
|
||||
+5
-70
@@ -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 {
|
||||
+8
-71
@@ -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) {
|
||||
+5
-26
@@ -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)
|
||||
}
|
||||
+20
-44
@@ -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
|
||||
+8
-60
@@ -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(() => {
|
||||
+7
-57
@@ -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 {
|
||||
+8
-10
@@ -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>
|
||||
|
||||
@@ -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') },
|
||||
]
|
||||
@@ -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') },
|
||||
]
|
||||
@@ -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') },
|
||||
]
|
||||
@@ -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 }
|
||||
},
|
||||
})
|
||||
@@ -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 }
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const teamRouter: RouteRecordRaw[] = [
|
||||
{ path: '/team', name: 'teamIndex', component: () => import('../page/team/TeamIndexPage.vue') },
|
||||
]
|
||||
@@ -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}`;
|
||||
}
|
||||
Reference in New Issue
Block a user