implemented infinite scrolling with load more functionality, pagination support, and expanded state handling for articles

This commit is contained in:
2025-08-02 23:53:16 +02:00
parent b82d94c230
commit ab133d0e4f
4 changed files with 125 additions and 20 deletions

View File

@@ -26,7 +26,8 @@
- [ ] Duplicate article detection - Avoid showing same story from multiple sources
## User Experience
- [ ] Dark/light theme toggle
- [x] Dark/light theme toggle
- [x] Infinite scroll / load more functionality for articles
- [ ] Customizable article layout - List view, card view, magazine style
- [ ] Keyboard shortcuts - Navigate articles without mouse
- [ ] Offline reading support - Cache articles for offline access

View File

@@ -290,7 +290,7 @@ const availableCountries = computed(() => {
return Array.from(countries).sort();
});
const articleCount = computed(() => news.articles.length);
const articleCount = computed(() => news.allArticles.length);
// Date presets
const datePresets: DatePreset[] = [

View File

@@ -44,16 +44,19 @@
<!-- Summary -->
<p
class="text-sm sm:text-base text-gray-700 dark:text-gray-300 line-clamp-3 mb-4 leading-relaxed">
class="text-sm sm:text-base text-gray-700 dark:text-gray-300 line-clamp-5 leading-relaxed">
{{ article.summary }}
</p>
</div>
<!-- View Summary Button -->
<!-- Article Footer -->
<div
class="flex justify-between items-center gap-4 p-4 sm:p6">
<button
@click="openModal(article)"
class="inline-flex items-center text-sm font-medium text-green-600 hover:text-green-800 transition-colors mb-3"
class="flex-1 inline-flex items-center justify-center cursor-pointer px-4 py-2 text-sm font-medium rounded-lg bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50 transition-colors"
>
View full summary
Full summary
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
@@ -61,30 +64,33 @@
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
</button>
</div>
<!-- Article Footer -->
<div class="px-4 sm:px-6 pb-4 sm:pb-6">
<a
:href="article.url"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center text-sm font-medium text-green-600 hover:text-green-800 transition-colors"
class="flex-1 inline-flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50 transition-colors"
>
Read full article
Full article
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/>
</svg>
</a>
</div>
</article>
</div>
<!-- Load More Button (if you want to add pagination later) -->
<div v-if="news.articles.length > 0" class="text-center mt-8">
<p class="text-sm text-gray-500">
Showing {{ news.articles.length }} articles
<!-- Loading State & Load More Trigger -->
<div v-if="news.articles.length > 0" class="text-center mt-8" ref="loadMoreTrigger">
<p v-if="loading" class="text-sm text-gray-500">
Loading more articles...
</p>
<p v-else-if="!news.hasMore" class="text-sm text-gray-500">
No more articles to load
</p>
<p v-else class="text-sm text-gray-500">
Showing {{ news.articles.length }} of {{ news.allArticles.length }} articles
</p>
</div>
@@ -98,11 +104,63 @@
</template>
<script setup lang="ts">
import {ref} from 'vue';
import {ref, onMounted, onUnmounted, watch} from 'vue';
import {useNews} from '../stores/useNews';
import ArticleModal from './ArticleModal.vue';
const news = useNews();
const loading = ref(false);
const loadMoreArticles = async () => {
if (loading.value || !news.hasMore) return;
loading.value = true;
try {
await news.loadMore();
} catch (error) {
console.error('Error loading more articles:', error);
} finally {
loading.value = false;
}
};
const observer = ref<IntersectionObserver | null>(null);
const loadMoreTrigger = ref<HTMLElement | null>(null);
onMounted(() => {
observer.value = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
loadMoreArticles();
}
},
{threshold: 0.5}
);
if (loadMoreTrigger.value) {
observer.value.observe(loadMoreTrigger.value);
}
});
onUnmounted(() => {
if (observer.value) {
observer.value.disconnect();
}
});
// Watch for changes to the loadMoreTrigger element
watch(loadMoreTrigger, (newEl, oldEl) => {
if (observer.value) {
// Disconnect from old element if it exists
if (oldEl) {
observer.value.unobserve(oldEl);
}
// Observe new element if it exists
if (newEl) {
observer.value.observe(newEl);
}
}
});
// Modal state
const isModalOpen = ref(false);

View File

@@ -12,7 +12,20 @@ export const useNews = defineStore('news', {
created_at: number
}[],
lastSync: 0,
clientTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone
clientTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
page: 1,
pageSize: 12,
hasMore: true,
allArticles: [] as {
id: number,
title: string,
summary: string,
url: string,
published: number,
country: string,
created_at: number
}[],
currentFilters: {} as Record<string, string | boolean>
}),
actions: {
async loadLastSync() {
@@ -38,7 +51,11 @@ export const useNews = defineStore('news', {
if (res.ok) {
const data = await res.json();
this.articles = data;
this.allArticles = data;
this.currentFilters = filters;
this.page = 1;
this.hasMore = data.length > this.pageSize;
this.articles = data.slice(0, this.pageSize);
this.lastSync = Date.now();
}
},
@@ -61,7 +78,11 @@ export const useNews = defineStore('news', {
}
const data = await res.json();
this.articles = data;
this.allArticles = data;
this.currentFilters = filters;
this.page = 1;
this.hasMore = data.length > this.pageSize;
this.articles = data.slice(0, this.pageSize);
return data;
} catch (error) {
console.error('Failed to get news:', error);
@@ -69,6 +90,31 @@ export const useNews = defineStore('news', {
}
},
async loadMore() {
if (!this.hasMore) return;
const nextPage = this.page + 1;
const startIndex = this.page * this.pageSize;
const endIndex = startIndex + this.pageSize;
const nextPageItems = this.allArticles.slice(startIndex, endIndex);
if (nextPageItems.length > 0) {
this.articles = [...this.articles, ...nextPageItems];
this.page = nextPage;
this.hasMore = endIndex < this.allArticles.length;
} else {
this.hasMore = false;
}
},
// Reset pagination state
resetPagination() {
this.page = 1;
this.hasMore = this.allArticles.length > this.pageSize;
this.articles = this.allArticles.slice(0, this.pageSize);
},
// New convenience methods
async getAllNews() {
return this.getNews({ all_countries: 'true', all_dates: 'true' });