added dark mode support across components and improved accessibility styling
This commit is contained in:
@@ -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;
|
||||||
|
@@ -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);
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
@@ -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>
|
||||||
|
@@ -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 }}
|
||||||
|
@@ -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();
|
||||||
|
@@ -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();
|
||||||
|
@@ -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');
|
||||||
|
56
frontend/src/stores/useDarkMode.ts
Normal file
56
frontend/src/stores/useDarkMode.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@@ -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;
|
||||||
|
Reference in New Issue
Block a user