refactor: extract PublicNoteList shared component

Move the duplicated <ul> + infinite scroll + note row markup into a
new PublicNoteList component with a #meta scoped slot. Both list views
now delegate rendering to it, supplying only their view-specific
author/date markup via the slot.
This commit is contained in:
Julien Calixte
2026-02-20 11:52:28 +01:00
parent b9744b3734
commit 6089a109ff
3 changed files with 99 additions and 112 deletions

View File

@@ -0,0 +1,65 @@
<script setup lang="ts">
import { PublicNoteListItem } from "@/modules/note/models/Note"
import { slugify } from "@/utils/slugify"
import { vInfiniteScroll } from "@vueuse/components"
defineProps<{
notes: PublicNoteListItem[]
canLoadMore: boolean
onLoadMore: () => Promise<void>
}>()
defineSlots<{
meta(props: { note: PublicNoteListItem }): unknown
}>()
</script>
<template>
<ul
class="list rounded-box shadow-sm"
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]"
>
<li v-for="note in notes" class="list-row">
<div class="list-col">
<router-link
:to="{
name: 'PublicNoteView',
params: {
did: note.did,
rkey: note.rkey,
slug: slugify(note.title),
},
}"
class="btn btn-link"
>{{ note.title }}</router-link
>
<div class="text-xs opacity-80 alias">
<slot name="meta" :note="note" />
</div>
</div>
</li>
</ul>
</template>
<style scoped lang="scss">
li {
display: flex;
.list-col {
display: flex;
flex-direction: column;
flex: 1;
}
a {
display: inline;
text-align: left;
}
.alias {
text-align: right;
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import BackButton from "@/components/BackButton.vue" import BackButton from "@/components/BackButton.vue"
import PublicNoteList from "@/components/PublicNoteList.vue"
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook" import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
import { getAuthor } from "@/modules/atproto/getAuthor" import { getAuthor } from "@/modules/atproto/getAuthor"
import { slugify } from "@/utils/slugify"
import { computedAsync } from "@vueuse/core" import { computedAsync } from "@vueuse/core"
import { computed } from "vue" import { computed } from "vue"
import { vInfiniteScroll } from "@vueuse/components"
const props = defineProps<{ did: string }>() const props = defineProps<{ did: string }>()
const did = computed(() => props.did) const did = computed(() => props.did)
@@ -23,33 +22,17 @@ const author = computedAsync(async () => getAuthor(did.value))
</div> </div>
<div v-if="isLoading"></div> <div v-if="isLoading"></div>
<div v-else> <div v-else>
<ul <PublicNoteList
class="list rounded-box shadow-sm" :notes="notes"
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]" :can-load-more="canLoadMore"
:on-load-more="onLoadMore"
> >
<li v-for="note in notes" class="list-row"> <template #meta="{ note }">
<div class="list-col">
<router-link
:to="{
name: 'PublicNoteView',
params: {
did: note.did,
rkey: note.rkey,
slug: slugify(note.title),
},
}"
class="btn btn-link"
>{{ note.title }}</router-link
>
<div class="text-xs opacity-80 alias">
<span v-if="note.publishedAt"> <span v-if="note.publishedAt">
{{ new Date(note.publishedAt).toLocaleDateString() }} {{ new Date(note.publishedAt).toLocaleDateString() }}
</span> </span>
</div> </template>
</div> </PublicNoteList>
</li>
</ul>
</div> </div>
</main> </main>
</template> </template>
@@ -81,27 +64,6 @@ const author = computedAsync(async () => getAuthor(did.value))
align-items: center; align-items: center;
} }
li {
display: flex;
.list-col {
display: flex;
flex-direction: column;
flex: 1;
}
a {
display: inline;
text-align: left;
}
.alias {
text-align: right;
display: flex;
justify-content: flex-end;
}
}
@media screen and (min-width: 769px) { @media screen and (min-width: 769px) {
overflow-y: auto; overflow-y: auto;
} }

View File

@@ -1,8 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import BackButton from "@/components/BackButton.vue" import BackButton from "@/components/BackButton.vue"
import PublicNoteList from "@/components/PublicNoteList.vue"
import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook" import { usePublicNoteList } from "@/hooks/usePublicNoteList.hook"
import { slugify } from "@/utils/slugify"
import { vInfiniteScroll } from "@vueuse/components"
const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } = const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
usePublicNoteList() usePublicNoteList()
@@ -16,26 +15,12 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
</div> </div>
<div v-if="isLoading"></div> <div v-if="isLoading"></div>
<div v-else> <div v-else>
<ul <PublicNoteList
class="list rounded-box shadow-sm" :notes="notes"
v-infinite-scroll="[onLoadMore, { canLoadMore: () => canLoadMore }]" :can-load-more="canLoadMore"
:on-load-more="onLoadMore"
> >
<li v-for="note in notes" class="list-row"> <template #meta="{ note }">
<div class="list-col">
<router-link
:to="{
name: 'PublicNoteView',
params: {
did: note.did,
rkey: note.rkey,
slug: slugify(note.title),
},
}"
class="btn btn-link"
>{{ note.title }}</router-link
>
<div class="text-xs opacity-80 alias">
<router-link <router-link
v-if="getAuthor(note.did)" v-if="getAuthor(note.did)"
:to="{ :to="{
@@ -49,15 +34,11 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
<template v-if="note.publishedAt"> <template v-if="note.publishedAt">
<span>&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;</span>
<span>{{ <span>{{ new Date(note.publishedAt).toLocaleDateString() }}</span>
new Date(note.publishedAt).toLocaleDateString()
}}</span>
</template> </template>
<div v-else class="skeleton h-4 w-20"></div> <div v-else class="skeleton h-4 w-20"></div>
</div> </template>
</div> </PublicNoteList>
</li>
</ul>
</div> </div>
</main> </main>
</template> </template>
@@ -89,27 +70,6 @@ const { notes, isLoading, canLoadMore, onLoadMore, getAuthor } =
align-items: center; align-items: center;
} }
li {
display: flex;
.list-col {
display: flex;
flex-direction: column;
flex: 1;
}
a {
display: inline;
text-align: left;
}
.alias {
text-align: right;
display: flex;
justify-content: flex-end;
}
}
@media screen and (min-width: 769px) { @media screen and (min-width: 769px) {
overflow-y: auto; overflow-y: auto;
} }