diff --git a/app/components/Post/PostEditor.vue b/app/components/Post/PostEditor.vue
index 29cc353..c5b5573 100644
--- a/app/components/Post/PostEditor.vue
+++ b/app/components/Post/PostEditor.vue
@@ -23,8 +23,6 @@ import { ref } from 'vue'
import * as tus from 'tus-js-client'
import { useSolarNetwork } from '~/composables/useSolarNetwork'
-import PubSelect from './PubSelect.vue'
-
// Interface for uploaded files in the editor
interface UploadedFile {
name: string
diff --git a/app/components/Post/PostItem.vue b/app/components/Post/PostItem.vue
index 7fc1b54..8e954a8 100644
--- a/app/components/Post/PostItem.vue
+++ b/app/components/Post/PostItem.vue
@@ -22,6 +22,10 @@
+
+
+
{{ props.item.repliesCount }} replies
+
Post truncated, tap to see details...
@@ -47,10 +51,6 @@ import { ref, watch } from "vue"
import { useMarkdownProcessor } from "~/composables/useMarkdownProcessor"
import type { SnPost } from "~/types/api"
-import PostHeader from "./PostHeader.vue"
-import AttachmentList from "./AttachmentList.vue"
-import PostReactionList from "./PostReactionList.vue"
-
const props = defineProps<{ item: SnPost }>()
const emit = defineEmits<{
react: [symbol: string, attitude: number, delta: number]
diff --git a/app/components/Post/PostList.vue b/app/components/Post/PostList.vue
new file mode 100644
index 0000000..69496c2
--- /dev/null
+++ b/app/components/Post/PostList.vue
@@ -0,0 +1,73 @@
+
+
+
+
+ {{ error }}
+
+
+
+
+
+ $emit('react', item.id, symbol, attitude, delta)"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Refresh
+
+
+
+
+
+
diff --git a/app/components/Post/RepliesList.vue b/app/components/Post/RepliesList.vue
new file mode 100644
index 0000000..fb0b69e
--- /dev/null
+++ b/app/components/Post/RepliesList.vue
@@ -0,0 +1,78 @@
+
+
+
+
+ {{ error }}
+
+
+
+
+
+
+ $emit('react', item.id, symbol, attitude, delta)
+ "
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/composables/usePostList.ts b/app/composables/usePostList.ts
new file mode 100644
index 0000000..3d24eed
--- /dev/null
+++ b/app/composables/usePostList.ts
@@ -0,0 +1,156 @@
+import { ref, computed } from "vue"
+import type { SnPost } from "~/types/api"
+
+export interface PostListParams {
+ pubName?: string
+ realm?: string
+ type?: number
+ categories?: string[]
+ tags?: string[]
+ pinned?: boolean
+ shuffle?: boolean
+ includeReplies?: boolean
+ mediaOnly?: boolean
+ queryTerm?: string
+ order?: string
+ periodStart?: number
+ periodEnd?: number
+ orderDesc?: boolean
+}
+
+export interface PostListState {
+ posts: SnPost[]
+ loading: boolean
+ error: string | null
+ hasMore: boolean
+ cursor: string | null
+ total: number
+}
+
+export const usePostList = (params: PostListParams = {}) => {
+ const api = useSolarNetwork()
+ const pageSize = 20
+
+ const state = ref
({
+ posts: [],
+ loading: false,
+ error: null,
+ hasMore: true,
+ cursor: null,
+ total: 0
+ })
+
+ const isLoading = computed(() => state.value.loading)
+ const hasError = computed(() => state.value.error !== null)
+ const posts = computed(() => state.value.posts)
+ const hasMore = computed(() => state.value.hasMore)
+
+ const buildQueryParams = (cursor: string | null = null) => {
+ const offset = cursor ? parseInt(cursor) : 0
+
+ const queryParams: Record<
+ string,
+ string | number | boolean | string[] | undefined
+ > = {
+ offset,
+ take: pageSize,
+ replies: params.includeReplies,
+ orderDesc: params.orderDesc ?? true
+ }
+
+ if (params.shuffle) queryParams.shuffle = params.shuffle
+ if (params.pubName) queryParams.pub = params.pubName
+ if (params.realm) queryParams.realm = params.realm
+ if (params.type !== undefined) queryParams.type = params.type
+ if (params.tags) queryParams.tags = params.tags
+ if (params.categories) queryParams.categories = params.categories
+ if (params.pinned !== undefined) queryParams.pinned = params.pinned
+ if (params.order) queryParams.order = params.order
+ if (params.periodStart) queryParams.periodStart = params.periodStart
+ if (params.periodEnd) queryParams.periodEnd = params.periodEnd
+ if (params.queryTerm) queryParams.query = params.queryTerm
+ if (params.mediaOnly !== undefined) queryParams.media = params.mediaOnly
+
+ return queryParams
+ }
+
+ const fetchPosts = async (cursor: string | null = null, append = false) => {
+ try {
+ state.value.loading = true
+ state.value.error = null
+
+ const queryParams = buildQueryParams(cursor)
+
+ let total: number = 0
+ const response = await api("/sphere/posts", {
+ method: "GET",
+ query: queryParams,
+ onResponse({ response }) {
+ total = parseInt(response.headers.get("x-total") || "0")
+ if (response._data) {
+ response._data = keysToCamel(response._data)
+ }
+ }
+ })
+
+ const fetchedPosts = response
+
+ if (append) {
+ state.value.posts = [...state.value.posts, ...fetchedPosts]
+ } else {
+ state.value.posts = fetchedPosts
+ }
+
+ // Check if we've reached the end based on X-Total header
+ const currentTotal = state.value.posts.length
+ const hasReachedEnd = total > 0 && currentTotal >= total
+
+ state.value.hasMore = !hasReachedEnd && fetchedPosts.length === pageSize
+ state.value.cursor = state.value.hasMore
+ ? ((cursor ? parseInt(cursor) : 0) + fetchedPosts.length).toString()
+ : null
+
+ return { hasReachedEnd }
+ } catch (error) {
+ state.value.error =
+ error instanceof Error ? error.message : "Failed to fetch posts"
+ console.error("Error fetching posts:", error)
+ return { hasReachedEnd: false }
+ } finally {
+ state.value.loading = false
+ }
+ }
+
+ const loadMore = async (options?: {
+ side: string
+ done: (status: "empty" | "loading" | "error") => void
+ }) => {
+ if (!state.value.hasMore || state.value.loading) {
+ options?.done("empty")
+ return
+ }
+
+ const result = await fetchPosts(state.value.cursor, true)
+
+ if (result.hasReachedEnd) {
+ options?.done("empty")
+ }
+ }
+
+ const refresh = () => {
+ fetchPosts(null, false)
+ }
+
+ // Initial load
+ fetchPosts()
+
+ return {
+ posts,
+ isLoading,
+ hasError,
+ hasMore,
+ error: computed(() => state.value.error),
+ loadMore,
+ refresh
+ }
+}
diff --git a/app/composables/useRepliesList.ts b/app/composables/useRepliesList.ts
new file mode 100644
index 0000000..d67ed67
--- /dev/null
+++ b/app/composables/useRepliesList.ts
@@ -0,0 +1,128 @@
+import { ref, computed } from "vue"
+import type { SnPost } from "~/types/api"
+
+export interface RepliesListParams {
+ postId: string
+}
+
+export interface RepliesListState {
+ replies: SnPost[]
+ loading: boolean
+ error: string | null
+ hasMore: boolean
+ cursor: string | null
+ total: number
+}
+
+export const useRepliesList = (params: RepliesListParams) => {
+ const api = useSolarNetwork()
+ const pageSize = 20
+
+ const state = ref({
+ replies: [],
+ loading: false,
+ error: null,
+ hasMore: true,
+ cursor: null,
+ total: 0
+ })
+
+ const isLoading = computed(() => state.value.loading)
+ const hasError = computed(() => state.value.error !== null)
+ const replies = computed(() => state.value.replies)
+ const hasMore = computed(() => state.value.hasMore)
+
+ const buildQueryParams = (cursor: string | null = null) => {
+ const offset = cursor ? parseInt(cursor) : 0
+
+ const queryParams: Record = {
+ offset,
+ take: pageSize
+ }
+
+ return queryParams
+ }
+
+ const fetchReplies = async (cursor: string | null = null, append = false) => {
+ try {
+ state.value.loading = true
+ state.value.error = null
+
+ const queryParams = buildQueryParams(cursor)
+
+ let total: number = 0
+ const response = await api(
+ `/sphere/posts/${params.postId}/replies`,
+ {
+ method: "GET",
+ query: queryParams,
+ onResponse({ response }) {
+ total = parseInt(response.headers.get("x-total") || "0")
+ if (response._data) {
+ response._data = keysToCamel(response._data)
+ }
+ }
+ }
+ )
+
+ const fetchedReplies = response
+
+ if (append) {
+ state.value.replies = [...state.value.replies, ...fetchedReplies]
+ } else {
+ state.value.replies = fetchedReplies
+ }
+
+ // Check if we've reached the end based on X-Total header
+ const currentTotal = state.value.replies.length
+ const hasReachedEnd = total > 0 && currentTotal >= total
+
+ state.value.hasMore = !hasReachedEnd && fetchedReplies.length === pageSize
+ state.value.cursor = state.value.hasMore
+ ? ((cursor ? parseInt(cursor) : 0) + fetchedReplies.length).toString()
+ : null
+
+ return { hasReachedEnd }
+ } catch (error) {
+ state.value.error =
+ error instanceof Error ? error.message : "Failed to fetch replies"
+ console.error("Error fetching replies:", error)
+ return { hasReachedEnd: false }
+ } finally {
+ state.value.loading = false
+ }
+ }
+
+ const loadMore = async (options?: {
+ side: string
+ done: (status: "empty" | "loading" | "error" | "ok") => void
+ }) => {
+ if (!state.value.hasMore || state.value.loading) {
+ options?.done("empty")
+ return
+ }
+
+ const result = await fetchReplies(state.value.cursor, true)
+
+ if (result.hasReachedEnd) {
+ options?.done("empty")
+ }
+ }
+
+ const refresh = () => {
+ fetchReplies(null, false)
+ }
+
+ // Initial load
+ fetchReplies()
+
+ return {
+ replies,
+ isLoading,
+ hasError,
+ hasMore,
+ error: computed(() => state.value.error),
+ loadMore,
+ refresh
+ }
+}
diff --git a/app/pages/posts/[id].vue b/app/pages/posts/[id].vue
index 09e6926..727f41d 100644
--- a/app/pages/posts/[id].vue
+++ b/app/pages/posts/[id].vue
@@ -70,6 +70,10 @@
+
+
+
+
@@ -162,7 +166,9 @@ const {
const post = computed(() => postData.value?.post || null)
const htmlContent = computed(() => postData.value?.html || "")
-const classesContent = computed(() => postData.value?.post.type == 1 ? 'prose-xl' : 'prose-md');
+const classesContent = computed(() =>
+ postData.value?.post.type == 1 ? "prose-xl" : "prose-md"
+)
useHead({
title: computed(() => {