feat(nostr-feed): Replace NostrFeed with SubmissionList on Home page
The link aggregator is now the main feed, replacing the old NostrFeed component. FAB button navigates to /submit for creating posts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1904a54131
commit
cdba64d246
1 changed files with 28 additions and 240 deletions
|
|
@ -1,268 +1,56 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-screen bg-background">
|
<div class="flex flex-col h-screen bg-background">
|
||||||
<PWAInstallPrompt auto-show />
|
<PWAInstallPrompt auto-show />
|
||||||
<!-- TODO: Implement push notifications properly - currently commenting out admin notifications dialog -->
|
|
||||||
<!-- <NotificationPermission auto-show /> -->
|
|
||||||
|
|
||||||
<!-- Compact Header with Filters Toggle (Mobile) -->
|
<!-- Header -->
|
||||||
<div class="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
|
<div class="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
|
||||||
<div class="flex items-center justify-between px-4 py-2 sm:px-6">
|
<div class="flex items-center justify-between px-4 py-2 sm:px-6">
|
||||||
<h1 class="text-lg font-semibold">Feed</h1>
|
<h1 class="text-lg font-semibold">Feed</h1>
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<!-- Active Filter Indicator -->
|
|
||||||
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
|
||||||
<span v-if="activeFilterCount > 0">{{ activeFilterCount }} filters</span>
|
|
||||||
<span v-else>All content</span>
|
|
||||||
</div>
|
|
||||||
<!-- Filter Toggle Button -->
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
@click="showFilters = !showFilters"
|
|
||||||
class="h-8 w-8 p-0"
|
|
||||||
>
|
|
||||||
<Filter class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Collapsible Filter Panel -->
|
|
||||||
<div v-if="showFilters" class="border-t bg-background/95 backdrop-blur">
|
|
||||||
<div class="px-4 py-3 sm:px-6">
|
|
||||||
<FeedFilters v-model="selectedFilters" :admin-pubkeys="adminPubkeys" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main Feed Area - Takes remaining height with scrolling -->
|
<!-- Main Feed Area -->
|
||||||
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
||||||
<!-- Collapsible Composer -->
|
<div class="px-4 sm:px-6">
|
||||||
<div v-if="showComposer || replyTo" class="border-b bg-background sticky top-0 z-10">
|
<SubmissionList
|
||||||
<div class="max-h-[70vh] overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
:show-ranks="false"
|
||||||
<div class="px-4 py-3 sm:px-6">
|
:show-time-range="true"
|
||||||
<!-- Regular Note Composer -->
|
initial-sort="hot"
|
||||||
<NoteComposer
|
@submission-click="onSubmissionClick"
|
||||||
v-if="composerType === 'note' || replyTo"
|
|
||||||
:reply-to="replyTo"
|
|
||||||
@note-published="onNotePublished"
|
|
||||||
@clear-reply="onClearReply"
|
|
||||||
@close="onCloseComposer"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Rideshare Composer -->
|
|
||||||
<RideshareComposer
|
|
||||||
v-else-if="composerType === 'rideshare'"
|
|
||||||
@rideshare-published="onRidesharePublished"
|
|
||||||
@close="onCloseComposer"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Feed Content - Natural flow with padding for sticky elements -->
|
|
||||||
<div>
|
|
||||||
<NostrFeed
|
|
||||||
:feed-type="feedType"
|
|
||||||
:content-filters="selectedFilters"
|
|
||||||
:admin-pubkeys="adminPubkeys"
|
|
||||||
:key="feedKey"
|
|
||||||
:compact-mode="true"
|
|
||||||
@reply-to-note="onReplyToNote"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Floating Action Buttons for Compose -->
|
<!-- Floating Action Button for Create Post -->
|
||||||
<div v-if="!showComposer && !replyTo" class="fixed bottom-6 right-6 z-50">
|
<div class="fixed bottom-6 right-6 z-50">
|
||||||
<!-- Main compose button -->
|
<Button
|
||||||
<div class="flex flex-col items-end gap-3">
|
@click="navigateToSubmit"
|
||||||
<!-- Secondary buttons (when expanded) -->
|
size="lg"
|
||||||
<div v-if="showComposerOptions" class="flex flex-col gap-2">
|
class="h-14 w-14 rounded-full shadow-lg hover:shadow-xl transition-all bg-primary hover:bg-primary/90 border-2 border-primary-foreground/20 flex items-center justify-center p-0"
|
||||||
<Button
|
>
|
||||||
@click="openComposer('note')"
|
<Plus class="h-6 w-6 stroke-[2.5]" />
|
||||||
size="lg"
|
</Button>
|
||||||
class="h-12 px-4 rounded-full shadow-lg hover:shadow-xl transition-all gap-2 bg-card border-2 border-border hover:bg-accent text-card-foreground"
|
|
||||||
>
|
|
||||||
<MessageSquare class="h-4 w-4" />
|
|
||||||
<span class="text-sm font-medium">Note</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
@click="openComposer('rideshare')"
|
|
||||||
size="lg"
|
|
||||||
class="h-12 px-4 rounded-full shadow-lg hover:shadow-xl transition-all gap-2 bg-card border-2 border-border hover:bg-accent text-card-foreground"
|
|
||||||
>
|
|
||||||
<Car class="h-4 w-4" />
|
|
||||||
<span class="text-sm font-medium">Rideshare</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main FAB -->
|
|
||||||
<Button
|
|
||||||
@click="toggleComposerOptions"
|
|
||||||
size="lg"
|
|
||||||
class="h-14 w-14 rounded-full shadow-lg hover:shadow-xl transition-all bg-primary hover:bg-primary/90 border-2 border-primary-foreground/20 flex items-center justify-center p-0"
|
|
||||||
>
|
|
||||||
<Plus
|
|
||||||
class="h-6 w-6 stroke-[2.5] transition-transform duration-200"
|
|
||||||
:class="{ 'rotate-45': showComposerOptions }"
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Quick Filters Bar (Mobile) -->
|
|
||||||
<div class="md:hidden sticky bottom-0 z-30 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t">
|
|
||||||
<div class="flex overflow-x-auto px-4 py-2 gap-2 scrollbar-hide">
|
|
||||||
<Button
|
|
||||||
v-for="(preset, key) in quickFilterPresets"
|
|
||||||
:key="key"
|
|
||||||
:variant="isPresetActive(key) ? 'default' : 'outline'"
|
|
||||||
size="sm"
|
|
||||||
@click="setQuickFilter(key)"
|
|
||||||
class="whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{{ preset.label }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// NostrFeed is now registered globally by the nostr-feed module
|
import { useRouter } from 'vue-router'
|
||||||
// No need to import it directly - use the modular version
|
|
||||||
// TODO: Re-enable when push notifications are properly implemented
|
|
||||||
// import NotificationPermission from '@/components/notifications/NotificationPermission.vue'
|
|
||||||
import { ref, computed, watch } from 'vue'
|
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Filter, Plus, MessageSquare, Car } from 'lucide-vue-next'
|
import { Plus } from 'lucide-vue-next'
|
||||||
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
||||||
import FeedFilters from '@/modules/nostr-feed/components/FeedFilters.vue'
|
import SubmissionList from '@/modules/nostr-feed/components/SubmissionList.vue'
|
||||||
import NoteComposer from '@/modules/nostr-feed/components/NoteComposer.vue'
|
import type { SubmissionWithMeta } from '@/modules/nostr-feed/types/submission'
|
||||||
import RideshareComposer from '@/modules/nostr-feed/components/RideshareComposer.vue'
|
|
||||||
import NostrFeed from '@/modules/nostr-feed/components/NostrFeed.vue'
|
|
||||||
import { FILTER_PRESETS } from '@/modules/nostr-feed/config/content-filters'
|
|
||||||
import appConfig from '@/app.config'
|
|
||||||
import type { ContentFilter } from '@/modules/nostr-feed/services/FeedService'
|
|
||||||
import type { ReplyToNote } from '@/modules/nostr-feed/components/NoteComposer.vue'
|
|
||||||
|
|
||||||
// Get admin pubkeys from app config
|
const router = useRouter()
|
||||||
const adminPubkeys = appConfig.modules['nostr-feed']?.config?.adminPubkeys || []
|
|
||||||
|
|
||||||
// UI state
|
// Handle submission click - navigate to detail page
|
||||||
const showFilters = ref(false)
|
function onSubmissionClick(submission: SubmissionWithMeta) {
|
||||||
const showComposer = ref(false)
|
router.push({ name: 'submission-detail', params: { id: submission.id } })
|
||||||
const showComposerOptions = ref(false)
|
|
||||||
const composerType = ref<'note' | 'rideshare'>('note')
|
|
||||||
|
|
||||||
// Feed configuration
|
|
||||||
const selectedFilters = ref<ContentFilter[]>(FILTER_PRESETS.all)
|
|
||||||
const feedKey = ref(0) // Force feed component to re-render when filters change
|
|
||||||
|
|
||||||
// Note composer state
|
|
||||||
const replyTo = ref<ReplyToNote | undefined>()
|
|
||||||
|
|
||||||
// Quick filter presets for mobile bottom bar
|
|
||||||
const quickFilterPresets = {
|
|
||||||
all: { label: 'All', filters: FILTER_PRESETS.all },
|
|
||||||
announcements: { label: 'Announcements', filters: FILTER_PRESETS.announcements },
|
|
||||||
rideshare: { label: 'Rideshare', filters: FILTER_PRESETS.rideshare }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed properties
|
// Navigate to submit page
|
||||||
const activeFilterCount = computed(() => selectedFilters.value.length)
|
function navigateToSubmit() {
|
||||||
|
router.push({ name: 'submit-post' })
|
||||||
const isPresetActive = (presetKey: string) => {
|
|
||||||
const preset = quickFilterPresets[presetKey as keyof typeof quickFilterPresets]
|
|
||||||
if (!preset) return false
|
|
||||||
|
|
||||||
return preset.filters.length === selectedFilters.value.length &&
|
|
||||||
preset.filters.every(pf => selectedFilters.value.some(sf => sf.id === pf.id))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine feed type based on selected filters
|
|
||||||
const feedType = computed(() => {
|
|
||||||
if (selectedFilters.value.length === 0) return 'all'
|
|
||||||
|
|
||||||
// Check if it matches the 'all' preset
|
|
||||||
if (selectedFilters.value.length === FILTER_PRESETS.all.length &&
|
|
||||||
FILTER_PRESETS.all.every(pf => selectedFilters.value.some(sf => sf.id === pf.id))) {
|
|
||||||
return 'all'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it matches the announcements preset
|
|
||||||
if (selectedFilters.value.length === FILTER_PRESETS.announcements.length &&
|
|
||||||
FILTER_PRESETS.announcements.every(pf => selectedFilters.value.some(sf => sf.id === pf.id))) {
|
|
||||||
return 'announcements'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it matches the rideshare preset
|
|
||||||
if (selectedFilters.value.length === FILTER_PRESETS.rideshare.length &&
|
|
||||||
FILTER_PRESETS.rideshare.every(pf => selectedFilters.value.some(sf => sf.id === pf.id))) {
|
|
||||||
return 'rideshare'
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all other cases, use custom
|
|
||||||
return 'custom'
|
|
||||||
})
|
|
||||||
|
|
||||||
// Force feed to reload when filters change
|
|
||||||
watch(selectedFilters, () => {
|
|
||||||
feedKey.value++
|
|
||||||
}, { deep: true })
|
|
||||||
|
|
||||||
// Handle note composer events
|
|
||||||
// Methods
|
|
||||||
const setQuickFilter = (presetKey: string) => {
|
|
||||||
const preset = quickFilterPresets[presetKey as keyof typeof quickFilterPresets]
|
|
||||||
if (preset) {
|
|
||||||
selectedFilters.value = preset.filters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onNotePublished = (noteId: string) => {
|
|
||||||
console.log('Note published:', noteId)
|
|
||||||
// Refresh the feed to show the new note
|
|
||||||
feedKey.value++
|
|
||||||
// Clear reply state and hide composer
|
|
||||||
replyTo.value = undefined
|
|
||||||
showComposer.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClearReply = () => {
|
|
||||||
replyTo.value = undefined
|
|
||||||
showComposer.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onReplyToNote = (note: ReplyToNote) => {
|
|
||||||
replyTo.value = note
|
|
||||||
showComposer.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCloseComposer = () => {
|
|
||||||
showComposer.value = false
|
|
||||||
showComposerOptions.value = false
|
|
||||||
replyTo.value = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// New composer methods
|
|
||||||
const toggleComposerOptions = () => {
|
|
||||||
showComposerOptions.value = !showComposerOptions.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const openComposer = (type: 'note' | 'rideshare') => {
|
|
||||||
composerType.value = type
|
|
||||||
showComposer.value = true
|
|
||||||
showComposerOptions.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRidesharePublished = (noteId: string) => {
|
|
||||||
console.log('Rideshare post published:', noteId)
|
|
||||||
// Refresh the feed to show the new rideshare post
|
|
||||||
feedKey.value++
|
|
||||||
// Hide composer
|
|
||||||
showComposer.value = false
|
|
||||||
showComposerOptions.value = false
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue