pinia
This commit is contained in:
Generated
+49
@@ -8,6 +8,7 @@
|
||||
"name": "ttglanding",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"pinia": "^2.2.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
@@ -1402,6 +1403,28 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz",
|
||||
"integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.6.3",
|
||||
"vue-demi": "^0.14.10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.7.0 || ^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pirates": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz",
|
||||
@@ -2486,6 +2509,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-demi": {
|
||||
"version": "0.14.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.6.4",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^2.2.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
|
||||
@@ -46,35 +46,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { useUiStore } from '../stores/ui.store'
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'))
|
||||
return match ? decodeURIComponent(match[1]) : null
|
||||
}
|
||||
|
||||
function setCookie(name: string, value: string, days = 365) {
|
||||
const expires = new Date(Date.now() + days * 864e5).toUTCString()
|
||||
document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires};path=/`
|
||||
}
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const ui = useUiStore()
|
||||
const { isRetroMode, menuOpen } = storeToRefs(ui)
|
||||
const { toggleMenu, toggleRetroMode } = ui
|
||||
const route = useRoute()
|
||||
const isRetroMode = ref(getCookie('retro-mode') === '1')
|
||||
|
||||
function toggleMenu() {
|
||||
menuOpen.value = !menuOpen.value
|
||||
}
|
||||
|
||||
function toggleRetroMode() {
|
||||
isRetroMode.value = !isRetroMode.value
|
||||
setCookie('retro-mode', isRetroMode.value ? '1' : '0')
|
||||
window.scrollTo({ top: 0, behavior: 'instant' })
|
||||
}
|
||||
|
||||
watch(() => route.path, () => {
|
||||
menuOpen.value = false
|
||||
ui.closeMenu()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { router } from './router/index.router'
|
||||
import App from './App.vue'
|
||||
import './styles/global.css'
|
||||
import './styles/retro.css'
|
||||
|
||||
createApp(App).use(router).mount('#app')
|
||||
createApp(App).use(createPinia()).use(router).mount('#app')
|
||||
|
||||
@@ -74,39 +74,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import wikiApi from '../../api/wiki.api'
|
||||
import type { WikiPageWithContent } from '../../lib/interfaces/wiki.interface'
|
||||
import { useBlogStore } from '../../stores/blog.store'
|
||||
|
||||
const blogPages = ref<WikiPageWithContent[]>([])
|
||||
const error = ref<string | null>(null)
|
||||
const loading = ref(true)
|
||||
const store = useBlogStore()
|
||||
const { pages: blogPages, loading, error } = storeToRefs(store)
|
||||
const { isNew, getPermalink, getCleanPreview } = store
|
||||
|
||||
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}`
|
||||
}
|
||||
|
||||
function getCleanPreview(content: string): string {
|
||||
if (!content) return ''
|
||||
return content.replace(/[#*`_\[\]()]/g, '').trim().slice(0, 300) + '...'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
blogPages.value = await wikiApi.listBlogPages()
|
||||
} catch (e: any) {
|
||||
error.value = `Failed to fetch wiki data: ${e.message}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
onMounted(() => store.fetchPages())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -40,32 +40,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import wikiApi from '../../api/wiki.api'
|
||||
import type { WikiPageContent } from '../../lib/interfaces/wiki.interface'
|
||||
import { useBlogStore } from '../../stores/blog.store'
|
||||
|
||||
const route = useRoute()
|
||||
const pageContent = ref<WikiPageContent | null>(null)
|
||||
const error = ref<string | null>(null)
|
||||
const loading = ref(true)
|
||||
const store = useBlogStore()
|
||||
const { currentPage: pageContent, currentPageLoading: loading, currentPageError: error } = storeToRefs(store)
|
||||
|
||||
onMounted(async () => {
|
||||
const slug = route.params.slug as string
|
||||
try {
|
||||
const result = await wikiApi.getBlogPage(slug)
|
||||
if (result) {
|
||||
pageContent.value = result
|
||||
} else {
|
||||
error.value = 'Post not found'
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
onMounted(() => store.fetchPage(route.params.slug as string))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -62,27 +62,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import softwareApi from '../../api/software.api'
|
||||
import type { Release, SoftwareEntry } from '../../lib/interfaces/software.interface'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
|
||||
const filters = ['all', 'released', 'demo', 'development', 'archived']
|
||||
const activeFilter = ref('released')
|
||||
const softwares = ref<SoftwareEntry[]>([])
|
||||
|
||||
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]
|
||||
}
|
||||
const store = useSoftwareStore()
|
||||
const { items: softwares } = storeToRefs(store)
|
||||
const { getLatestStable } = store
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
softwares.value = await softwareApi.index()
|
||||
} catch (e) {
|
||||
console.error('Failed to load catalog:', e)
|
||||
}
|
||||
})
|
||||
onMounted(() => store.fetchAll())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -170,10 +170,12 @@
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { RouterLink, useRoute } from 'vue-router'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import softwareApi from '../../api/software.api'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
import type { Release, Software } from '../../lib/interfaces/software.interface'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useSoftwareStore()
|
||||
|
||||
const software = ref<Software | null>(null)
|
||||
const releases = ref<Release[]>([])
|
||||
const error = ref<string | null>(null)
|
||||
@@ -195,8 +197,8 @@ const latestStable = computed(() => stableReleases.value[0] ?? null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const all = await softwareApi.index()
|
||||
const item = all.find(s => s.software.name === route.params.name)
|
||||
await store.fetchAll()
|
||||
const item = store.findByName(route.params.name as string)
|
||||
if (item) {
|
||||
software.value = item.software
|
||||
releases.value = item.releases
|
||||
|
||||
@@ -59,30 +59,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import gitApi from '../../api/git.api'
|
||||
import type { GiteaRepo, Commit } from '../../lib/interfaces/git.interface'
|
||||
import { useGitStore } from '../../stores/git.store'
|
||||
|
||||
const publicRepos = ref<GiteaRepo[]>([])
|
||||
const recentCommits = ref<Commit[]>([])
|
||||
const error = ref<string | null>(null)
|
||||
const store = useGitStore()
|
||||
const { repos: publicRepos, commits: recentCommits, error } = storeToRefs(store)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
publicRepos.value = await gitApi.repos()
|
||||
|
||||
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 = e.message
|
||||
}
|
||||
})
|
||||
onMounted(() => store.fetchAll())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -187,109 +187,29 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { RouterLink } from 'vue-router'
|
||||
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'
|
||||
import { useEventStore } from '../../stores/event.store'
|
||||
import { useSoftwareStore } from '../../stores/software.store'
|
||||
import { useYoutubeStore } from '../../stores/youtube.store'
|
||||
|
||||
const nextEvent = ref<(Event & { dateISO: string; dateText: string }) | null>(null)
|
||||
const followingEvents = ref<(Event & { dateText: string })[]>([])
|
||||
const highlightedSoftware = ref<SoftwareEntry | null>(null)
|
||||
const highlightedStableRelease = ref<Release | null>(null)
|
||||
const countdownText = ref('-- : -- : -- : --')
|
||||
const eventStore = useEventStore()
|
||||
const { nextEvent, followingEvents, countdownText } = storeToRefs(eventStore)
|
||||
|
||||
const ytState = ref<'loading' | 'loaded' | 'error' | 'no-config'>('loading')
|
||||
const ytVideoId = ref('')
|
||||
const ytTitle = ref('')
|
||||
const ytPublishDate = ref('')
|
||||
const ytViewCount = ref('')
|
||||
const ytError = ref('')
|
||||
const softwareStore = useSoftwareStore()
|
||||
const { highlighted: highlightedSoftware, highlightedStableRelease } = storeToRefs(softwareStore)
|
||||
|
||||
let countdownInterval: ReturnType<typeof setInterval> | null = null
|
||||
const ytStore = useYoutubeStore()
|
||||
const { state: ytState, videoId: ytVideoId, title: ytTitle, publishDate: ytPublishDate, viewCount: ytViewCount, error: ytError } = storeToRefs(ytStore)
|
||||
|
||||
function startCountdown(targetISO: string) {
|
||||
const targetDate = new Date(targetISO).getTime()
|
||||
|
||||
function update() {
|
||||
const distance = targetDate - Date.now()
|
||||
if (distance < 0) {
|
||||
countdownText.value = 'LIVE NOW!'
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
return
|
||||
}
|
||||
const d = String(Math.floor(distance / 86400000)).padStart(2, '0')
|
||||
const h = String(Math.floor((distance % 86400000) / 3600000)).padStart(2, '0')
|
||||
const m = String(Math.floor((distance % 3600000) / 60000)).padStart(2, '0')
|
||||
const s = String(Math.floor((distance % 60000) / 1000)).padStart(2, '0')
|
||||
countdownText.value = `${d}d ${h}h ${m}m ${s}s`
|
||||
}
|
||||
|
||||
update()
|
||||
countdownInterval = setInterval(update, 1000)
|
||||
}
|
||||
|
||||
function formatYtDate(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'
|
||||
return n + ' views'
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
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)
|
||||
}
|
||||
|
||||
try {
|
||||
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)
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
onMounted(() => {
|
||||
eventStore.fetch()
|
||||
softwareStore.fetchHighlighted()
|
||||
ytStore.fetchLatest()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
})
|
||||
onUnmounted(() => eventStore.cleanup())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -78,28 +78,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { formatDateTime } from '../../lib/dateFormat'
|
||||
import wikiApi, { WIKI_BASE } from '../../api/wiki.api'
|
||||
import type { WikiPage } from '../../lib/interfaces/wiki.interface'
|
||||
import { WIKI_BASE } from '../../api/wiki.api'
|
||||
import { useHowtosStore } from '../../stores/howtos.store'
|
||||
|
||||
const recentPages = ref<WikiPage[]>([])
|
||||
const error = ref<string | null>(null)
|
||||
const loading = ref(true)
|
||||
const store = useHowtosStore()
|
||||
const { pages: recentPages, loading, error } = storeToRefs(store)
|
||||
const { isNew } = store
|
||||
|
||||
function isNew(createdAt: string): boolean {
|
||||
return (new Date().getTime() - new Date(createdAt).getTime()) < 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
recentPages.value = await wikiApi.listHowtoPages()
|
||||
} catch (e: any) {
|
||||
error.value = `Failed to fetch wiki data: ${e.message}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
onMounted(() => store.fetchPages())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -24,24 +24,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import memberApi from '../../api/member.api'
|
||||
import type { Member } from '../../lib/interfaces/member.interface'
|
||||
import { onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useMemberStore } from '../../stores/member.store'
|
||||
|
||||
const members = ref<Member[]>([])
|
||||
const store = useMemberStore()
|
||||
const { members } = storeToRefs(store)
|
||||
const { resolvedAvatarUrl } = store
|
||||
|
||||
function resolvedAvatarUrl(member: Member): string {
|
||||
if (member.image_url) return member.image_url
|
||||
return new URL(`../../assets/team/${member.avatar_filename}`, import.meta.url).href
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
members.value = await memberApi.index()
|
||||
} catch (e) {
|
||||
console.error('Failed to load team members:', e)
|
||||
}
|
||||
})
|
||||
onMounted(() => store.fetch())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import wikiApi from '../api/wiki.api'
|
||||
import type { WikiPageWithContent, WikiPageContent } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
export const useBlogStore = defineStore('blog', () => {
|
||||
const pages = ref<WikiPageWithContent[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const currentPage = ref<WikiPageContent | null>(null)
|
||||
const currentPageLoading = ref(false)
|
||||
const currentPageError = ref<string | null>(null)
|
||||
|
||||
let _loaded = false
|
||||
|
||||
async function fetchPages() {
|
||||
if (_loaded) return
|
||||
loading.value = true
|
||||
try {
|
||||
pages.value = await wikiApi.listBlogPages()
|
||||
_loaded = true
|
||||
} catch (e: any) {
|
||||
error.value = `Failed to fetch wiki data: ${e.message}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPage(slug: string) {
|
||||
currentPage.value = null
|
||||
currentPageError.value = null
|
||||
currentPageLoading.value = true
|
||||
try {
|
||||
const result = await wikiApi.getBlogPage(slug)
|
||||
if (result) {
|
||||
currentPage.value = result
|
||||
} else {
|
||||
currentPageError.value = 'Post not found'
|
||||
}
|
||||
} catch (e: any) {
|
||||
currentPageError.value = e.message
|
||||
} finally {
|
||||
currentPageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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}`
|
||||
}
|
||||
|
||||
function getCleanPreview(content: string): string {
|
||||
if (!content) return ''
|
||||
return content.replace(/[#*`_\[\]()]/g, '').trim().slice(0, 300) + '...'
|
||||
}
|
||||
|
||||
return { pages, loading, error, currentPage, currentPageLoading, currentPageError, fetchPages, fetchPage, isNew, getPermalink, getCleanPreview }
|
||||
})
|
||||
@@ -0,0 +1,61 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import eventApi from '../api/event.api'
|
||||
import { formatDateTime } from '../lib/dateFormat'
|
||||
import type { Event } from '../lib/interfaces/event.interface'
|
||||
|
||||
type EventWithText = Event & { dateISO: string; dateText: string }
|
||||
type FollowingEvent = Event & { dateText: string }
|
||||
|
||||
export const useEventStore = defineStore('event', () => {
|
||||
const nextEvent = ref<EventWithText | null>(null)
|
||||
const followingEvents = ref<FollowingEvent[]>([])
|
||||
const countdownText = ref('-- : -- : -- : --')
|
||||
|
||||
let countdownInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
function startCountdown(targetISO: string) {
|
||||
const targetDate = new Date(targetISO).getTime()
|
||||
|
||||
function update() {
|
||||
const distance = targetDate - Date.now()
|
||||
if (distance < 0) {
|
||||
countdownText.value = 'LIVE NOW!'
|
||||
if (countdownInterval) clearInterval(countdownInterval)
|
||||
return
|
||||
}
|
||||
const d = String(Math.floor(distance / 86400000)).padStart(2, '0')
|
||||
const h = String(Math.floor((distance % 86400000) / 3600000)).padStart(2, '0')
|
||||
const m = String(Math.floor((distance % 3600000) / 60000)).padStart(2, '0')
|
||||
const s = String(Math.floor((distance % 60000) / 1000)).padStart(2, '0')
|
||||
countdownText.value = `${d}d ${h}h ${m}m ${s}s`
|
||||
}
|
||||
|
||||
update()
|
||||
countdownInterval = setInterval(update, 1000)
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
try {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval)
|
||||
countdownInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
return { nextEvent, followingEvents, countdownText, fetch, cleanup }
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import gitApi from '../api/git.api'
|
||||
import type { GiteaRepo, Commit } from '../lib/interfaces/git.interface'
|
||||
|
||||
export const useGitStore = defineStore('git', () => {
|
||||
const repos = ref<GiteaRepo[]>([])
|
||||
const commits = ref<Commit[]>([])
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function fetchAll() {
|
||||
try {
|
||||
repos.value = await gitApi.repos()
|
||||
|
||||
const commitPromises = repos.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())
|
||||
commits.value = all.slice(0, 20)
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
}
|
||||
}
|
||||
|
||||
return { repos, commits, error, fetchAll }
|
||||
})
|
||||
@@ -0,0 +1,30 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import wikiApi from '../api/wiki.api'
|
||||
import type { WikiPage } from '../lib/interfaces/wiki.interface'
|
||||
|
||||
export const useHowtosStore = defineStore('howtos', () => {
|
||||
const pages = ref<WikiPage[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
let _loaded = false
|
||||
|
||||
async function fetchPages() {
|
||||
if (_loaded) return
|
||||
loading.value = true
|
||||
try {
|
||||
pages.value = await wikiApi.listHowtoPages()
|
||||
_loaded = true
|
||||
} catch (e: any) {
|
||||
error.value = `Failed to fetch wiki data: ${e.message}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function isNew(createdAt: string): boolean {
|
||||
return (new Date().getTime() - new Date(createdAt).getTime()) < 7 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
return { pages, loading, error, fetchPages, isNew }
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import memberApi from '../api/member.api'
|
||||
import type { Member } from '../lib/interfaces/member.interface'
|
||||
|
||||
export const useMemberStore = defineStore('member', () => {
|
||||
const members = ref<Member[]>([])
|
||||
let _loaded = false
|
||||
|
||||
async function fetch() {
|
||||
if (_loaded) return
|
||||
try {
|
||||
members.value = await memberApi.index()
|
||||
_loaded = true
|
||||
} catch (e) {
|
||||
console.error('Failed to load team members:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function resolvedAvatarUrl(member: Member): string {
|
||||
if (member.image_url) return member.image_url
|
||||
return new URL(`../assets/team/${member.avatar_filename}`, import.meta.url).href
|
||||
}
|
||||
|
||||
return { members, fetch, resolvedAvatarUrl }
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import softwareApi from '../api/software.api'
|
||||
import type { SoftwareEntry, Release } from '../lib/interfaces/software.interface'
|
||||
|
||||
export const useSoftwareStore = defineStore('software', () => {
|
||||
const items = ref<SoftwareEntry[]>([])
|
||||
const highlighted = ref<SoftwareEntry | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
let _loaded = false
|
||||
|
||||
const highlightedStableRelease = computed(() => {
|
||||
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
|
||||
})
|
||||
|
||||
async function fetchAll() {
|
||||
if (_loaded) return
|
||||
loading.value = true
|
||||
try {
|
||||
items.value = await softwareApi.index()
|
||||
_loaded = true
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchHighlighted() {
|
||||
try {
|
||||
highlighted.value = await softwareApi.highlighted()
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch highlighted software:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function findByName(name: string): SoftwareEntry | undefined {
|
||||
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 }
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
function getCookie(name: string): string | null {
|
||||
const match = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'))
|
||||
return match ? decodeURIComponent(match[1]) : null
|
||||
}
|
||||
|
||||
function setCookie(name: string, value: string, days = 365) {
|
||||
const expires = new Date(Date.now() + days * 864e5).toUTCString()
|
||||
document.cookie = `${name}=${encodeURIComponent(value)};expires=${expires};path=/`
|
||||
}
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
const isRetroMode = ref(getCookie('retro-mode') === '1')
|
||||
const menuOpen = ref(false)
|
||||
|
||||
function toggleRetroMode() {
|
||||
isRetroMode.value = !isRetroMode.value
|
||||
setCookie('retro-mode', isRetroMode.value ? '1' : '0')
|
||||
window.scrollTo({ top: 0, behavior: 'instant' })
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
menuOpen.value = !menuOpen.value
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
menuOpen.value = false
|
||||
}
|
||||
|
||||
return { isRetroMode, menuOpen, toggleRetroMode, toggleMenu, closeMenu }
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import youtubeApi from '../api/youtube.api'
|
||||
|
||||
export const useYoutubeStore = defineStore('youtube', () => {
|
||||
const state = ref<'loading' | 'loaded' | 'error' | 'no-config'>('loading')
|
||||
const videoId = ref('')
|
||||
const title = ref('')
|
||||
const publishDate = ref('')
|
||||
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'
|
||||
return n + ' views'
|
||||
}
|
||||
|
||||
async function fetchLatest() {
|
||||
state.value = 'loading'
|
||||
try {
|
||||
const video = await youtubeApi.latestVideo()
|
||||
if (!video) {
|
||||
state.value = 'no-config'
|
||||
return
|
||||
}
|
||||
videoId.value = video.id
|
||||
title.value = video.title
|
||||
publishDate.value = formatDate(video.publishDate)
|
||||
viewCount.value = video.viewCount ? formatViews(Number(video.viewCount)) : ''
|
||||
state.value = 'loaded'
|
||||
} catch (err: any) {
|
||||
error.value = 'Error loading video: ' + err.message
|
||||
state.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
return { state, videoId, title, publishDate, viewCount, error, fetchLatest }
|
||||
})
|
||||
Reference in New Issue
Block a user