fix sync issue, frontend improvement
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
padding: 0.5rem;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
@@ -69,7 +69,7 @@ a,
|
||||
.feed-title {
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New';
|
||||
font-size: clamp(1.1rem, 4vw, 1.4rem);
|
||||
font-size: clamp(1.25rem, 4.5vw, 1.6rem);
|
||||
font-weight: bold;
|
||||
color: var(--color-accent-2);
|
||||
border-bottom: 1px solid #ccc;
|
||||
@@ -110,6 +110,6 @@ h3 {
|
||||
|
||||
@media (min-width: 768px) {
|
||||
#app {
|
||||
padding: 2rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useFeeds } from '@/composables/useFeeds'
|
||||
|
||||
const router = useRouter()
|
||||
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead } = useFeeds()
|
||||
const { sync, showModal, viewMode, toggleViewMode, layout, toggleLayout, markAllRead, feeds } = useFeeds()
|
||||
|
||||
const menuOpen = ref(false)
|
||||
|
||||
@@ -52,7 +52,7 @@ function handleToggleLayout() {
|
||||
<template>
|
||||
<header class="app-nav">
|
||||
<div class="app-nav__wrapper">
|
||||
<span class="app-nav__title">RSS Reader</span>
|
||||
<span class="app-nav__title">RSS Reader<span v-if="feeds.length" class="app-nav__unread"> ({{ feeds.length }})</span></span>
|
||||
<button
|
||||
class="app-nav__hamburger"
|
||||
type="button"
|
||||
@@ -111,6 +111,11 @@ function handleToggleLayout() {
|
||||
font-size: clamp(1.1rem, 4vw, 1.4rem);
|
||||
}
|
||||
|
||||
.app-nav__unread {
|
||||
font-weight: normal;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.app-nav__hamburger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -17,8 +17,20 @@ const {
|
||||
fetchData,
|
||||
getReadable,
|
||||
setInitialLoad,
|
||||
showMessageForXSeconds,
|
||||
} = useFeeds()
|
||||
|
||||
const shareLabel = navigator.share ? 'Share' : 'Copy link'
|
||||
|
||||
async function shareUrl(url) {
|
||||
if (navigator.share) {
|
||||
await navigator.share({ url })
|
||||
} else {
|
||||
await navigator.clipboard.writeText(url)
|
||||
showMessageForXSeconds('Link copied.', 2)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
setInitialLoad(false)
|
||||
await fetchData()
|
||||
@@ -49,6 +61,9 @@ onMounted(async () => {
|
||||
<h3>{{ feed.timestamp }}</h3>
|
||||
<p v-if="!feed.readable" class="feed-original-link">
|
||||
<a :href="feed.url" target="_blank" rel="noopener noreferrer">Read original article ↗</a>
|
||||
<button type="button" class="feed-share-btn" :title="shareLabel" @click="shareUrl(feed.url)" :aria-label="shareLabel">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg>
|
||||
</button>
|
||||
</p>
|
||||
<p class="feed-content" v-html='feed.content'></p>
|
||||
</div>
|
||||
@@ -64,6 +79,7 @@ onMounted(async () => {
|
||||
<h3>{{ feeds[currentIndex].timestamp }}</h3>
|
||||
<p v-if="!feeds[currentIndex].readable" class="feed-original-link">
|
||||
<a :href="feeds[currentIndex].url" target="_blank" rel="noopener noreferrer">Read original article ↗</a>
|
||||
<button type="button" class="feed-share-btn" :title="shareLabel" @click="shareUrl(feeds[currentIndex].url)">{{ shareLabel }}</button>
|
||||
</p>
|
||||
<p class="feed-content" v-html="feeds[currentIndex].content"></p>
|
||||
</template>
|
||||
@@ -123,6 +139,13 @@ onMounted(async () => {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.feed-original-link {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.feed-original-link a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -136,6 +159,25 @@ onMounted(async () => {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.feed-share-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
opacity: 0.45;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.feed-share-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.article-single {
|
||||
position: relative;
|
||||
padding-bottom: 5rem;
|
||||
@@ -179,10 +221,14 @@ onMounted(async () => {
|
||||
line-height: 1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
||||
cursor: pointer;
|
||||
opacity: 0.55;
|
||||
transition: opacity 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.article-nav__btn:hover:not(:disabled) {
|
||||
.article-nav__btn:hover:not(:disabled),
|
||||
.article-nav__btn:focus-visible {
|
||||
border-color: var(--color-border-hover);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.article-nav__btn:disabled {
|
||||
|
||||
@@ -148,6 +148,26 @@ describe('AppNav', () => {
|
||||
confirmSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('shows the unread count in the title when there are articles', async () => {
|
||||
const { feeds } = useFeeds()
|
||||
feeds.value = [
|
||||
{ id: 1, title: 'Article one', content: '', url: 'https://example.test/1', timestamp: '2026-01-01' },
|
||||
{ id: 2, title: 'Article two', 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('(2)')
|
||||
})
|
||||
|
||||
it('hides the unread count when there are no articles', async () => {
|
||||
const wrapper = mount(AppNav, { global: { plugins: [router] } })
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.app-nav__unread').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('does not mark articles as read when the confirmation is dismissed', async () => {
|
||||
const { feeds } = useFeeds()
|
||||
feeds.value = [
|
||||
|
||||
@@ -10,7 +10,7 @@ const message = ref('')
|
||||
const showModal = ref(false)
|
||||
const viewMode = ref('list') // 'list' | 'article' — toggled from the hamburger menu
|
||||
const currentIndex = ref(0)
|
||||
const layout = ref('list') // 'list' | 'cards' — list-view display style, toggled from the hamburger menu
|
||||
const layout = ref(localStorage.getItem('layout') || 'list') // 'list' | 'cards' — list-view display style, toggled from the hamburger menu
|
||||
|
||||
let observer; // Declare observer outside the setup function
|
||||
let initialLoad = false
|
||||
@@ -219,6 +219,7 @@ function toggleViewMode() {
|
||||
|
||||
function toggleLayout() {
|
||||
layout.value = layout.value === 'list' ? 'cards' : 'list'
|
||||
localStorage.setItem('layout', layout.value)
|
||||
}
|
||||
|
||||
function nextArticle() {
|
||||
|
||||
Reference in New Issue
Block a user