all caught up icon, counter dynamic
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useFeeds } from '@/composables/useFeeds'
|
||||
|
||||
const router = useRouter()
|
||||
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feeds } = useFeeds()
|
||||
|
||||
const unreadCount = computed(() => feeds.value.filter(f => !f.read).length)
|
||||
|
||||
const menuOpen = ref(false)
|
||||
|
||||
function toggleMenu() {
|
||||
@@ -52,7 +54,7 @@ function handleToggleLayout() {
|
||||
<template>
|
||||
<header class="app-nav">
|
||||
<div class="app-nav__wrapper">
|
||||
<span class="app-nav__title">RSS Reader<span v-if="feeds.length" class="app-nav__unread"> ({{ feeds.length }})</span></span>
|
||||
<span class="app-nav__title">RSS Reader<span v-if="unreadCount" class="app-nav__unread"> ({{ unreadCount }})</span></span>
|
||||
<button
|
||||
class="app-nav__hamburger"
|
||||
type="button"
|
||||
|
||||
@@ -53,7 +53,13 @@ onMounted(async () => {
|
||||
<div v-if="showMessage" class="message">{{ message }}</div>
|
||||
|
||||
<div v-if="viewMode === 'list'" id='article' class='article' :class="{ 'article--cards': layout === 'cards' }">
|
||||
<p v-if="feeds.length == 0">No unread articles.</p>
|
||||
<div v-if="feeds.length == 0" class="empty-state">
|
||||
<svg class="empty-state__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="7 12.5 10.5 16 17 9"/>
|
||||
</svg>
|
||||
<p class="empty-state__label">All caught up</p>
|
||||
</div>
|
||||
<template v-for="( feed, index ) in feeds ">
|
||||
<div v-bind:id="index" class="observe">
|
||||
<p class="feed-source">{{ feed.feedTitle }}</p>
|
||||
@@ -72,7 +78,13 @@ onMounted(async () => {
|
||||
|
||||
<div v-else class="article-single">
|
||||
<button type="button" class="article-single__back" @click="leaveArticleView">← Back to list</button>
|
||||
<p v-if="feeds.length == 0">No unread articles.</p>
|
||||
<div v-if="feeds.length == 0" class="empty-state">
|
||||
<svg class="empty-state__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="7 12.5 10.5 16 17 9"/>
|
||||
</svg>
|
||||
<p class="empty-state__label">All caught up</p>
|
||||
</div>
|
||||
<template v-else>
|
||||
<p class="feed-source">{{ feeds[currentIndex].feedTitle }}</p>
|
||||
<h2 @click="getReadable(feeds[currentIndex], currentIndex)" class="feed-title">{{ feeds[currentIndex].title }}</h2>
|
||||
@@ -108,6 +120,25 @@ onMounted(async () => {
|
||||
/* Plain vertical stack of bordered "cards" — deliberately not flex/grid, and
|
||||
with no truncation/max-height: normal block flow lets each card grow to fit
|
||||
its own full content (images included), with no cross-element interaction. */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 4rem 1rem;
|
||||
gap: 1rem;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.empty-state__icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.empty-state__label {
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.article--cards .observe {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -161,6 +161,19 @@ describe('AppNav', () => {
|
||||
expect(wrapper.find('.app-nav__title').text()).toContain('(2)')
|
||||
})
|
||||
|
||||
it('excludes already-read articles from the counter while in article view', async () => {
|
||||
const { feeds } = useFeeds()
|
||||
feeds.value = [
|
||||
{ id: 1, title: 'Article one', read: true, content: '', url: 'https://example.test/1', timestamp: '2026-01-01' },
|
||||
{ id: 2, title: 'Article two', read: false, content: '', url: 'https://example.test/2', timestamp: '2026-01-02' },
|
||||
]
|
||||
|
||||
const wrapper = mount(AppNav, { global: { plugins: [router] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.app-nav__title').text()).toContain('(1)')
|
||||
})
|
||||
|
||||
it('hides the unread count when there are no articles', async () => {
|
||||
const wrapper = mount(AppNav, { global: { plugins: [router] } })
|
||||
await flushPromises()
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('RssFeeds', () => {
|
||||
await flushPromises()
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith('/api/v1/article/get/7', expect.anything())
|
||||
expect(wrapper.text()).toContain('No unread articles.')
|
||||
expect(wrapper.text()).toContain('All caught up')
|
||||
})
|
||||
|
||||
it('renders the fetched feed items', async () => {
|
||||
@@ -68,7 +68,7 @@ describe('RssFeeds', () => {
|
||||
|
||||
expect(wrapper.text()).toContain('Article one')
|
||||
expect(wrapper.text()).toContain('My Feed')
|
||||
expect(wrapper.text()).not.toContain('No unread articles.')
|
||||
expect(wrapper.text()).not.toContain('All caught up')
|
||||
})
|
||||
|
||||
it('renders the list as cards when the card layout is selected', async () => {
|
||||
|
||||
@@ -80,6 +80,7 @@ async function getReadable(feed, index) {
|
||||
base.setAttribute('href', feed.url);
|
||||
doc.head.prepend(base);
|
||||
doc.querySelectorAll('img').forEach(resolveTemplatedImage);
|
||||
doc.querySelectorAll('video, audio').forEach(el => el.remove());
|
||||
const article = new Readability(doc).parse();
|
||||
feeds.value[index].content = article.content;
|
||||
feeds.value[index].readable = true;
|
||||
|
||||
Reference in New Issue
Block a user