feat: migrate everything to typescript

This commit is contained in:
2025-11-07 09:55:20 +02:00
parent 234b2550ab
commit d3c0590dca
12 changed files with 343 additions and 194 deletions

View File

@@ -12,7 +12,7 @@ module.exports = [
...tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended),
{
rules: {
'no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
'no-unused-vars': ['off'],
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },

View File

@@ -3,13 +3,15 @@
<!-- Left sidebar -->
<NcAppNavigation>
<template #search>
<NcAppNavigationSearch v-model="searchValue" :label="strings.searchLabel"
:placeholder="strings.searchPlaceholder" />
<NcAppNavigationSearch
v-model="searchValue"
:label="strings.searchLabel"
:placeholder="strings.searchPlaceholder"
/>
</template>
<template #list>
<NcAppNavigationItem :name="strings.navHome" :to="{ path: '/' }"
:open="true">
<NcAppNavigationItem :name="strings.navHome" :to="{ path: '/' }" :open="true">
<template #icon>
<HomeIcon :size="20" />
</template>
@@ -20,7 +22,8 @@
:key="`header-${header.id}`"
:name="header.name"
:open="isHeaderOpen(header.id)"
@click.native.prevent="toggleHeader(header.id)">
@click.native.prevent="toggleHeader(header.id)"
>
<template #icon>
<FolderIcon :size="20" />
</template>
@@ -30,7 +33,8 @@
v-for="category in header.categories"
:key="`category-${category.id}`"
:name="category.name"
:to="{ path: `/c/${category.slug}` }">
:to="{ path: `/c/${category.slug}` }"
>
<template #icon>
<ForumIcon :size="20" />
</template>
@@ -63,7 +67,8 @@
</NcContent>
</template>
<script>
<script lang="ts">
import { defineComponent } from 'vue'
import { t } from '@nextcloud/l10n'
import NcContent from '@nextcloud/vue/components/NcContent'
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
@@ -76,9 +81,9 @@ import ForumIcon from '@icons/Forum.vue'
import FolderIcon from '@icons/Folder.vue'
import PuzzleIcon from '@icons/Puzzle.vue'
import InfoIcon from '@icons/Information.vue'
import { useCategories } from '@/composables/useCategories.js'
import { useCategories } from '@/composables/useCategories'
export default {
export default defineComponent({
name: 'AppUserWrapper',
components: {
NcContent,
@@ -108,7 +113,7 @@ export default {
return {
searchValue: '',
isRouterLoading: false,
openHeaders: {}, // Track which headers are open
openHeaders: {} as Record<number, boolean>, // Track which headers are open
// Mount path for this app section; adjust to your mount.
basePath: '/apps/forum',
strings: {
@@ -126,8 +131,8 @@ export default {
navExamples: t('forum', 'Examples'),
navAbout: t('forum', 'About'),
},
_removeBeforeEach: null,
_removeAfterEach: null,
_removeBeforeEach: null as (() => void) | null,
_removeAfterEach: null as (() => void) | null,
}
},
async created() {
@@ -136,7 +141,7 @@ export default {
await this.fetchCategories()
// Initialize all headers as open by default
const openState = {}
const openState: Record<number, boolean> = {}
this.categoryHeaders.forEach((header) => {
openState[header.id] = true
})
@@ -160,23 +165,23 @@ export default {
if (typeof this._removeAfterEach === 'function') this._removeAfterEach()
},
methods: {
isPrefixRoute(prefix) {
isPrefixRoute(prefix: string): boolean {
return this.$route.path.startsWith(prefix)
},
toggleHeader(headerId) {
toggleHeader(headerId: number): void {
// Vue 3 doesn't need $set - direct assignment works with reactivity
this.openHeaders = {
...this.openHeaders,
[headerId]: !this.openHeaders[headerId]
[headerId]: !this.openHeaders[headerId],
}
},
isHeaderOpen(headerId) {
isHeaderOpen(headerId: number): boolean {
return this.openHeaders[headerId] !== false
},
},
}
})
</script>
<style scoped lang="scss">

View File

@@ -19,14 +19,16 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent, type PropType } from 'vue'
import { t } from '@nextcloud/l10n'
import type { Category } from '@/types'
export default {
export default defineComponent({
name: 'CategoryCard',
props: {
category: {
type: Object,
type: Object as PropType<Category>,
required: true,
},
},
@@ -39,7 +41,7 @@ export default {
},
}
},
}
})
</script>
<style scoped lang="scss">

View File

@@ -47,7 +47,8 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent, type PropType } from 'vue'
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
import NcActions from '@nextcloud/vue/components/NcActions'
@@ -57,8 +58,9 @@ import PencilIcon from '@icons/Pencil.vue'
import DeleteIcon from '@icons/Delete.vue'
import { t } from '@nextcloud/l10n'
import { getCurrentUser } from '@nextcloud/auth'
import type { Post } from '@/types'
export default {
export default defineComponent({
name: 'PostCard',
components: {
NcAvatar,
@@ -71,7 +73,7 @@ export default {
},
props: {
post: {
type: Object,
type: Object as PropType<Post>,
required: true,
},
isFirstPost: {
@@ -94,22 +96,21 @@ export default {
currentUser() {
return getCurrentUser()
},
canEdit() {
return this.currentUser && this.currentUser.uid === this.post.authorId
canEdit(): boolean {
return this.currentUser !== null && this.currentUser.uid === this.post.authorId
},
canDelete() {
canDelete(): boolean {
// For now, only author can delete. Later add admin/moderator check
return this.currentUser && this.currentUser.uid === this.post.authorId
return this.currentUser !== null && this.currentUser.uid === this.post.authorId
},
formattedContent() {
formattedContent(): string {
// Content is already parsed by BBCodeService on the backend
// BBCodeService handles HTML escaping before parsing BBCodes
return this.post.content
},
},
methods: {
},
}
methods: {},
})
</script>
<style scoped lang="scss">

View File

@@ -47,15 +47,17 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent, type PropType } from 'vue'
import NcDateTime from '@nextcloud/vue/components/NcDateTime'
import PinIcon from '@icons/Pin.vue'
import LockIcon from '@icons/Lock.vue'
import CommentIcon from '@icons/Comment.vue'
import EyeIcon from '@icons/Eye.vue'
import { t } from '@nextcloud/l10n'
import type { Thread } from '@/types'
export default {
export default defineComponent({
name: 'ThreadCard',
components: {
NcDateTime,
@@ -66,7 +68,7 @@ export default {
},
props: {
thread: {
type: Object,
type: Object as PropType<Thread>,
required: true,
},
},
@@ -81,7 +83,7 @@ export default {
},
}
},
}
})
</script>
<style scoped lang="scss">

View File

@@ -1,92 +0,0 @@
import { ref, computed } from 'vue'
import { ocs } from '@/axios'
// Shared state - will persist across components
// The API returns an array of headers, each with a nested 'categories' array
const categoryHeaders = ref([])
const loading = ref(false)
const error = ref(null)
const loaded = ref(false)
/**
* Composable for managing categories
* Provides shared state across components to avoid redundant API calls
*/
export function useCategories() {
/**
* Fetch categories from the API
* Uses cached data if already loaded
*/
const fetchCategories = async (force = false) => {
// Return cached data if already loaded and not forcing refresh
if (loaded.value && !force) {
return categoryHeaders.value
}
try {
loading.value = true
error.value = null
const response = await ocs.get('/categories')
categoryHeaders.value = response.data || []
loaded.value = true
return categoryHeaders.value
} catch (e) {
console.error('Failed to fetch categories:', e)
error.value = e.message || 'Failed to load categories'
throw e
} finally {
loading.value = false
}
}
/**
* Get all categories as a flat list (extracted from all headers)
* Useful for sidebar navigation
*/
const categoriesList = computed(() => {
const allCategories = []
categoryHeaders.value.forEach((header) => {
if (header.categories && Array.isArray(header.categories)) {
allCategories.push(...header.categories)
}
})
// Sort by sortOrder
return allCategories.sort((a, b) => a.sortOrder - b.sortOrder)
})
/**
* Refresh categories from the API
*/
const refresh = () => {
return fetchCategories(true)
}
/**
* Clear cached categories
*/
const clear = () => {
categoryHeaders.value = []
loaded.value = false
error.value = null
}
return {
// State
categoryHeaders,
loading,
error,
loaded,
// Computed
categoriesList,
// Methods
fetchCategories,
refresh,
clear,
}
}

View File

@@ -0,0 +1,73 @@
import { ref, type Ref } from 'vue'
import { ocs } from '@/axios'
import type { CategoryHeader } from '@/types'
// Shared state - will persist across components
// The API returns an array of headers, each with a nested 'categories' array
const categoryHeaders = ref<CategoryHeader[]>([])
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const loaded = ref<boolean>(false)
/**
* Composable for managing categories
* Provides shared state across components to avoid redundant API calls
*/
export function useCategories() {
/**
* Fetch categories from the API
* Uses cached data if already loaded
*/
const fetchCategories = async (force = false): Promise<CategoryHeader[]> => {
// Return cached data if already loaded and not forcing refresh
if (loaded.value && !force) {
return categoryHeaders.value
}
try {
loading.value = true
error.value = null
const response = await ocs.get<CategoryHeader[]>('/categories')
categoryHeaders.value = response.data || []
loaded.value = true
return categoryHeaders.value
} catch (e) {
console.error('Failed to fetch categories:', e)
error.value = (e as Error).message || 'Failed to load categories'
throw e
} finally {
loading.value = false
}
}
/**
* Refresh categories from the API
*/
const refresh = (): Promise<CategoryHeader[]> => {
return fetchCategories(true)
}
/**
* Clear cached categories
*/
const clear = (): void => {
categoryHeaders.value = []
loaded.value = false
error.value = null
}
return {
// State
categoryHeaders: categoryHeaders as Ref<CategoryHeader[]>,
loading: loading as Ref<boolean>,
error: error as Ref<string | null>,
loaded: loaded as Ref<boolean>,
// Methods
fetchCategories,
refresh,
clear,
}
}

5
src/types/index.ts Normal file
View File

@@ -0,0 +1,5 @@
/**
* Central export for all TypeScript types
*/
export * from './models'

125
src/types/models.ts Normal file
View File

@@ -0,0 +1,125 @@
/**
* Forum TypeScript Models
* These interfaces match the backend JSON responses
*/
export interface Category {
id: number
headerId: number
name: string
description: string | null
slug: string
sortOrder: number
threadCount: number
postCount: number
createdAt: number
updatedAt: number
}
export interface CategoryHeader {
id: number
name: string
sortOrder: number
createdAt: number
categories?: Category[]
}
export interface Thread {
id: number
categoryId: number
authorId: string
title: string
slug: string
viewCount: number
postCount: number
lastPostId: number | null
isLocked: boolean
isPinned: boolean
isHidden: boolean
createdAt: number
updatedAt: number
// Enriched fields (added by Thread::enrichThreadAuthor)
authorDisplayName?: string
authorIsDeleted?: boolean
}
export interface Post {
id: number
threadId: number
authorId: string
content: string
contentRaw: string
slug: string
isEdited: boolean
editedAt: number | null
createdAt: number
updatedAt: number
// Enriched fields (added by Post::enrichPostContent)
authorDisplayName?: string
authorIsDeleted?: boolean
}
export interface ForumUser {
id: number
userId: string
postCount: number
createdAt: number
updatedAt: number
deletedAt: number | null
isDeleted: boolean
}
export interface BBCode {
id: number
tag: string
replacement: string
description: string | null
enabled: boolean
parseInner: boolean
createdAt: number
}
export interface ReadMarker {
id: number
userId: string
threadId: number
lastReadPostId: number
readAt: number
}
export interface Role {
id: number
name: string
description: string | null
createdAt: number
}
export interface UserRole {
id: number
userId: string
roleId: number
createdAt: number
}
export interface Reaction {
id: number
postId: number
userId: string
reactionType: string
createdAt: number
}
export interface Attachment {
id: number
postId: number
fileid: number
filename: string
createdAt: number
}
export interface CatHeader {
id: number
name: string
sortOrder: number
createdAt: number
}

View File

@@ -18,8 +18,12 @@
</div>
<!-- Empty state -->
<NcEmptyContent v-else-if="categoryHeaders.length === 0" :title="strings.emptyTitle"
:description="strings.emptyDesc" class="mt-16" />
<NcEmptyContent
v-else-if="categoryHeaders.length === 0"
:title="strings.emptyTitle"
:description="strings.emptyDesc"
class="mt-16"
/>
<!-- Categories list -->
<section v-else class="mt-16">
@@ -28,8 +32,12 @@
<!-- Categories grid -->
<div v-if="header.categories && header.categories.length > 0" class="categories-grid">
<CategoryCard v-for="category in header.categories" :key="category.id" :category="category"
@click="navigateToCategory(category)" />
<CategoryCard
v-for="category in header.categories"
:key="category.id"
:category="category"
@click="navigateToCategory(category)"
/>
</div>
<!-- Empty state for header with no categories -->
@@ -39,16 +47,17 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import CategoryCard from '@/components/CategoryCard.vue'
import { useCategories } from '@/composables/useCategories.js'
import { useCategories } from '@/composables/useCategories'
import type { Category } from '@/types'
import { t } from '@nextcloud/l10n'
export default {
export default defineComponent({
name: 'CategoriesView',
components: {
NcButton,
@@ -94,11 +103,11 @@ export default {
}
},
navigateToCategory(category) {
navigateToCategory(category: Category) {
this.$router.push(`/c/${category.slug}`)
},
},
}
})
</script>
<style scoped lang="scss">

View File

@@ -3,12 +3,12 @@
<!-- Toolbar -->
<div class="toolbar">
<div class="toolbar-left">
<NcButton type="tertiary" @click="goBack">{{ strings.back }}</NcButton>
<NcButton @click="goBack">{{ strings.back }}</NcButton>
</div>
<div class="toolbar-right">
<NcButton @click="refresh" :disabled="loading">{{ strings.refresh }}</NcButton>
<NcButton type="primary" @click="createThread" :disabled="loading || category?.isLocked">
<NcButton @click="createThread" :disabled="loading">
{{ strings.newThread }}
</NcButton>
</div>
@@ -46,7 +46,7 @@
class="mt-16"
>
<template #action>
<NcButton type="primary" @click="createThread">{{ strings.newThread }}</NcButton>
<NcButton @click="createThread">{{ strings.newThread }}</NcButton>
</template>
</NcEmptyContent>
@@ -69,16 +69,17 @@
</div>
</template>
<script>
<script lang="ts">
import { defineComponent } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import ThreadCard from '@/components/ThreadCard.vue'
import type { Category, Thread } from '@/types'
import { ocs } from '@/axios'
import { t, n } from '@nextcloud/l10n'
export default {
export default defineComponent({
name: 'CategoryView',
components: {
NcButton,
@@ -89,9 +90,9 @@ export default {
data() {
return {
loading: false,
category: null,
threads: [],
error: null,
category: null as Category | null,
threads: [] as Thread[],
error: null as string | null,
limit: 50,
offset: 0,
@@ -104,18 +105,19 @@ export default {
emptyTitle: t('forum', 'No threads yet'),
emptyDesc: t('forum', 'Be the first to start a discussion in this category.'),
retry: t('forum', 'Retry'),
showingThreads: (count) => n('forum', 'Showing %n thread', 'Showing %n threads', count),
showingThreads: (count: number) =>
n('forum', 'Showing %n thread', 'Showing %n threads', count),
},
}
},
computed: {
categoryId() {
return this.$route.params.id ? parseInt(this.$route.params.id) : null
categoryId(): number | null {
return this.$route.params.id ? parseInt(this.$route.params.id as string) : null
},
categorySlug() {
return this.$route.params.slug || null
categorySlug(): string | null {
return (this.$route.params.slug as string) || null
},
sortedThreads() {
sortedThreads(): Thread[] {
// Sort pinned threads first, then by updatedAt descending
return [...this.threads].sort((a, b) => {
if (a.isPinned !== b.isPinned) {
@@ -143,7 +145,7 @@ export default {
}
} catch (e) {
console.error('Failed to refresh', e)
this.error = e.message || t('forum', 'An unexpected error occurred')
this.error = (e as Error).message || t('forum', 'An unexpected error occurred')
} finally {
this.loading = false
}
@@ -153,9 +155,9 @@ export default {
try {
let resp
if (this.categorySlug) {
resp = await ocs.get(`/categories/slug/${this.categorySlug}`)
resp = await ocs.get<Category>(`/categories/slug/${this.categorySlug}`)
} else if (this.categoryId) {
resp = await ocs.get(`/categories/${this.categoryId}`)
resp = await ocs.get<Category>(`/categories/${this.categoryId}`)
} else {
throw new Error(t('forum', 'No category ID or slug provided'))
}
@@ -168,7 +170,7 @@ export default {
async fetchThreads() {
try {
const resp = await ocs.get(`/categories/${this.category.id}/threads`, {
const resp = await ocs.get<Thread[]>(`/categories/${this.category!.id}/threads`, {
params: {
limit: this.limit,
offset: this.offset,
@@ -181,7 +183,7 @@ export default {
}
},
navigateToThread(thread) {
navigateToThread(thread: Thread) {
this.$router.push(`/t/${thread.slug}`)
},
@@ -194,7 +196,7 @@ export default {
this.$router.back()
},
},
}
})
</script>
<style scoped lang="scss">

View File

@@ -3,12 +3,12 @@
<!-- Toolbar -->
<div class="toolbar">
<div class="toolbar-left">
<NcButton type="tertiary" @click="goBack">{{ strings.back }}</NcButton>
<NcButton @click="goBack">{{ strings.back }}</NcButton>
</div>
<div class="toolbar-right">
<NcButton @click="refresh" :disabled="loading">{{ strings.refresh }}</NcButton>
<NcButton type="primary" @click="replyToThread" :disabled="loading || thread?.isLocked">
<NcButton @click="replyToThread" :disabled="loading || thread?.isLocked">
{{ strings.reply }}
</NcButton>
</div>
@@ -21,7 +21,12 @@
</div>
<!-- Error state -->
<NcEmptyContent v-else-if="error" :title="strings.errorTitle" :description="error" class="mt-16">
<NcEmptyContent
v-else-if="error"
:title="strings.errorTitle"
:description="error"
class="mt-16"
>
<template #action>
<NcButton @click="refresh">{{ strings.retry }}</NcButton>
</template>
@@ -64,8 +69,15 @@
<!-- Posts list -->
<section v-if="!loading && !error && posts.length > 0" class="mt-16">
<div class="posts-list">
<PostCard v-for="(post, index) in posts" :key="post.id" :post="post" :is-first-post="index === 0"
@reply="handleReply" @edit="handleEdit" @delete="handleDelete" />
<PostCard
v-for="(post, index) in posts"
:key="post.id"
:post="post"
:is-first-post="index === 0"
@reply="handleReply"
@edit="handleEdit"
@delete="handleDelete"
/>
</div>
<!-- Pagination info -->
@@ -75,16 +87,21 @@
</section>
<!-- Empty posts state (thread exists but no posts) -->
<NcEmptyContent v-else-if="!loading && !error && thread && posts.length === 0" :title="strings.emptyPostsTitle"
:description="strings.emptyPostsDesc" class="mt-16">
<NcEmptyContent
v-else-if="!loading && !error && thread && posts.length === 0"
:title="strings.emptyPostsTitle"
:description="strings.emptyPostsDesc"
class="mt-16"
>
<template #action>
<NcButton type="primary" @click="replyToThread">{{ strings.reply }}</NcButton>
<NcButton @click="replyToThread">{{ strings.reply }}</NcButton>
</template>
</NcEmptyContent>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
@@ -93,11 +110,11 @@ import PostCard from '@/components/PostCard.vue'
import PinIcon from '@icons/Pin.vue'
import LockIcon from '@icons/Lock.vue'
import EyeIcon from '@icons/Eye.vue'
import type { Thread, Post } from '@/types'
import { ocs } from '@/axios'
import { t, n } from '@nextcloud/l10n'
export default {
export default defineComponent({
name: 'ThreadView',
components: {
NcButton,
@@ -112,9 +129,9 @@ export default {
data() {
return {
loading: false,
thread: null,
posts: [],
error: null,
thread: null as Thread | null,
posts: [] as Post[],
error: null as string | null,
limit: 50,
offset: 0,
@@ -128,26 +145,26 @@ export default {
emptyPostsDesc: t('forum', 'Be the first to post in this thread.'),
retry: t('forum', 'Retry'),
by: t('forum', 'by'),
views: (count: string) => n('forum', '%n view', '%n views', count),
views: (count: number) => n('forum', '%n view', '%n views', count),
pinned: t('forum', 'Pinned thread'),
locked: t('forum', 'Locked thread'),
showingPosts: (count) => n('forum', 'Showing %n post', 'Showing %n posts', count),
showingPosts: (count: number) => n('forum', 'Showing %n post', 'Showing %n posts', count),
},
}
},
computed: {
threadId() {
return this.$route.params.id ? parseInt(this.$route.params.id) : null
threadId(): number | null {
return this.$route.params.id ? parseInt(this.$route.params.id as string) : null
},
threadSlug() {
return this.$route.params.slug || null
threadSlug(): string | null {
return (this.$route.params.slug as string) || null
},
},
created() {
this.refresh()
},
methods: {
async refresh() {
async refresh(): Promise<void> {
try {
this.loading = true
this.error = null
@@ -161,19 +178,19 @@ export default {
}
} catch (e) {
console.error('Failed to refresh', e)
this.error = e.message || t('forum', 'An unexpected error occurred')
this.error = (e as Error).message || t('forum', 'An unexpected error occurred')
} finally {
this.loading = false
}
},
async fetchThread() {
async fetchThread(): Promise<void> {
try {
let resp
if (this.threadSlug) {
resp = await ocs.get(`/threads/slug/${this.threadSlug}`)
resp = await ocs.get<Thread>(`/threads/slug/${this.threadSlug}`)
} else if (this.threadId) {
resp = await ocs.get(`/threads/${this.threadId}`)
resp = await ocs.get<Thread>(`/threads/${this.threadId}`)
} else {
throw new Error(t('forum', 'No thread ID or slug provided'))
}
@@ -184,9 +201,9 @@ export default {
}
},
async fetchPosts() {
async fetchPosts(): Promise<void> {
try {
const resp = await ocs.get(`/threads/${this.thread.id}/posts`, {
const resp = await ocs.get<Post[]>(`/threads/${this.thread!.id}/posts`, {
params: {
limit: this.limit,
offset: this.offset,
@@ -204,7 +221,7 @@ export default {
}
},
async markAsRead() {
async markAsRead(): Promise<void> {
try {
// Get the last post ID from the current view
const lastPost = this.posts[this.posts.length - 1]
@@ -223,19 +240,19 @@ export default {
}
},
handleReply(post) {
handleReply(post: Post): void {
console.log('Reply to post:', post.id)
// TODO: Implement reply functionality
// Could open a reply form or navigate to a reply page
},
handleEdit(post) {
handleEdit(post: Post): void {
console.log('Edit post:', post.id)
// TODO: Implement edit functionality
// Could open an edit dialog or navigate to edit page
},
async handleDelete(post) {
async handleDelete(post: Post): Promise<void> {
console.log('Delete post:', post.id)
// TODO: Implement delete functionality with confirmation
// if (confirm(t('forum', 'Are you sure you want to delete this post?'))) {
@@ -244,17 +261,17 @@ export default {
// }
},
replyToThread() {
replyToThread(): void {
console.log('Reply to thread:', this.thread?.id)
// TODO: Implement reply to thread functionality
// Could open a reply form at the bottom or navigate to a reply page
},
goBack() {
goBack(): void {
this.$router.back()
},
},
}
})
</script>
<style scoped lang="scss">