added dark mode support across components and improved accessibility styling

This commit is contained in:
2025-08-02 23:15:35 +02:00
parent 93c06d10ee
commit b82d94c230
10 changed files with 281 additions and 157 deletions

View File

@@ -1,95 +1,105 @@
<template>
<div id="app" class="min-h-screen bg-gray-50">
<!-- Header -->
<header class="bg-white shadow-sm border-b sticky top-0 z-40">
<div id="app" class="min-h-screen bg-gray-50 dark:bg-gray-900">
<header
class="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700 sticky top-0 z-40">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<h1 class="text-xl sm:text-2xl font-bold text-gray-900">Owly News</h1>
<h1 class="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white">Owly News</h1>
<!-- Mobile Menu Button -->
<button
@click="showMobileMenu = !showMobileMenu"
class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100"
<div class="flex items-center gap-4">
<button
@click="toggleDarkMode"
class="p-2 rounded-md text-gray-400 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
>
<svg v-if="darkMode.isDarkMode" class="h-6 w-6" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"/>
</svg>
<svg v-else class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/>
</svg>
</button>
<button
@click="showMobileMenu = !showMobileMenu"
class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 cursor-pointer"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"/>
</svg>
</button>
<nav class="hidden lg:flex space-x-4">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="[
'px-3 py-2 rounded-md text-sm font-medium transition-colors cursor-pointer',
activeTab === tab.id
? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
]"
>
{{ tab.label }}
</button>
</nav>
</div>
<div
:class="{ 'hidden': !showMobileMenu }"
class="lg:hidden py-2 border-t"
>
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<!-- Desktop Navigation -->
<nav class="hidden lg:flex space-x-4">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
:class="[
'px-3 py-2 rounded-md text-sm font-medium transition-colors',
<div class="flex flex-col space-y-1">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id; showMobileMenu = false"
:class="[
'px-3 py-2 rounded-md text-left text-base font-medium transition-colors cursor-pointer',
activeTab === tab.id
? 'bg-green-100 text-green-700'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700'
]"
>
{{ tab.label }}
</button>
</nav>
</div>
<!-- Mobile Navigation -->
<div
:class="{ 'hidden': !showMobileMenu }"
class="lg:hidden py-2 border-t"
>
<div class="flex flex-col space-y-1">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id; showMobileMenu = false"
:class="[
'px-3 py-2 rounded-md text-left text-base font-medium transition-colors',
activeTab === tab.id
? 'bg-green-100 text-green-700'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
]"
>
{{ tab.label }}
</button>
>
{{ tab.label }}
</button>
</div>
</div>
</div>
</div>
</header>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8">
<!-- Management Tab -->
<div v-if="activeTab === 'management'" class="space-y-6">
<!-- Control Panel - Responsive Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<SyncButton />
<NewsRefreshButton />
<ModelStatus />
<SyncButton/>
<NewsRefreshButton/>
<ModelStatus/>
</div>
<!-- Management Tools -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<FeedManager />
<CronSlider />
<FeedManager/>
<CronSlider/>
</div>
</div>
<!-- News Tab -->
<div v-if="activeTab === 'news'" class="space-y-6">
<!-- News Filters -->
<NewsFilters />
<NewsFilters/>
<!-- News List -->
<NewsList />
<NewsList/>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useNews } from './stores/useNews';
import {ref, onMounted} from 'vue';
import {useNews} from './stores/useNews';
import {useDarkMode} from './stores/useDarkMode';
import SyncButton from './components/SyncButton.vue';
import NewsRefreshButton from './components/NewsRefreshButton.vue';
import ModelStatus from './components/ModelStatus.vue';
@@ -99,19 +109,27 @@ import NewsFilters from './components/NewsFilters.vue';
import NewsList from './components/NewsList.vue';
const news = useNews();
const darkMode = useDarkMode();
const activeTab = ref('news');
const showMobileMenu = ref(false);
const toggleDarkMode = () => {
darkMode.toggleDarkMode();
};
onMounted(() => {
darkMode.initDarkMode();
});
const tabs = [
{ id: 'news', label: 'News' },
{ id: 'management', label: 'Management' }
{id: 'news', label: 'News'},
{id: 'management', label: 'Management'}
];
onMounted(async () => {
await news.loadLastSync();
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (showMobileMenu.value && !(e.target as Element).closest('header')) {
showMobileMenu.value = false;
@@ -120,7 +138,6 @@ document.addEventListener('click', (e) => {
</script>
<style scoped>
/* Component-scoped styles to prevent global conflicts */
#app {
display: block;
grid-template-columns: none;

View File

@@ -1,4 +1,3 @@
<template>
<Teleport to="body">
<Transition
@@ -12,24 +11,26 @@
>
<div
v-if="isOpen"
class="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-md bg-white/20"
class="fixed inset-0 z-50 flex items-center justify-center p-4 backdrop-blur-md bg-white/20 dark:bg-black/20"
@click="closeModal"
>
<div
class="bg-white/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 max-w-2xl w-full max-h-[90vh] overflow-hidden modal-content"
class="bg-white/95 dark:bg-gray-800/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-white/20 dark:border-gray-700/20 max-w-2xl w-full max-h-[90vh] overflow-hidden modal-content"
@click.stop
>
<!-- Modal Header with glass effect -->
<div class="sticky top-0 bg-white/80 backdrop-blur-sm border-b border-white/30 px-6 py-4">
<div
class="sticky top-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border-b border-white/30 dark:border-gray-700/30 px-6 py-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<!-- Country badge with glass effect -->
<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-white/60 backdrop-blur-sm text-gray-800 border border-white/30 mb-2">
<span
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm text-gray-800 dark:text-gray-200 border border-white/30 dark:border-gray-600/30 mb-2">
{{ article?.country }}
</span>
<!-- Title -->
<h3 class="text-lg font-semibold text-gray-900 leading-6 pr-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white leading-6 pr-4">
{{ article?.title }}
</h3>
@@ -37,7 +38,7 @@
<time
v-if="article"
:datetime="new Date(article.published * 1000).toISOString()"
class="text-sm text-gray-600 block mt-2"
class="text-sm text-gray-600 dark:text-gray-400 block mt-2"
>
{{ formatDate(article.published) }}
</time>
@@ -46,11 +47,12 @@
<!-- Close button with glass effect -->
<button
@click="closeModal"
class="flex-shrink-0 bg-white/60 backdrop-blur-sm rounded-full p-2 text-gray-500 hover:text-gray-700 hover:bg-white/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 border border-white/30 transition-all duration-200"
class="flex-shrink-0 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm rounded-full p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-white/80 dark:hover:bg-gray-600/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 dark:focus:ring-green-600 border border-white/30 dark:border-gray-600/30 transition-all duration-200"
>
<span class="sr-only">Close</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
@@ -60,23 +62,28 @@
<div class="px-6 py-4 overflow-y-auto custom-scrollbar">
<!-- Full Summary -->
<div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center">
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
<h4
class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3 flex items-center">
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
Summary
</h4>
<div class="prose prose-sm max-w-none">
<p class="text-gray-800 leading-relaxed whitespace-pre-wrap">{{ article?.summary }}</p>
<p class="text-gray-800 dark:text-gray-200 leading-relaxed whitespace-pre-wrap">
{{ article?.summary }}</p>
</div>
</div>
</div>
<!-- Modal Footer with glass effect -->
<div class="sticky bottom-0 bg-white/80 backdrop-blur-sm border-t border-white/30 px-6 py-4 flex flex-col sm:flex-row gap-3 sm:justify-between">
<div
class="sticky bottom-0 bg-white/80 dark:bg-gray-800/80 backdrop-blur-sm border-t border-white/30 dark:border-gray-700/30 px-6 py-4 flex flex-col sm:flex-row gap-3 sm:justify-between">
<button
@click="closeModal"
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-gray-700 bg-white/60 backdrop-blur-sm border border-white/40 rounded-lg shadow-sm hover:bg-white/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 transition-all duration-200"
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white/60 dark:bg-gray-700/60 backdrop-blur-sm border border-white/40 dark:border-gray-600/40 rounded-lg shadow-sm hover:bg-white/80 dark:hover:bg-gray-600/80 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 dark:focus:ring-green-600 transition-all duration-200"
>
Close
</button>
@@ -90,7 +97,8 @@
>
Read Full Article
<svg class="w-4 h-4 ml-2" 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" />
<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"/>
</svg>
</a>
</div>
@@ -101,7 +109,7 @@
</template>
<script setup lang="ts">
import { watch, onUnmounted } from 'vue';
import {watch, onUnmounted} from 'vue';
interface Article {
id: number;
@@ -188,12 +196,20 @@ function formatDate(timestamp: number): string {
border-radius: 3px;
}
:root.dark .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(16, 185, 129, 0.3);
border-radius: 3px;
backdrop-filter: blur(4px);
}
:root.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(16, 185, 129, 0.4);
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(16, 185, 129, 0.5);
}

View File

@@ -20,10 +20,10 @@ async function update() {
}
</script>
<template>
<div class="p-4 bg-white rounded shadow">
<div class="p-4 bg-white dark:bg-gray-800 rounded shadow dark:shadow-gray-700 dark:text-gray-200">
<label class="block font-semibold mb-2">Fetch Interval (Stunden)</label>
<input type="range" min="0.5" max="24" step="0.5" v-model="hours" @change="update"
class="w-full"/>
class="w-full accent-green-600 dark:accent-green-500"/>
<p class="text-sm mt-2">{{ hours }} h <span v-if="saving" class="animate-pulse"></span></p>
</div>
</template>

View File

@@ -15,23 +15,30 @@ async function add() {
}
</script>
<template>
<div class="bg-white rounded p-4 shadow space-y-2">
<h2 class="font-semibold text-lg">Feeds verwalten</h2>
<div class="bg-white dark:bg-gray-800 rounded p-4 shadow space-y-2">
<h2 class="font-semibold text-lg dark:text-gray-200">Feeds verwalten</h2>
<div class="flex gap-2">
<select v-model="country" class="border rounded p-1 flex-1">
<select v-model="country"
class="border dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded p-1 flex-1">
<option>DE</option>
<option>FR</option>
<option>EU</option>
</select>
<input v-model="url" placeholder="https://…" class="border rounded p-1 flex-[3]"/>
<button @click="add" class="bg-green-600 text-white px-3 rounded">+</button>
<input v-model="url" placeholder="https://…"
class="border dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded p-1 flex-[3]"/>
<button @click="add"
class="bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-800 text-white px-3 rounded">
+
</button>
</div>
<ul class="list-disc pl-5">
<li v-for="(f, index) in feeds.list" :key="index" class="flex justify-between items-center">
<span>{{ f.country }} {{ f.url }}</span>
<button @click="feeds.remove(f.url)" class="text-red-600"></button>
<span class="dark:text-gray-200">{{ f.country }} {{ f.url }}</span>
<button @click="feeds.remove(f.url)"
class="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-500">
</button>
</li>
</ul>
</div>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useModel } from '../stores/useModel';
import {onMounted} from 'vue';
import {useModel} from '../stores/useModel';
const model = useModel();
@@ -10,29 +10,29 @@ onMounted(async () => {
</script>
<template>
<div class="bg-white rounded p-4 shadow">
<h3 class="font-semibold mb-2">Model Status</h3>
<div class="bg-white dark:bg-gray-800 rounded p-4 shadow">
<h3 class="font-semibold mb-2 text-gray-900 dark:text-white">Model Status</h3>
<div v-if="model.status === 'loading'" class="text-gray-600">
<div v-if="model.status === 'loading'" class="text-gray-600 dark:text-gray-400">
Loading model information...
</div>
<div v-else-if="model.status === 'error'" class="text-red-600">
<div v-else-if="model.status === 'error'" class="text-red-600 dark:text-red-400">
Error: {{ model.error }}
</div>
<div v-else>
<div class="flex items-center mb-2">
<span class="font-medium mr-2">Name:</span>
<span>{{ model.name }}</span>
<span class="font-medium mr-2 text-gray-900 dark:text-white">Name:</span>
<span class="dark:text-gray-200">{{ model.name }}</span>
</div>
<div class="flex items-center">
<span class="font-medium mr-2">Status:</span>
<span class="font-medium mr-2 text-gray-900 dark:text-white">Status:</span>
<span
:class="{
'text-green-600': model.status === 'ready',
'text-red-600': model.status === 'not available'
'text-green-600 dark:text-green-400': model.status === 'ready',
'text-red-600 dark:text-red-400': model.status === 'not available'
}"
>
{{ model.status }}

View File

@@ -1,12 +1,13 @@
<template>
<div class="bg-white rounded-lg shadow-sm border overflow-hidden">
<div
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border dark:border-gray-700 overflow-hidden">
<!-- Mobile Header with Toggle -->
<div class="lg:hidden">
<button
@click="showFilters = !showFilters"
class="w-full px-4 py-3 flex items-center justify-between bg-gray-50 hover:bg-gray-100 transition-colors"
>
<span class="font-medium text-gray-900">Filters</span>
<span class="font-medium text-gray-900 dark:text-white">Filters</span>
<div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">{{ articleCount }} articles</span>
<svg
@@ -16,16 +17,17 @@
stroke="currentColor"
viewBox="0 0 24 24"
>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 9l-7 7-7-7"/>
</svg>
</div>
</button>
</div>
<!-- Desktop Header -->
<div class="hidden lg:block px-6 py-4 border-b">
<div class="hidden lg:block px-6 py-4 border-b dark:border-gray-700">
<div class="flex items-center justify-between">
<h3 class="text-lg font-medium text-gray-900">News Filters</h3>
<h3 class="text-lg font-medium text-gray-900 dark:text-white">News Filters</h3>
<span class="text-sm text-gray-500">{{ articleCount }} articles found</span>
</div>
</div>
@@ -38,9 +40,9 @@
<!-- Mobile/Tablet: Stacked Layout -->
<div class="lg:hidden space-y-6">
<!-- Country Filter Mobile -->
<div class="bg-gray-50 rounded-lg p-4">
<h4 class="font-medium text-gray-900 mb-3">Countries</h4>
<div class="space-y-3">
<div class="bg-gray-50 dark:bg-gray-800 dark:text-gray-200 rounded-lg p-4">
<h4 class="font-medium text-gray-900 dark:text-white mb-3">Countries</h4>
<div class="space-y-3 ">
<label class="flex items-center">
<input
type="checkbox"
@@ -48,7 +50,7 @@
@change="onAllCountriesChange"
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span class="ml-3 text-sm font-medium">All Countries</span>
<span class="ml-3 text-sm dark:text-gray-200">All Countries</span>
</label>
<div v-if="!filters.allCountries" class="grid grid-cols-2 gap-2 pl-6">
<label v-for="country in availableCountries" :key="country" class="flex items-center">
@@ -65,8 +67,8 @@
</div>
<!-- Date Filter Mobile -->
<div class="bg-gray-50 rounded-lg p-4">
<h4 class="font-medium text-gray-900 mb-3">Date Range</h4>
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<h4 class="font-medium text-gray-900 dark:text-white mb-3">Date Range</h4>
<div class="space-y-3">
<label class="flex items-center">
<input
@@ -75,16 +77,16 @@
@change="onAllDatesChange"
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span class="ml-3 text-sm font-medium">All Dates</span>
<span class="ml-3 text-sm font-medium dark:text-gray-200">All Dates</span>
</label>
<div v-if="!filters.allDates" class="space-y-3 pl-6">
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">From</label>
<label class="block text-xs font-medium text-gray-700 dark:text-gray-200 mb-1">From</label>
<input
type="date"
v-model="filters.fromDate"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500 dark:text-gray-200"
/>
</div>
<div>
@@ -92,20 +94,20 @@
<input
type="date"
v-model="filters.toDate"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500 dark:text-gray-200"
/>
</div>
</div>
<!-- Quick Presets Mobile -->
<div>
<label class="block text-xs font-medium text-gray-700 mb-2">Quick Presets</label>
<h4 class="font-medium text-gray-900 dark:text-white mb-3">Quick Presets</h4>
<div class="grid grid-cols-2 gap-2">
<button
v-for="preset in datePresets"
:key="preset.label"
@click="applyDatePreset(preset)"
class="px-3 py-2 text-sm text-green-600 bg-green-50 hover:bg-green-100 rounded-md transition-colors"
class="px-3 py-2 text-sm text-green-600 bg-green-50 hover:bg-green-100 dark:hover:bg-green-700 rounded-md transition-colors"
>
{{ preset.label }}
</button>
@@ -123,9 +125,12 @@
class="flex-1 px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-medium transition-colors"
>
<span v-if="isLoading" class="flex items-center justify-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
</span>
@@ -145,7 +150,8 @@
<div class="hidden lg:grid lg:grid-cols-4 gap-6">
<!-- Country Filter Desktop -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Countries</label>
<label
class="block text-sm font-medium text-gray-700 dark:text-white mb-3">Countries</label>
<div class="space-y-2">
<label class="flex items-center">
<input
@@ -154,7 +160,7 @@
@change="onAllCountriesChange"
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span class="ml-2 text-sm font-medium">All Countries</span>
<span class="ml-2 text-sm font-medium dark:text-white">All Countries</span>
</label>
<div v-if="!filters.allCountries" class="space-y-1 max-h-40 overflow-y-auto">
<label v-for="country in availableCountries" :key="country" class="flex items-center">
@@ -164,7 +170,7 @@
v-model="filters.selectedCountries"
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span class="ml-2 text-sm">{{ country }}</span>
<span class="ml-2 text-sm dark:text-gray-200">{{ country }}</span>
</label>
</div>
</div>
@@ -172,7 +178,8 @@
<!-- Date Range Filter Desktop -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Date Range</label>
<label class="block text-sm font-medium text-gray-700 dark:text-white mb-3">Date
Range</label>
<div class="space-y-2">
<label class="flex items-center">
<input
@@ -181,23 +188,23 @@
@change="onAllDatesChange"
class="rounded border-gray-300 text-green-600 focus:ring-green-500"
/>
<span class="ml-2 text-sm font-medium">All Dates</span>
<span class="ml-2 text-sm font-medium dark:text-white">All Dates</span>
</label>
<div v-if="!filters.allDates" class="space-y-2">
<div>
<label class="block text-xs text-gray-500 mb-1">From</label>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">From</label>
<input
type="date"
v-model="filters.fromDate"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">To</label>
<label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">To</label>
<input
type="date"
v-model="filters.toDate"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:ring-green-500 focus:border-green-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
@@ -206,13 +213,14 @@
<!-- Quick Date Presets Desktop -->
<div v-if="!filters.allDates">
<label class="block text-sm font-medium text-gray-700 mb-3">Quick Presets</label>
<label class="block text-sm font-medium text-gray-700 dark:text-white mb-3">Quick
Presets</label>
<div class="space-y-1">
<button
v-for="preset in datePresets"
:key="preset.label"
@click="applyDatePreset(preset)"
class="block w-full text-left px-3 py-2 text-sm text-green-600 hover:bg-green-50 rounded-md transition-colors"
class="block w-full text-left px-3 py-2 text-sm text-green-600 dark:text-green-400 hover:bg-green-50 dark:hover:bg-green-900 rounded-md transition-colors"
>
{{ preset.label }}
</button>
@@ -224,12 +232,15 @@
<button
@click="applyFilters"
:disabled="isLoading"
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-medium transition-colors"
class="px-4 py-2 bg-green-600 text-white rounded-md hover:bg-green-700 disabled:bg-gray-400 dark:disabled:bg-gray-600 disabled:cursor-not-allowed font-medium transition-colors"
>
<span v-if="isLoading" class="flex items-center justify-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Loading...
</span>
@@ -249,9 +260,9 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue';
import { useNews } from '../stores/useNews';
import { useFeeds } from '../stores/useFeeds';
import {ref, onMounted, computed} from 'vue';
import {useNews} from '../stores/useNews';
import {useFeeds} from '../stores/useFeeds';
const news = useNews();
const feeds = useFeeds();

View File

@@ -2,11 +2,14 @@
<div>
<!-- Empty State -->
<div v-if="news.articles.length === 0" class="text-center py-12">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h4" />
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none"
stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h4"/>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No articles found</h3>
<p class="mt-2 text-gray-500">Try adjusting your filters to see more results.</p>
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-white">No articles found</h3>
<p class="mt-2 text-gray-500 dark:text-gray-400">Try adjusting your filters to see more
results.</p>
</div>
<!-- Articles Grid -->
@@ -14,12 +17,13 @@
<article
v-for="article in news.articles"
:key="article.id"
class="bg-white rounded-lg shadow-sm border hover:shadow-md transition-all duration-200 overflow-hidden group"
class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 hover:shadow-md dark:hover:shadow-lg dark:hover:shadow-gray-800/50 transition-all duration-200 overflow-hidden group"
>
<!-- Article Header -->
<div class="p-4 sm:p-6">
<div class="flex items-start justify-between mb-3">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">
{{ article.country }}
</span>
<time
@@ -31,14 +35,16 @@
</div>
<!-- Title -->
<h3 class="text-base sm:text-lg font-semibold text-gray-900 mb-3 line-clamp-2 group-hover:text-green-600 transition-colors">
<h3
class="text-base sm:text-lg font-semibold text-gray-900 dark:text-white mb-3 line-clamp-2 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors">
<a :href="article.url" target="_blank" rel="noopener noreferrer">
{{ article.title }}
</a>
</h3>
<!-- Summary -->
<p class="text-sm sm:text-base text-gray-700 line-clamp-3 mb-4 leading-relaxed">
<p
class="text-sm sm:text-base text-gray-700 dark:text-gray-300 line-clamp-3 mb-4 leading-relaxed">
{{ article.summary }}
</p>
@@ -49,8 +55,10 @@
>
View 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" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
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>
@@ -65,7 +73,8 @@
>
Read 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" />
<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"/>
</svg>
</a>
</div>
@@ -89,8 +98,8 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useNews } from '../stores/useNews';
import {ref} from 'vue';
import {useNews} from '../stores/useNews';
import ArticleModal from './ArticleModal.vue';
const news = useNews();

View File

@@ -1,12 +1,18 @@
import './assets/main.css';
import './style.css';
import {createPinia} from 'pinia';
import {createApp} from 'vue';
import App from "@/App.vue";
import {useDarkMode} from './stores/useDarkMode';
const app = createApp(App);
const pinia = createPinia();
app.use(createPinia());
app.use(pinia);
// Initialize dark mode before mounting the app
const darkModeStore = useDarkMode(pinia);
darkModeStore.initDarkMode();
app.mount('#app');

View File

@@ -0,0 +1,56 @@
import {defineStore} from 'pinia';
export const useDarkMode = defineStore('darkMode', {
state: () => ({
isDarkMode: localStorage.theme === "dark" ||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
}),
actions: {
// Explicitly choose light mode
setLightMode() {
localStorage.theme = "light";
this.isDarkMode = false;
this.applyDarkMode();
},
// Explicitly choose dark mode
setDarkMode() {
localStorage.theme = "dark";
this.isDarkMode = true;
this.applyDarkMode();
},
// Respect OS preference (remove explicit theme choice)
setSystemMode() {
localStorage.removeItem("theme");
this.isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
this.applyDarkMode();
},
// Toggle between light and dark (keeping system as fallback)
toggleDarkMode() {
if (this.isDarkMode) {
this.setLightMode();
} else {
this.setDarkMode();
}
},
applyDarkMode() {
document.documentElement.classList.toggle('dark', this.isDarkMode);
},
initDarkMode() {
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
document.documentElement.classList.toggle(
"dark",
localStorage.theme === "dark" ||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
);
// Update state to match what was applied
this.isDarkMode = localStorage.theme === "dark" ||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches);
}
}
});

View File

@@ -2,6 +2,8 @@
@import "./assets/main.css";
@custom-variant dark (&:where(.dark, .dark *));
/* Custom scrollbar for webkit browsers */
::-webkit-scrollbar {
width: 6px;