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> <template>
<div id="app" class="min-h-screen bg-gray-50"> <div id="app" class="min-h-screen bg-gray-50 dark:bg-gray-900">
<!-- Header --> <header
<header class="bg-white shadow-sm border-b sticky top-0 z-40"> 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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16"> <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 --> <div class="flex items-center gap-4">
<button <button
@click="showMobileMenu = !showMobileMenu" @click="toggleDarkMode"
class="lg:hidden p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100" 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"> <div class="flex flex-col space-y-1">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" /> <button
</svg> v-for="tab in tabs"
</button> :key="tab.id"
@click="activeTab = tab.id; showMobileMenu = false"
<!-- Desktop Navigation --> :class="[
<nav class="hidden lg:flex space-x-4"> 'px-3 py-2 rounded-md text-left text-base font-medium transition-colors cursor-pointer',
<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',
activeTab === tab.id activeTab === tab.id
? 'bg-green-100 text-green-700' ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300'
: 'text-gray-500 hover:text-gray-700 hover:bg-gray-100' : '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 }} {{ tab.label }}
</button> </button>
</nav> </div>
</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>
</div> </div>
</div> </div>
</div> </div>
</header> </header>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8"> <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"> <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"> <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<SyncButton /> <SyncButton/>
<NewsRefreshButton /> <NewsRefreshButton/>
<ModelStatus /> <ModelStatus/>
</div> </div>
<!-- Management Tools -->
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6"> <div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<FeedManager /> <FeedManager/>
<CronSlider /> <CronSlider/>
</div> </div>
</div> </div>
<!-- News Tab -->
<div v-if="activeTab === 'news'" class="space-y-6"> <div v-if="activeTab === 'news'" class="space-y-6">
<!-- News Filters --> <NewsFilters/>
<NewsFilters />
<!-- News List --> <NewsList/>
<NewsList />
</div> </div>
</main> </main>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'; import {ref, onMounted} from 'vue';
import { useNews } from './stores/useNews'; import {useNews} from './stores/useNews';
import {useDarkMode} from './stores/useDarkMode';
import SyncButton from './components/SyncButton.vue'; import SyncButton from './components/SyncButton.vue';
import NewsRefreshButton from './components/NewsRefreshButton.vue'; import NewsRefreshButton from './components/NewsRefreshButton.vue';
import ModelStatus from './components/ModelStatus.vue'; import ModelStatus from './components/ModelStatus.vue';
@@ -99,19 +109,27 @@ import NewsFilters from './components/NewsFilters.vue';
import NewsList from './components/NewsList.vue'; import NewsList from './components/NewsList.vue';
const news = useNews(); const news = useNews();
const darkMode = useDarkMode();
const activeTab = ref('news'); const activeTab = ref('news');
const showMobileMenu = ref(false); const showMobileMenu = ref(false);
const toggleDarkMode = () => {
darkMode.toggleDarkMode();
};
onMounted(() => {
darkMode.initDarkMode();
});
const tabs = [ const tabs = [
{ id: 'news', label: 'News' }, {id: 'news', label: 'News'},
{ id: 'management', label: 'Management' } {id: 'management', label: 'Management'}
]; ];
onMounted(async () => { onMounted(async () => {
await news.loadLastSync(); await news.loadLastSync();
}); });
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (showMobileMenu.value && !(e.target as Element).closest('header')) { if (showMobileMenu.value && !(e.target as Element).closest('header')) {
showMobileMenu.value = false; showMobileMenu.value = false;
@@ -120,7 +138,6 @@ document.addEventListener('click', (e) => {
</script> </script>
<style scoped> <style scoped>
/* Component-scoped styles to prevent global conflicts */
#app { #app {
display: block; display: block;
grid-template-columns: none; grid-template-columns: none;

View File

@@ -1,4 +1,3 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<Transition <Transition
@@ -12,24 +11,26 @@
> >
<div <div
v-if="isOpen" 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" @click="closeModal"
> >
<div <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 @click.stop
> >
<!-- Modal Header with glass effect --> <!-- 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 items-start justify-between">
<div class="flex-1"> <div class="flex-1">
<!-- Country badge with glass effect --> <!-- 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 }} {{ article?.country }}
</span> </span>
<!-- Title --> <!-- 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 }} {{ article?.title }}
</h3> </h3>
@@ -37,7 +38,7 @@
<time <time
v-if="article" v-if="article"
:datetime="new Date(article.published * 1000).toISOString()" :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) }} {{ formatDate(article.published) }}
</time> </time>
@@ -46,11 +47,12 @@
<!-- Close button with glass effect --> <!-- Close button with glass effect -->
<button <button
@click="closeModal" @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> <span class="sr-only">Close</span>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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> </svg>
</button> </button>
</div> </div>
@@ -60,23 +62,28 @@
<div class="px-6 py-4 overflow-y-auto custom-scrollbar"> <div class="px-6 py-4 overflow-y-auto custom-scrollbar">
<!-- Full Summary --> <!-- Full Summary -->
<div class="mb-6"> <div class="mb-6">
<h4 class="text-sm font-medium text-gray-700 mb-3 flex items-center"> <h4
<svg class="w-4 h-4 mr-2 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3 flex items-center">
<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 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> </svg>
Summary Summary
</h4> </h4>
<div class="prose prose-sm max-w-none"> <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> </div>
</div> </div>
<!-- Modal Footer with glass effect --> <!-- 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 <button
@click="closeModal" @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 Close
</button> </button>
@@ -90,7 +97,8 @@
> >
Read Full Article Read Full Article
<svg class="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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> </svg>
</a> </a>
</div> </div>
@@ -101,7 +109,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { watch, onUnmounted } from 'vue'; import {watch, onUnmounted} from 'vue';
interface Article { interface Article {
id: number; id: number;
@@ -188,12 +196,20 @@ function formatDate(timestamp: number): string {
border-radius: 3px; border-radius: 3px;
} }
:root.dark .custom-scrollbar::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1);
}
.custom-scrollbar::-webkit-scrollbar-thumb { .custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(16, 185, 129, 0.3); background: rgba(16, 185, 129, 0.3);
border-radius: 3px; border-radius: 3px;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
} }
:root.dark .custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(16, 185, 129, 0.4);
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover { .custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(16, 185, 129, 0.5); background: rgba(16, 185, 129, 0.5);
} }

View File

@@ -20,10 +20,10 @@ async function update() {
} }
</script> </script>
<template> <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> <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" <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> <p class="text-sm mt-2">{{ hours }} h <span v-if="saving" class="animate-pulse"></span></p>
</div> </div>
</template> </template>

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
<template> <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 --> <!-- Mobile Header with Toggle -->
<div class="lg:hidden"> <div class="lg:hidden">
<button <button
@click="showFilters = !showFilters" @click="showFilters = !showFilters"
class="w-full px-4 py-3 flex items-center justify-between bg-gray-50 hover:bg-gray-100 transition-colors" 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"> <div class="flex items-center space-x-2">
<span class="text-sm text-gray-500">{{ articleCount }} articles</span> <span class="text-sm text-gray-500">{{ articleCount }} articles</span>
<svg <svg
@@ -16,16 +17,17 @@
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" 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> </svg>
</div> </div>
</button> </button>
</div> </div>
<!-- Desktop Header --> <!-- 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"> <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> <span class="text-sm text-gray-500">{{ articleCount }} articles found</span>
</div> </div>
</div> </div>
@@ -38,9 +40,9 @@
<!-- Mobile/Tablet: Stacked Layout --> <!-- Mobile/Tablet: Stacked Layout -->
<div class="lg:hidden space-y-6"> <div class="lg:hidden space-y-6">
<!-- Country Filter Mobile --> <!-- Country Filter Mobile -->
<div class="bg-gray-50 rounded-lg p-4"> <div class="bg-gray-50 dark:bg-gray-800 dark:text-gray-200 rounded-lg p-4">
<h4 class="font-medium text-gray-900 mb-3">Countries</h4> <h4 class="font-medium text-gray-900 dark:text-white mb-3">Countries</h4>
<div class="space-y-3"> <div class="space-y-3 ">
<label class="flex items-center"> <label class="flex items-center">
<input <input
type="checkbox" type="checkbox"
@@ -48,7 +50,7 @@
@change="onAllCountriesChange" @change="onAllCountriesChange"
class="rounded border-gray-300 text-green-600 focus:ring-green-500" 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> </label>
<div v-if="!filters.allCountries" class="grid grid-cols-2 gap-2 pl-6"> <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"> <label v-for="country in availableCountries" :key="country" class="flex items-center">
@@ -65,8 +67,8 @@
</div> </div>
<!-- Date Filter Mobile --> <!-- Date Filter Mobile -->
<div class="bg-gray-50 rounded-lg p-4"> <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-4">
<h4 class="font-medium text-gray-900 mb-3">Date Range</h4> <h4 class="font-medium text-gray-900 dark:text-white mb-3">Date Range</h4>
<div class="space-y-3"> <div class="space-y-3">
<label class="flex items-center"> <label class="flex items-center">
<input <input
@@ -75,16 +77,16 @@
@change="onAllDatesChange" @change="onAllDatesChange"
class="rounded border-gray-300 text-green-600 focus:ring-green-500" 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> </label>
<div v-if="!filters.allDates" class="space-y-3 pl-6"> <div v-if="!filters.allDates" class="space-y-3 pl-6">
<div class="grid grid-cols-2 gap-3"> <div class="grid grid-cols-2 gap-3">
<div> <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 <input
type="date" type="date"
v-model="filters.fromDate" 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>
<div> <div>
@@ -92,20 +94,20 @@
<input <input
type="date" type="date"
v-model="filters.toDate" 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>
</div> </div>
<!-- Quick Presets Mobile --> <!-- Quick Presets Mobile -->
<div> <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"> <div class="grid grid-cols-2 gap-2">
<button <button
v-for="preset in datePresets" v-for="preset in datePresets"
:key="preset.label" :key="preset.label"
@click="applyDatePreset(preset)" @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 }} {{ preset.label }}
</button> </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" 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"> <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"> <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none"
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> viewBox="0 0 24 24">
<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> <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> </svg>
Loading... Loading...
</span> </span>
@@ -145,7 +150,8 @@
<div class="hidden lg:grid lg:grid-cols-4 gap-6"> <div class="hidden lg:grid lg:grid-cols-4 gap-6">
<!-- Country Filter Desktop --> <!-- Country Filter Desktop -->
<div> <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"> <div class="space-y-2">
<label class="flex items-center"> <label class="flex items-center">
<input <input
@@ -154,7 +160,7 @@
@change="onAllCountriesChange" @change="onAllCountriesChange"
class="rounded border-gray-300 text-green-600 focus:ring-green-500" 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> </label>
<div v-if="!filters.allCountries" class="space-y-1 max-h-40 overflow-y-auto"> <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"> <label v-for="country in availableCountries" :key="country" class="flex items-center">
@@ -164,7 +170,7 @@
v-model="filters.selectedCountries" v-model="filters.selectedCountries"
class="rounded border-gray-300 text-green-600 focus:ring-green-500" 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> </label>
</div> </div>
</div> </div>
@@ -172,7 +178,8 @@
<!-- Date Range Filter Desktop --> <!-- Date Range Filter Desktop -->
<div> <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"> <div class="space-y-2">
<label class="flex items-center"> <label class="flex items-center">
<input <input
@@ -181,23 +188,23 @@
@change="onAllDatesChange" @change="onAllDatesChange"
class="rounded border-gray-300 text-green-600 focus:ring-green-500" 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> </label>
<div v-if="!filters.allDates" class="space-y-2"> <div v-if="!filters.allDates" class="space-y-2">
<div> <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 <input
type="date" type="date"
v-model="filters.fromDate" 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>
<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 <input
type="date" type="date"
v-model="filters.toDate" 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>
</div> </div>
@@ -206,13 +213,14 @@
<!-- Quick Date Presets Desktop --> <!-- Quick Date Presets Desktop -->
<div v-if="!filters.allDates"> <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"> <div class="space-y-1">
<button <button
v-for="preset in datePresets" v-for="preset in datePresets"
:key="preset.label" :key="preset.label"
@click="applyDatePreset(preset)" @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 }} {{ preset.label }}
</button> </button>
@@ -224,12 +232,15 @@
<button <button
@click="applyFilters" @click="applyFilters"
:disabled="isLoading" :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"> <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"> <svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" fill="none"
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> viewBox="0 0 24 24">
<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> <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> </svg>
Loading... Loading...
</span> </span>
@@ -249,9 +260,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue'; import {ref, onMounted, computed} from 'vue';
import { useNews } from '../stores/useNews'; import {useNews} from '../stores/useNews';
import { useFeeds } from '../stores/useFeeds'; import {useFeeds} from '../stores/useFeeds';
const news = useNews(); const news = useNews();
const feeds = useFeeds(); const feeds = useFeeds();

View File

@@ -2,11 +2,14 @@
<div> <div>
<!-- Empty State --> <!-- Empty State -->
<div v-if="news.articles.length === 0" class="text-center py-12"> <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"> <svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none"
<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" /> 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> </svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No articles found</h3> <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">Try adjusting your filters to see more results.</p> <p class="mt-2 text-gray-500 dark:text-gray-400">Try adjusting your filters to see more
results.</p>
</div> </div>
<!-- Articles Grid --> <!-- Articles Grid -->
@@ -14,12 +17,13 @@
<article <article
v-for="article in news.articles" v-for="article in news.articles"
:key="article.id" :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 --> <!-- Article Header -->
<div class="p-4 sm:p-6"> <div class="p-4 sm:p-6">
<div class="flex items-start justify-between mb-3"> <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 }} {{ article.country }}
</span> </span>
<time <time
@@ -31,14 +35,16 @@
</div> </div>
<!-- Title --> <!-- 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"> <a :href="article.url" target="_blank" rel="noopener noreferrer">
{{ article.title }} {{ article.title }}
</a> </a>
</h3> </h3>
<!-- Summary --> <!-- 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 }} {{ article.summary }}
</p> </p>
@@ -49,8 +55,10 @@
> >
View full summary View full summary
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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"
<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" /> 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> </svg>
</button> </button>
</div> </div>
@@ -65,7 +73,8 @@
> >
Read full article Read full article
<svg class="ml-1 w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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> </svg>
</a> </a>
</div> </div>
@@ -89,8 +98,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import {ref} from 'vue';
import { useNews } from '../stores/useNews'; import {useNews} from '../stores/useNews';
import ArticleModal from './ArticleModal.vue'; import ArticleModal from './ArticleModal.vue';
const news = useNews(); const news = useNews();

View File

@@ -1,12 +1,18 @@
import './assets/main.css'; import './assets/main.css';
import './style.css';
import {createPinia} from 'pinia'; import {createPinia} from 'pinia';
import {createApp} from 'vue'; import {createApp} from 'vue';
import App from "@/App.vue"; import App from "@/App.vue";
import {useDarkMode} from './stores/useDarkMode';
const app = createApp(App); 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'); 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"; @import "./assets/main.css";
@custom-variant dark (&:where(.dark, .dark *));
/* Custom scrollbar for webkit browsers */ /* Custom scrollbar for webkit browsers */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 6px; width: 6px;