Filtered on publisher page

This commit is contained in:
2025-11-08 13:56:06 +08:00
parent 209be30d45
commit 23b1cb4a63
2 changed files with 239 additions and 6 deletions

View File

@@ -14,7 +14,7 @@
class="carousel-container rounded-lg overflow-hidden" class="carousel-container rounded-lg overflow-hidden"
:style="carouselStyle" :style="carouselStyle"
> >
<v-card width="100%" class="transition-all duration-300" border> <v-card width="100%" height="100%" class="transition-all duration-300" border>
<v-carousel <v-carousel
height="100%" height="100%"
hide-delimiter-background hide-delimiter-background
@@ -25,10 +25,12 @@
<v-carousel-item <v-carousel-item
v-for="attachment in attachments" v-for="attachment in attachments"
:key="attachment.id" :key="attachment.id"
height="100%"
cover cover
> >
<attachment-item <attachment-item
original original
class="h-full"
:item="attachment" :item="attachment"
/> />
</v-carousel-item> </v-carousel-item>

View File

@@ -9,7 +9,146 @@
<v-container> <v-container>
<div class="layout"> <div class="layout">
<div class="main"> <div class="main">
<post-list :params="{ pubName: username }" /> <!-- Filter Section -->
<v-card class="mb-4">
<v-tabs
v-model="activeCategoryTab"
color="primary"
class="mb-2"
grow
>
<v-tab value="all">All</v-tab>
<v-tab value="posts">Posts</v-tab>
<v-tab value="articles">Articles</v-tab>
</v-tabs>
<v-row>
<v-col cols="12" sm="6">
<v-checkbox
:model-value="includeReplies"
label="Include replies"
:indeterminate="includeReplies === null"
density="compact"
prepend-icon="mdi-reply"
hide-details
class="px-4"
@update:model-value="cycleIncludeReplies"
/>
</v-col>
<v-col cols="12" sm="6">
<v-checkbox
v-model="mediaOnly"
label="Media only"
density="compact"
hide-details
class="px-4"
prepend-icon="mdi-attachment"
/>
</v-col>
</v-row>
<v-checkbox
v-model="orderDesc"
label="Descending order"
density="compact"
prepend-icon="mdi-sort"
class="px-4"
hide-details
/>
<v-divider class="my-1" />
<v-list-item
title="Advanced filters"
prepend-icon="mdi-filter-variant"
@click="showAdvancedFilters = !showAdvancedFilters"
>
<template #append>
<v-icon>
{{
showAdvancedFilters ? "mdi-chevron-up" : "mdi-chevron-down"
}}
</v-icon>
</template>
</v-list-item>
<v-expand-transition>
<div v-if="showAdvancedFilters" class="my-3 px-4">
<v-text-field
v-model="queryTerm"
hide-details
label="Search"
placeholder="Search posts"
prepend-inner-icon="mdi-magnify"
variant="outlined"
density="comfortable"
class="mb-3"
/>
<v-select
v-model="order"
hide-details
label="Sort by"
:items="[
{ title: 'Date', value: 'date' },
{ title: 'Popularity', value: 'popularity' }
]"
variant="outlined"
density="comfortable"
class="mb-3"
/>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
:model-value="periodStartFormatted"
hide-details
label="From date"
prepend-inner-icon="mdi-calendar"
variant="outlined"
density="comfortable"
readonly
@click="openDatePicker('start')"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
:model-value="periodEndFormatted"
hide-details
label="To date"
prepend-inner-icon="mdi-calendar"
variant="outlined"
density="comfortable"
readonly
@click="openDatePicker('end')"
/>
</v-col>
</v-row>
</div>
</v-expand-transition>
</v-card>
<post-list :key="filterKey" :params="postListParams" />
<!-- Date Picker Dialog -->
<v-dialog v-model="datePickerDialog" max-width="400">
<v-card>
<v-card-title>{{
datePickerType === "start" ? "From date" : "To date"
}}</v-card-title>
<v-card-text>
<v-date-picker
v-model="tempDate"
:max="new Date().toISOString().split('T')[0]"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn @click="datePickerDialog = false">Cancel</v-btn>
<v-btn color="primary" @click="confirmDatePicker">OK</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div> </div>
<div class="sidebar flex flex-col gap-3"> <div class="sidebar flex flex-col gap-3">
<v-card class="w-full"> <v-card class="w-full">
@@ -20,7 +159,9 @@
<div class="text-xl font-bold"> <div class="text-xl font-bold">
{{ user.nick || user.name }} {{ user.nick || user.name }}
</div> </div>
<div class="text-body-2 text-medium-emphasis">@{{ user.name }}</div> <div class="text-body-2 text-medium-emphasis">
@{{ user.name }}
</div>
</div> </div>
</div> </div>
<div v-if="htmlBio" class="bio-prose" v-html="htmlBio"></div> <div v-if="htmlBio" class="bio-prose" v-html="htmlBio"></div>
@@ -46,6 +187,7 @@
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { useMarkdownProcessor } from "~/composables/useMarkdownProcessor" import { useMarkdownProcessor } from "~/composables/useMarkdownProcessor"
import type { SnPublisher } from "~/types/api" import type { SnPublisher } from "~/types/api"
import type { PostListParams } from "~/composables/usePostList"
import PostList from "~/components/Post/PostList.vue" import PostList from "~/components/Post/PostList.vue"
@@ -88,9 +230,7 @@ const htmlBio = ref<string | undefined>(undefined)
watch( watch(
user, user,
(value) => { (value) => {
htmlBio.value = value?.bio htmlBio.value = value?.bio ? render(value.bio) : undefined
? render(value.bio)
: undefined
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
) )
@@ -106,6 +246,97 @@ const userPicture = computed(() => {
: undefined : undefined
}) })
// Filter state
const activeCategoryTab = ref("all")
const includeReplies = ref<boolean | null>(null)
const mediaOnly = ref(false)
const orderDesc = ref(true)
const showAdvancedFilters = ref(false)
const queryTerm = ref("")
const order = ref<string | undefined>(undefined)
const periodStart = ref<number | undefined>(undefined)
const periodEnd = ref<number | undefined>(undefined)
// Date picker dialog
const datePickerDialog = ref(false)
const datePickerType = ref<"start" | "end">("start")
const tempDate = ref<Date | undefined>(undefined)
const postListParams = computed<PostListParams>(() => {
const params: PostListParams = {
pubName: username.value,
includeReplies: includeReplies.value ?? undefined,
mediaOnly: mediaOnly.value,
orderDesc: orderDesc.value,
queryTerm: queryTerm.value || undefined,
order: order.value,
periodStart: periodStart.value,
periodEnd: periodEnd.value
}
// Set type based on active tab
if (activeCategoryTab.value === "posts") {
params.type = 0 // Assuming 0 is for posts
} else if (activeCategoryTab.value === "articles") {
params.type = 1 // Assuming 1 is for articles
}
// 'all' means no type filter
return params
})
const periodStartFormatted = computed(() => {
return periodStart.value
? new Date(periodStart.value * 1000).toISOString().split("T")[0]
: ""
})
const periodEndFormatted = computed(() => {
return periodEnd.value
? new Date(periodEnd.value * 1000).toISOString().split("T")[0]
: ""
})
// Create a key that changes when filters change to force PostList re-mount
const filterKey = computed(() => {
return JSON.stringify(postListParams.value)
})
const cycleIncludeReplies = () => {
if (includeReplies.value === null) {
includeReplies.value = false
} else if (includeReplies.value === false) {
includeReplies.value = true
} else {
includeReplies.value = null
}
}
const openDatePicker = (type: "start" | "end") => {
datePickerType.value = type
tempDate.value =
type === "start"
? periodStart.value
? new Date(periodStart.value * 1000)
: new Date()
: periodEnd.value
? new Date(periodEnd.value * 1000)
: new Date()
datePickerDialog.value = true
}
const confirmDatePicker = () => {
if (tempDate.value) {
const timestamp = Math.floor(tempDate.value.getTime() / 1000)
if (datePickerType.value === "start") {
periodStart.value = timestamp
} else {
periodEnd.value = timestamp
}
}
datePickerDialog.value = false
}
definePageMeta({ definePageMeta({
alias: ["/p/:name()"] alias: ["/p/:name()"]
}) })