Refactor Svelte components for improved formatting and consistency
- Cleaned up formatting in AddFeedModal.svelte, ArticleCard.svelte, Navbar.svelte, Sidebar.svelte, and Toasts.svelte by adding missing commas and adjusting indentation. - Enhanced readability by ensuring consistent use of line breaks and spacing across components. - Updated event handlers and bindings for better clarity and maintainability.
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
lastFetched: Date.now(),
|
||||
fetchInterval: 30,
|
||||
enabled: true,
|
||||
consecutiveErrors: 0
|
||||
consecutiveErrors: 0,
|
||||
});
|
||||
await db.saveArticles(articles);
|
||||
await newsStore.refresh();
|
||||
@@ -40,26 +40,37 @@
|
||||
</script>
|
||||
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center p-4">
|
||||
<button
|
||||
class="absolute inset-0 bg-black/60 backdrop-blur-sm w-full h-full border-none cursor-default"
|
||||
<button
|
||||
class="absolute inset-0 bg-black/60 backdrop-blur-sm w-full h-full border-none cursor-default"
|
||||
onclick={() => onOpenChange(false)}
|
||||
aria-label="Close modal"
|
||||
></button>
|
||||
|
||||
<div class="bg-bg-primary border border-border-color rounded-2xl shadow-2xl w-full max-w-md relative overflow-hidden z-10">
|
||||
|
||||
<div
|
||||
class="bg-bg-primary border border-border-color rounded-2xl shadow-2xl w-full max-w-md relative overflow-hidden z-10"
|
||||
>
|
||||
<div class="p-6 border-b border-border-color flex justify-between items-center">
|
||||
<h2 class="text-xl font-bold">Add RSS Feed</h2>
|
||||
<button class="text-text-secondary hover:text-text-primary" onclick={() => onOpenChange(false)}>
|
||||
<button
|
||||
class="text-text-secondary hover:text-text-primary"
|
||||
onclick={() => onOpenChange(false)}
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form class="p-6 space-y-4" onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||
<form
|
||||
class="p-6 space-y-4"
|
||||
onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<label for="url" class="text-sm font-medium text-text-secondary">Feed URL</label>
|
||||
<input
|
||||
<input
|
||||
id="url"
|
||||
type="url"
|
||||
type="url"
|
||||
bind:value={feedUrl}
|
||||
placeholder="https://example.com/rss.xml"
|
||||
class="w-full bg-bg-secondary border border-border-color rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-accent-blue/20 transition-all"
|
||||
@@ -69,7 +80,7 @@
|
||||
|
||||
<div class="space-y-2">
|
||||
<label for="category" class="text-sm font-medium text-text-secondary">Category</label>
|
||||
<select
|
||||
<select
|
||||
id="category"
|
||||
bind:value={categoryId}
|
||||
class="w-full bg-bg-secondary border border-border-color rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-accent-blue/20 transition-all text-sm"
|
||||
@@ -84,8 +95,8 @@
|
||||
<p class="text-red-500 text-sm">{error}</p>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full btn-primary flex items-center justify-center gap-2 py-3"
|
||||
disabled={loading}
|
||||
>
|
||||
@@ -99,4 +110,3 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
|
||||
|
||||
if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
|
||||
if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
||||
}
|
||||
|
||||
function getSource(feedId: string) {
|
||||
const feed = newsStore.feeds.find(f => f.id === feedId);
|
||||
const feed = newsStore.feeds.find((f) => f.id === feedId);
|
||||
return feed?.title || new URL(feedId).hostname;
|
||||
}
|
||||
|
||||
@@ -27,12 +27,12 @@
|
||||
e.stopPropagation();
|
||||
const encodedUrl = btoa(article.link);
|
||||
const shareUrl = `${window.location.origin}/share?url=${encodedUrl}`;
|
||||
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
copied = true;
|
||||
toast.success('Share link copied to clipboard');
|
||||
setTimeout(() => copied = false, 2000);
|
||||
setTimeout(() => (copied = false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy share link:', err);
|
||||
toast.error('Failed to copy share link');
|
||||
@@ -71,26 +71,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="card group relative flex flex-col sm:flex-row gap-4 transition-all hover:shadow-md {article.read ? 'opacity-60' : ''} {newsStore.readingArticle?.url === article.link ? 'ring-2 ring-accent-blue shadow-lg bg-accent-blue/5' : ''}">
|
||||
<article
|
||||
class="card group relative flex flex-col sm:flex-row gap-4 transition-all hover:shadow-md {article.read
|
||||
? 'opacity-60'
|
||||
: ''} {newsStore.readingArticle?.url === article.link
|
||||
? 'ring-2 ring-accent-blue shadow-lg bg-accent-blue/5'
|
||||
: ''}"
|
||||
>
|
||||
{#if newsStore.isSelectMode}
|
||||
<div class="flex items-center pl-4 z-20">
|
||||
<div class="relative w-5 h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
class="peer appearance-none w-5 h-5 rounded border-2 border-border-color checked:bg-accent-blue checked:border-accent-blue transition-all cursor-pointer"
|
||||
checked={newsStore.selectedArticleIds.has(article.id)}
|
||||
onchange={handleToggleSelect}
|
||||
/>
|
||||
<Check
|
||||
size={14}
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white opacity-0 peer-checked:opacity-100 pointer-events-none transition-opacity"
|
||||
<Check
|
||||
size={14}
|
||||
class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white opacity-0 peer-checked:opacity-100 pointer-events-none transition-opacity"
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
class="absolute inset-0 w-full h-full text-left cursor-pointer z-0"
|
||||
<button
|
||||
class="absolute inset-0 w-full h-full text-left cursor-pointer z-0"
|
||||
onclick={() => {
|
||||
if (newsStore.isSelectMode) {
|
||||
const isSelected = newsStore.selectedArticleIds.has(article.id);
|
||||
@@ -106,21 +112,25 @@
|
||||
|
||||
<div class="flex-1 min-w-0 p-4 relative z-10 pointer-events-none">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs font-semibold text-accent-blue hover:underline pointer-events-auto">{getSource(article.feedId)}</span>
|
||||
<span class="text-xs font-semibold text-accent-blue hover:underline pointer-events-auto"
|
||||
>{getSource(article.feedId)}</span
|
||||
>
|
||||
<span class="text-text-secondary text-xs">•</span>
|
||||
<span class="text-text-secondary text-xs">{formatDate(article.pubDate)}</span>
|
||||
</div>
|
||||
|
||||
|
||||
<h3 class="text-lg font-bold leading-snug mb-2 group-hover:text-accent-blue transition-colors">
|
||||
{article.title}
|
||||
</h3>
|
||||
|
||||
|
||||
<p class="text-text-secondary text-sm line-clamp-2 mb-4">
|
||||
{article.description}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-4 text-text-secondary opacity-0 group-hover:opacity-100 transition-opacity pointer-events-auto">
|
||||
<button
|
||||
|
||||
<div
|
||||
class="flex items-center gap-4 text-text-secondary opacity-0 group-hover:opacity-100 transition-opacity pointer-events-auto"
|
||||
>
|
||||
<button
|
||||
class="flex items-center gap-1.5 px-3 py-1 rounded-full hover:bg-bg-secondary transition-colors text-xs font-semibold hover:text-accent-blue"
|
||||
onclick={fetchFullText}
|
||||
disabled={loadingFullText}
|
||||
@@ -132,15 +142,17 @@
|
||||
{/if}
|
||||
Read
|
||||
</button>
|
||||
<button
|
||||
class="p-1.5 rounded-full hover:bg-bg-secondary transition-colors {article.saved ? 'text-accent-blue' : 'hover:text-text-primary'}"
|
||||
<button
|
||||
class="p-1.5 rounded-full hover:bg-bg-secondary transition-colors {article.saved
|
||||
? 'text-accent-blue'
|
||||
: 'hover:text-text-primary'}"
|
||||
title={article.saved ? 'Remove from saved' : 'Save for later'}
|
||||
onclick={toggleSave}
|
||||
>
|
||||
<Bookmark size={18} fill={article.saved ? 'currentColor' : 'none'} />
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-text-primary p-1.5 rounded-full hover:bg-bg-secondary transition-colors flex items-center gap-1"
|
||||
<button
|
||||
class="hover:text-text-primary p-1.5 rounded-full hover:bg-bg-secondary transition-colors flex items-center gap-1"
|
||||
title="Copy share link"
|
||||
onclick={shareArticle}
|
||||
>
|
||||
@@ -150,10 +162,13 @@
|
||||
<Share2 size={18} />
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
class="hover:text-text-primary p-1.5 rounded-full hover:bg-bg-secondary transition-colors"
|
||||
<button
|
||||
class="hover:text-text-primary p-1.5 rounded-full hover:bg-bg-secondary transition-colors"
|
||||
title="Open in new tab"
|
||||
onclick={(e) => { e.stopPropagation(); window.open(article.link, '_blank'); }}
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(article.link, '_blank');
|
||||
}}
|
||||
>
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
@@ -161,8 +176,14 @@
|
||||
</div>
|
||||
|
||||
{#if article.imageUrl}
|
||||
<div class="w-full sm:w-32 h-48 sm:h-32 flex-shrink-0 sm:m-4 rounded-xl overflow-hidden bg-bg-secondary border border-border-color relative z-10 pointer-events-none">
|
||||
<img src={article.imageUrl} alt="" class="w-full h-full object-cover transition-transform group-hover:scale-105" />
|
||||
<div
|
||||
class="w-full sm:w-32 h-48 sm:h-32 flex-shrink-0 sm:m-4 rounded-xl overflow-hidden bg-bg-secondary border border-border-color relative z-10 pointer-events-none"
|
||||
>
|
||||
<img
|
||||
src={article.imageUrl}
|
||||
alt=""
|
||||
class="w-full h-full object-cover transition-transform group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</article>
|
||||
|
||||
@@ -5,17 +5,35 @@
|
||||
let { onAddFeed } = $props();
|
||||
</script>
|
||||
|
||||
<header class="sticky top-0 z-50 bg-bg-primary/80 backdrop-blur-md border-b border-border-color px-4 py-2 flex justify-between items-center h-[calc(64px+env(safe-area-inset-top,0px))] pt-safe">
|
||||
<header
|
||||
class="sticky top-0 z-50 bg-bg-primary/80 backdrop-blur-md border-b border-border-color px-4 py-2 flex justify-between items-center h-[calc(64px+env(safe-area-inset-top,0px))] pt-safe"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="md:hidden p-2 hover:bg-bg-secondary rounded-full text-text-secondary"
|
||||
<button
|
||||
class="md:hidden p-2 hover:bg-bg-secondary rounded-full text-text-secondary"
|
||||
aria-label="Menu"
|
||||
onclick={() => newsStore.showSidebar = !newsStore.showSidebar}
|
||||
onclick={() => (newsStore.showSidebar = !newsStore.showSidebar)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line
|
||||
x1="3"
|
||||
y1="18"
|
||||
x2="21"
|
||||
y2="18"
|
||||
></line></svg
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 cursor-pointer focus:outline-none focus:ring-2 focus:ring-accent-blue/20 rounded-lg px-1"
|
||||
<button
|
||||
class="flex items-center gap-1.5 cursor-pointer focus:outline-none focus:ring-2 focus:ring-accent-blue/20 rounded-lg px-1"
|
||||
onclick={() => {
|
||||
newsStore.selectFeed(null);
|
||||
newsStore.currentView = 'all';
|
||||
@@ -24,7 +42,22 @@
|
||||
aria-label="Web News Home"
|
||||
>
|
||||
<div class="w-8 h-8 bg-accent-blue rounded-lg flex items-center justify-center text-white">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle
|
||||
cx="5"
|
||||
cy="19"
|
||||
r="1"
|
||||
></circle></svg
|
||||
>
|
||||
</div>
|
||||
<h1 class="text-xl font-bold tracking-tight hidden sm:block">Web News</h1>
|
||||
</button>
|
||||
@@ -33,9 +66,9 @@
|
||||
<div class="flex-1 max-w-2xl mx-4 hidden sm:block">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for topics, locations & sources"
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for topics, locations & sources"
|
||||
class="w-full bg-bg-secondary border-none rounded-xl py-2.5 pl-10 pr-4 focus:ring-2 focus:ring-accent-blue/20 outline-none transition-all"
|
||||
bind:value={newsStore.searchQuery}
|
||||
oninput={() => newsStore.loadArticles()}
|
||||
@@ -45,32 +78,42 @@
|
||||
|
||||
<div class="flex items-center gap-1 sm:gap-2">
|
||||
{#if newsStore.ping !== null && !newsStore.isWails && !newsStore.isCapacitor}
|
||||
<div class="hidden md:flex items-center gap-1.5 px-3 py-1.5 bg-bg-secondary rounded-full border border-border-color">
|
||||
<div class="w-1.5 h-1.5 rounded-full {newsStore.ping < 200 ? 'bg-green-500' : newsStore.ping < 500 ? 'bg-yellow-500' : 'bg-red-500'}"></div>
|
||||
<div
|
||||
class="hidden md:flex items-center gap-1.5 px-3 py-1.5 bg-bg-secondary rounded-full border border-border-color"
|
||||
>
|
||||
<div
|
||||
class="w-1.5 h-1.5 rounded-full {newsStore.ping < 200
|
||||
? 'bg-green-500'
|
||||
: newsStore.ping < 500
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-red-500'}"
|
||||
></div>
|
||||
<span class="text-[10px] font-medium text-text-secondary">{newsStore.ping}ms</span>
|
||||
</div>
|
||||
{:else if !newsStore.isOnline}
|
||||
<div class="hidden md:flex items-center gap-1.5 px-3 py-1.5 bg-red-500/10 rounded-full border border-red-500/20">
|
||||
<div
|
||||
class="hidden md:flex items-center gap-1.5 px-3 py-1.5 bg-red-500/10 rounded-full border border-red-500/20"
|
||||
>
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></div>
|
||||
<span class="text-[10px] font-medium text-red-500">Offline</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
<button
|
||||
class="p-2 hover:bg-bg-secondary rounded-full text-text-secondary transition-colors"
|
||||
onclick={() => newsStore.refresh()}
|
||||
title="Refresh feeds"
|
||||
>
|
||||
<RefreshCw size={20} class={newsStore.loading ? 'animate-spin text-accent-blue' : ''} />
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
class="p-2 hover:bg-bg-secondary rounded-full text-text-secondary transition-colors"
|
||||
onclick={onAddFeed}
|
||||
title="Add RSS Feed"
|
||||
>
|
||||
<Plus size={24} />
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
class="p-2 hover:bg-bg-secondary rounded-full text-text-secondary transition-colors"
|
||||
onclick={() => newsStore.toggleTheme()}
|
||||
title="Toggle theme"
|
||||
@@ -83,4 +126,3 @@
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -2,17 +2,35 @@
|
||||
import { newsStore } from '$lib/store.svelte';
|
||||
import { db } from '$lib/db';
|
||||
import { exportToOPML, parseOPML } from '$lib/opml';
|
||||
import { Home, Star, Bookmark, Hash, Settings as SettingsIcon, ChevronRight, ChevronDown, AlertCircle, Edit2, GripVertical, Plus, Trash2, Save, X, Download, Upload, GitBranch } from 'lucide-svelte';
|
||||
import {
|
||||
Home,
|
||||
Star,
|
||||
Bookmark,
|
||||
Hash,
|
||||
Settings as SettingsIcon,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
AlertCircle,
|
||||
Edit2,
|
||||
GripVertical,
|
||||
Plus,
|
||||
Trash2,
|
||||
Save,
|
||||
X,
|
||||
Download,
|
||||
Upload,
|
||||
GitBranch,
|
||||
} from 'lucide-svelte';
|
||||
import { toast } from '$lib/toast.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let { onOpenSettings } = $props();
|
||||
|
||||
|
||||
let expandedCategories = $state<Record<string, boolean>>({});
|
||||
|
||||
$effect(() => {
|
||||
// Expand new categories by default
|
||||
newsStore.categories.forEach(cat => {
|
||||
newsStore.categories.forEach((cat) => {
|
||||
if (expandedCategories[cat.id] === undefined) {
|
||||
expandedCategories[cat.id] = true;
|
||||
}
|
||||
@@ -38,7 +56,7 @@
|
||||
|
||||
function getFeedsForCategory(categoryId: string) {
|
||||
return newsStore.feeds
|
||||
.filter(f => f.categoryId === categoryId)
|
||||
.filter((f) => f.categoryId === categoryId)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
@@ -66,34 +84,34 @@
|
||||
|
||||
if (draggedCategoryId && targetType === 'category') {
|
||||
const cats = [...newsStore.categories].sort((a, b) => a.order - b.order);
|
||||
const fromIndex = cats.findIndex(c => c.id === draggedCategoryId);
|
||||
const toIndex = cats.findIndex(c => c.id === targetId);
|
||||
|
||||
const fromIndex = cats.findIndex((c) => c.id === draggedCategoryId);
|
||||
const toIndex = cats.findIndex((c) => c.id === targetId);
|
||||
|
||||
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||
const [moved] = cats.splice(fromIndex, 1);
|
||||
cats.splice(toIndex, 0, moved);
|
||||
await newsStore.reorderCategories(cats.map(c => c.id));
|
||||
await newsStore.reorderCategories(cats.map((c) => c.id));
|
||||
}
|
||||
} else if (draggedFeedId && targetType === 'feed') {
|
||||
const sourceFeed = newsStore.feeds.find(f => f.id === draggedFeedId);
|
||||
const targetFeed = newsStore.feeds.find(f => f.id === targetId);
|
||||
|
||||
const sourceFeed = newsStore.feeds.find((f) => f.id === draggedFeedId);
|
||||
const targetFeed = newsStore.feeds.find((f) => f.id === targetId);
|
||||
|
||||
if (sourceFeed && targetFeed && sourceFeed.categoryId === targetFeed.categoryId) {
|
||||
const catFeeds = newsStore.feeds
|
||||
.filter(f => f.categoryId === sourceFeed.categoryId)
|
||||
.filter((f) => f.categoryId === sourceFeed.categoryId)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
|
||||
const fromIndex = catFeeds.findIndex(f => f.id === draggedFeedId);
|
||||
const toIndex = catFeeds.findIndex(f => f.id === targetId);
|
||||
|
||||
|
||||
const fromIndex = catFeeds.findIndex((f) => f.id === draggedFeedId);
|
||||
const toIndex = catFeeds.findIndex((f) => f.id === targetId);
|
||||
|
||||
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
||||
const [moved] = catFeeds.splice(fromIndex, 1);
|
||||
catFeeds.splice(toIndex, 0, moved);
|
||||
await newsStore.reorderFeeds(catFeeds.map(f => f.id));
|
||||
await newsStore.reorderFeeds(catFeeds.map((f) => f.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
draggedCategoryId = null;
|
||||
draggedFeedId = null;
|
||||
}
|
||||
@@ -111,7 +129,7 @@
|
||||
|
||||
async function saveCategory() {
|
||||
if (!editingCategoryId) return;
|
||||
const cat = newsStore.categories.find(c => c.id === editingCategoryId);
|
||||
const cat = newsStore.categories.find((c) => c.id === editingCategoryId);
|
||||
if (cat) {
|
||||
await newsStore.updateCategory({ ...cat, name: editingCategoryName });
|
||||
}
|
||||
@@ -126,13 +144,16 @@
|
||||
|
||||
async function saveFeed() {
|
||||
if (!editingFeedId) return;
|
||||
const feed = newsStore.feeds.find(f => f.id === editingFeedId);
|
||||
const feed = newsStore.feeds.find((f) => f.id === editingFeedId);
|
||||
if (feed) {
|
||||
await newsStore.updateFeed({
|
||||
...feed,
|
||||
title: editingFeedTitle,
|
||||
id: editingFeedUrl
|
||||
}, editingFeedId);
|
||||
await newsStore.updateFeed(
|
||||
{
|
||||
...feed,
|
||||
title: editingFeedTitle,
|
||||
id: editingFeedUrl,
|
||||
},
|
||||
editingFeedId
|
||||
);
|
||||
}
|
||||
editingFeedId = null;
|
||||
}
|
||||
@@ -145,14 +166,14 @@
|
||||
try {
|
||||
const text = await file.text();
|
||||
const { feeds, categories } = parseOPML(text);
|
||||
|
||||
|
||||
if (categories.length > 0) {
|
||||
await db.saveCategories(categories as any);
|
||||
}
|
||||
if (feeds.length > 0) {
|
||||
await db.saveFeeds(feeds as any);
|
||||
}
|
||||
|
||||
|
||||
toast.success(`Imported ${feeds.length} feeds`);
|
||||
await newsStore.init();
|
||||
} catch (err) {
|
||||
@@ -183,234 +204,315 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="w-64 flex-shrink-0 bg-bg-primary border-r border-border-color z-40 transition-transform duration-300 {newsStore.showSidebar ? 'translate-x-0' : '-translate-x-full md:translate-x-0'} fixed md:static top-0 left-0 h-full"
|
||||
<aside
|
||||
class="w-64 flex-shrink-0 bg-bg-primary border-r border-border-color z-40 transition-transform duration-300 {newsStore.showSidebar
|
||||
? 'translate-x-0'
|
||||
: '-translate-x-full md:translate-x-0'} fixed md:static top-0 left-0 h-full"
|
||||
>
|
||||
<div class="flex flex-col gap-6 py-6 overflow-y-auto h-full pt-[calc(64px+env(safe-area-inset-top,0px))] md:pt-0 scroll-container">
|
||||
<nav class="flex flex-col gap-1 px-2">
|
||||
<button
|
||||
class="flex items-center gap-3 px-4 py-2.5 rounded-xl transition-colors {newsStore.currentView === 'all' && newsStore.selectedFeedId === null ? 'bg-accent-blue/10 text-accent-blue font-semibold' : 'text-text-primary hover:bg-bg-secondary'}"
|
||||
onclick={() => { newsStore.selectView('all'); newsStore.readingArticle = null; newsStore.showSidebar = false; }}
|
||||
>
|
||||
<Home size={20} />
|
||||
<span>Top stories</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-3 px-4 py-2.5 rounded-xl transition-colors {newsStore.currentView === 'following' ? 'bg-accent-blue/10 text-accent-blue font-semibold' : 'text-text-primary hover:bg-bg-secondary'}"
|
||||
onclick={() => { newsStore.selectView('following'); newsStore.readingArticle = null; newsStore.showSidebar = false; }}
|
||||
>
|
||||
<Star size={20} />
|
||||
<span>Following</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-3 px-4 py-2.5 rounded-xl transition-colors {newsStore.currentView === 'saved' ? 'bg-accent-blue/10 text-accent-blue font-semibold' : 'text-text-primary hover:bg-bg-secondary'}"
|
||||
onclick={() => { newsStore.selectView('saved'); newsStore.readingArticle = null; newsStore.showSidebar = false; }}
|
||||
>
|
||||
<Bookmark size={20} />
|
||||
<span>Saved stories</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="px-6 py-2">
|
||||
<div class="h-px bg-border-color w-full"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 px-2 overflow-y-auto">
|
||||
<div class="flex items-center justify-between px-4 mb-4">
|
||||
<h3 class="text-xs font-semibold text-text-secondary uppercase tracking-wider">Subscriptions</h3>
|
||||
<button
|
||||
class="text-text-secondary hover:text-accent-blue transition-colors p-1 {isManageMode ? 'text-accent-blue' : ''}"
|
||||
onclick={() => isManageMode = !isManageMode}
|
||||
title="Manage feeds"
|
||||
<div
|
||||
class="flex flex-col gap-6 py-6 overflow-y-auto h-full pt-[calc(64px+env(safe-area-inset-top,0px))] md:pt-0 scroll-container"
|
||||
>
|
||||
<nav class="flex flex-col gap-1 px-2">
|
||||
<button
|
||||
class="flex items-center gap-3 px-4 py-2.5 rounded-xl transition-colors {newsStore.currentView ===
|
||||
'all' && newsStore.selectedFeedId === null
|
||||
? 'bg-accent-blue/10 text-accent-blue font-semibold'
|
||||
: 'text-text-primary hover:bg-bg-secondary'}"
|
||||
onclick={() => {
|
||||
newsStore.selectView('all');
|
||||
newsStore.readingArticle = null;
|
||||
newsStore.showSidebar = false;
|
||||
}}
|
||||
>
|
||||
{#if isManageMode}
|
||||
<X size={14} />
|
||||
{:else}
|
||||
<Edit2 size={14} />
|
||||
{/if}
|
||||
<Home size={20} />
|
||||
<span>Top stories</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-3 px-4 py-2.5 rounded-xl transition-colors {newsStore.currentView ===
|
||||
'following'
|
||||
? 'bg-accent-blue/10 text-accent-blue font-semibold'
|
||||
: 'text-text-primary hover:bg-bg-secondary'}"
|
||||
onclick={() => {
|
||||
newsStore.selectView('following');
|
||||
newsStore.readingArticle = null;
|
||||
newsStore.showSidebar = false;
|
||||
}}
|
||||
>
|
||||
<Star size={20} />
|
||||
<span>Following</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-3 px-4 py-2.5 rounded-xl transition-colors {newsStore.currentView ===
|
||||
'saved'
|
||||
? 'bg-accent-blue/10 text-accent-blue font-semibold'
|
||||
: 'text-text-primary hover:bg-bg-secondary'}"
|
||||
onclick={() => {
|
||||
newsStore.selectView('saved');
|
||||
newsStore.readingArticle = null;
|
||||
newsStore.showSidebar = false;
|
||||
}}
|
||||
>
|
||||
<Bookmark size={20} />
|
||||
<span>Saved stories</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="px-6 py-2">
|
||||
<div class="h-px bg-border-color w-full"></div>
|
||||
</div>
|
||||
|
||||
{#if isManageMode}
|
||||
<div class="px-4 mb-4 space-y-3">
|
||||
<div class="flex gap-1">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newCategoryName}
|
||||
placeholder="Add category..."
|
||||
class="flex-1 bg-bg-secondary border border-border-color rounded-lg px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-accent-blue/30"
|
||||
onkeydown={(e) => e.key === 'Enter' && addCategory()}
|
||||
/>
|
||||
<button class="p-1 bg-accent-blue text-white rounded-lg hover:bg-accent-blue/80 transition-colors" onclick={addCategory}>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<label class="flex-1 flex items-center justify-center gap-2 py-1.5 bg-bg-secondary border border-border-color rounded-lg text-[10px] font-medium text-text-secondary hover:bg-bg-primary transition-colors cursor-pointer">
|
||||
<Upload size={12} />
|
||||
Import
|
||||
<input type="file" accept=".opml,.xml" class="hidden" onchange={handleImport} />
|
||||
</label>
|
||||
<button
|
||||
class="flex-1 flex items-center justify-center gap-2 py-1.5 bg-bg-secondary border border-border-color rounded-lg text-[10px] font-medium text-text-secondary hover:bg-bg-primary transition-colors"
|
||||
onclick={handleExport}
|
||||
>
|
||||
<Download size={12} />
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 px-2 overflow-y-auto">
|
||||
<div class="flex items-center justify-between px-4 mb-4">
|
||||
<h3 class="text-xs font-semibold text-text-secondary uppercase tracking-wider">
|
||||
Subscriptions
|
||||
</h3>
|
||||
<button
|
||||
class="text-text-secondary hover:text-accent-blue transition-colors p-1 {isManageMode
|
||||
? 'text-accent-blue'
|
||||
: ''}"
|
||||
onclick={() => (isManageMode = !isManageMode)}
|
||||
title="Manage feeds"
|
||||
>
|
||||
{#if isManageMode}
|
||||
<X size={14} />
|
||||
{:else}
|
||||
<Edit2 size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-1" role="list">
|
||||
{#each [...newsStore.categories].sort((a, b) => a.order - b.order) as cat (cat.id)}
|
||||
{@const catFeeds = getFeedsForCategory(cat.id)}
|
||||
{#if catFeeds.length > 0 || isManageMode}
|
||||
<div
|
||||
class="space-y-1 rounded-xl transition-all {dragOverId === cat.id ? 'ring-2 ring-accent-blue/50 bg-accent-blue/5' : ''}"
|
||||
draggable={isManageMode}
|
||||
role="listitem"
|
||||
ondragstart={(e) => handleDragStart(e, 'category', cat.id)}
|
||||
ondragover={(e) => handleDragOver(e, cat.id)}
|
||||
ondrop={(e) => handleDrop(e, 'category', cat.id)}
|
||||
>
|
||||
<div class="flex items-center group">
|
||||
{#if isManageMode}
|
||||
<div class="text-text-secondary/30 cursor-grab active:cursor-grabbing pl-2">
|
||||
<GripVertical size={14} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editingCategoryId === cat.id}
|
||||
<div class="flex-1 flex items-center gap-1 px-2 py-1">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editingCategoryName}
|
||||
class="flex-1 bg-bg-primary border border-border-color rounded px-2 py-0.5 text-sm outline-none"
|
||||
onkeydown={(e) => e.key === 'Enter' && saveCategory()}
|
||||
/>
|
||||
<button class="text-green-500" onclick={saveCategory}><Save size={14} /></button>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="flex-1 flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium text-text-secondary hover:bg-bg-secondary transition-colors text-left min-w-0"
|
||||
onclick={() => toggleCategory(cat.id)}
|
||||
title={cat.name}
|
||||
>
|
||||
{#if expandedCategories[cat.id]}
|
||||
<ChevronDown size={16} />
|
||||
{:else}
|
||||
<ChevronRight size={16} />
|
||||
{/if}
|
||||
<span class="truncate">{cat.name}</span>
|
||||
<span class="ml-auto text-[10px] bg-bg-secondary px-1.5 py-0.5 rounded-full">{catFeeds.length}</span>
|
||||
</button>
|
||||
|
||||
{#if isManageMode}
|
||||
<div class="px-4 mb-4 space-y-3">
|
||||
<div class="flex gap-1">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={newCategoryName}
|
||||
placeholder="Add category..."
|
||||
class="flex-1 bg-bg-secondary border border-border-color rounded-lg px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-accent-blue/30"
|
||||
onkeydown={(e) => e.key === 'Enter' && addCategory()}
|
||||
/>
|
||||
<button
|
||||
class="p-1 bg-accent-blue text-white rounded-lg hover:bg-accent-blue/80 transition-colors"
|
||||
onclick={addCategory}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<label
|
||||
class="flex-1 flex items-center justify-center gap-2 py-1.5 bg-bg-secondary border border-border-color rounded-lg text-[10px] font-medium text-text-secondary hover:bg-bg-primary transition-colors cursor-pointer"
|
||||
>
|
||||
<Upload size={12} />
|
||||
Import
|
||||
<input type="file" accept=".opml,.xml" class="hidden" onchange={handleImport} />
|
||||
</label>
|
||||
<button
|
||||
class="flex-1 flex items-center justify-center gap-2 py-1.5 bg-bg-secondary border border-border-color rounded-lg text-[10px] font-medium text-text-secondary hover:bg-bg-primary transition-colors"
|
||||
onclick={handleExport}
|
||||
>
|
||||
<Download size={12} />
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-1" role="list">
|
||||
{#each [...newsStore.categories].sort((a, b) => a.order - b.order) as cat (cat.id)}
|
||||
{@const catFeeds = getFeedsForCategory(cat.id)}
|
||||
{#if catFeeds.length > 0 || isManageMode}
|
||||
<div
|
||||
class="space-y-1 rounded-xl transition-all {dragOverId === cat.id
|
||||
? 'ring-2 ring-accent-blue/50 bg-accent-blue/5'
|
||||
: ''}"
|
||||
draggable={isManageMode}
|
||||
role="listitem"
|
||||
ondragstart={(e) => handleDragStart(e, 'category', cat.id)}
|
||||
ondragover={(e) => handleDragOver(e, cat.id)}
|
||||
ondrop={(e) => handleDrop(e, 'category', cat.id)}
|
||||
>
|
||||
<div class="flex items-center group">
|
||||
{#if isManageMode}
|
||||
<div class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity pr-2">
|
||||
<button class="p-1 text-text-secondary hover:text-accent-blue" onclick={() => startEditCategory(cat)}><Edit2 size={12} /></button>
|
||||
<button class="p-1 text-text-secondary hover:text-red-500" onclick={() => newsStore.deleteCategory(cat.id)}><Trash2 size={12} /></button>
|
||||
<div class="text-text-secondary/30 cursor-grab active:cursor-grabbing pl-2">
|
||||
<GripVertical size={14} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if expandedCategories[cat.id]}
|
||||
<div class="pl-4 space-y-0.5" transition:slide={{ duration: 200 }} role="list">
|
||||
{#each catFeeds as feed (feed.id)}
|
||||
<div
|
||||
class="flex items-center group rounded-xl transition-all {dragOverId === feed.id ? 'ring-2 ring-accent-blue/50 bg-accent-blue/5' : ''}"
|
||||
draggable={isManageMode}
|
||||
role="listitem"
|
||||
ondragstart={(e) => handleDragStart(e, 'feed', feed.id)}
|
||||
ondragover={(e) => handleDragOver(e, feed.id)}
|
||||
ondrop={(e) => handleDrop(e, 'feed', feed.id)}
|
||||
{#if editingCategoryId === cat.id}
|
||||
<div class="flex-1 flex items-center gap-1 px-2 py-1">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editingCategoryName}
|
||||
class="flex-1 bg-bg-primary border border-border-color rounded px-2 py-0.5 text-sm outline-none"
|
||||
onkeydown={(e) => e.key === 'Enter' && saveCategory()}
|
||||
/>
|
||||
<button class="text-green-500" onclick={saveCategory}><Save size={14} /></button
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="flex-1 flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium text-text-secondary hover:bg-bg-secondary transition-colors text-left min-w-0"
|
||||
onclick={() => toggleCategory(cat.id)}
|
||||
title={cat.name}
|
||||
>
|
||||
{#if isManageMode}
|
||||
<div class="text-text-secondary/30 cursor-grab active:cursor-grabbing pl-2">
|
||||
<GripVertical size={12} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if editingFeedId === feed.id}
|
||||
<div class="flex-1 flex flex-col gap-1 p-2 bg-bg-secondary/50 rounded-xl">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editingFeedTitle}
|
||||
placeholder="Feed title"
|
||||
class="w-full bg-bg-primary border border-border-color rounded px-2 py-1 text-xs outline-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editingFeedUrl}
|
||||
placeholder="Feed URL"
|
||||
class="w-full bg-bg-primary border border-border-color rounded px-2 py-1 text-[10px] outline-none"
|
||||
/>
|
||||
<div class="flex justify-end gap-1 mt-1">
|
||||
<button class="p-1 text-red-500 hover:bg-red-500/10 rounded" onclick={() => editingFeedId = null}><X size={14} /></button>
|
||||
<button class="p-1 text-green-500 hover:bg-green-500/10 rounded" onclick={saveFeed}><Save size={14} /></button>
|
||||
</div>
|
||||
</div>
|
||||
{#if expandedCategories[cat.id]}
|
||||
<ChevronDown size={16} />
|
||||
{:else}
|
||||
<button
|
||||
class="flex-1 flex items-center gap-3 px-4 py-2 rounded-xl text-sm transition-colors {newsStore.selectedFeedId === feed.id ? 'bg-accent-blue/10 text-accent-blue font-semibold' : 'text-text-secondary hover:bg-bg-secondary'} text-left min-w-0"
|
||||
onclick={() => { newsStore.selectFeed(feed.id); newsStore.readingArticle = null; newsStore.showSidebar = false; }}
|
||||
title={feed.title}
|
||||
>
|
||||
{#if feed.error}
|
||||
<AlertCircle size={16} class="text-red-500 flex-shrink-0" />
|
||||
{:else if feed.icon}
|
||||
<img src={feed.icon} alt="" class="w-4 h-4 rounded-sm flex-shrink-0" />
|
||||
{:else}
|
||||
<Hash size={16} class="flex-shrink-0" />
|
||||
{/if}
|
||||
<span class="truncate {feed.error ? 'text-red-500' : ''}">{feed.title}</span>
|
||||
</button>
|
||||
<ChevronRight size={16} />
|
||||
{/if}
|
||||
<span class="truncate">{cat.name}</span>
|
||||
<span class="ml-auto text-[10px] bg-bg-secondary px-1.5 py-0.5 rounded-full"
|
||||
>{catFeeds.length}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
{#if isManageMode}
|
||||
<div
|
||||
class="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity pr-2"
|
||||
>
|
||||
<button
|
||||
class="p-1 text-text-secondary hover:text-accent-blue"
|
||||
onclick={() => startEditCategory(cat)}><Edit2 size={12} /></button
|
||||
>
|
||||
<button
|
||||
class="p-1 text-text-secondary hover:text-red-500"
|
||||
onclick={() => newsStore.deleteCategory(cat.id)}
|
||||
><Trash2 size={12} /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if expandedCategories[cat.id]}
|
||||
<div class="pl-4 space-y-0.5" transition:slide={{ duration: 200 }} role="list">
|
||||
{#each catFeeds as feed (feed.id)}
|
||||
<div
|
||||
class="flex items-center group rounded-xl transition-all {dragOverId ===
|
||||
feed.id
|
||||
? 'ring-2 ring-accent-blue/50 bg-accent-blue/5'
|
||||
: ''}"
|
||||
draggable={isManageMode}
|
||||
role="listitem"
|
||||
ondragstart={(e) => handleDragStart(e, 'feed', feed.id)}
|
||||
ondragover={(e) => handleDragOver(e, feed.id)}
|
||||
ondrop={(e) => handleDrop(e, 'feed', feed.id)}
|
||||
>
|
||||
{#if isManageMode}
|
||||
<div class="opacity-0 group-hover:opacity-100 transition-opacity pr-2 flex items-center gap-0.5">
|
||||
<button class="p-1 text-text-secondary hover:text-accent-blue" onclick={() => startEditFeed(feed)} title="Edit feed"><Edit2 size={12} /></button>
|
||||
<button class="p-1 text-text-secondary hover:text-red-500" onclick={() => newsStore.deleteFeed(feed.id)} title="Delete feed"><Trash2 size={12} /></button>
|
||||
<div class="text-text-secondary/30 cursor-grab active:cursor-grabbing pl-2">
|
||||
<GripVertical size={12} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if editingFeedId === feed.id}
|
||||
<div class="flex-1 flex flex-col gap-1 p-2 bg-bg-secondary/50 rounded-xl">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editingFeedTitle}
|
||||
placeholder="Feed title"
|
||||
class="w-full bg-bg-primary border border-border-color rounded px-2 py-1 text-xs outline-none"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editingFeedUrl}
|
||||
placeholder="Feed URL"
|
||||
class="w-full bg-bg-primary border border-border-color rounded px-2 py-1 text-[10px] outline-none"
|
||||
/>
|
||||
<div class="flex justify-end gap-1 mt-1">
|
||||
<button
|
||||
class="p-1 text-red-500 hover:bg-red-500/10 rounded"
|
||||
onclick={() => (editingFeedId = null)}><X size={14} /></button
|
||||
>
|
||||
<button
|
||||
class="p-1 text-green-500 hover:bg-green-500/10 rounded"
|
||||
onclick={saveFeed}><Save size={14} /></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="flex-1 flex items-center gap-3 px-4 py-2 rounded-xl text-sm transition-colors {newsStore.selectedFeedId ===
|
||||
feed.id
|
||||
? 'bg-accent-blue/10 text-accent-blue font-semibold'
|
||||
: 'text-text-secondary hover:bg-bg-secondary'} text-left min-w-0"
|
||||
onclick={() => {
|
||||
newsStore.selectFeed(feed.id);
|
||||
newsStore.readingArticle = null;
|
||||
newsStore.showSidebar = false;
|
||||
}}
|
||||
title={feed.title}
|
||||
>
|
||||
{#if feed.error}
|
||||
<AlertCircle size={16} class="text-red-500 flex-shrink-0" />
|
||||
{:else if feed.icon}
|
||||
<img src={feed.icon} alt="" class="w-4 h-4 rounded-sm flex-shrink-0" />
|
||||
{:else}
|
||||
<Hash size={16} class="flex-shrink-0" />
|
||||
{/if}
|
||||
<span class="truncate {feed.error ? 'text-red-500' : ''}"
|
||||
>{feed.title}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
{#if isManageMode}
|
||||
<div
|
||||
class="opacity-0 group-hover:opacity-100 transition-opacity pr-2 flex items-center gap-0.5"
|
||||
>
|
||||
<button
|
||||
class="p-1 text-text-secondary hover:text-accent-blue"
|
||||
onclick={() => startEditFeed(feed)}
|
||||
title="Edit feed"><Edit2 size={12} /></button
|
||||
>
|
||||
<button
|
||||
class="p-1 text-text-secondary hover:text-red-500"
|
||||
onclick={() => newsStore.deleteFeed(feed.id)}
|
||||
title="Delete feed"><Trash2 size={12} /></button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if newsStore.feeds.length === 0}
|
||||
<p class="px-4 text-xs text-text-secondary italic">No feeds added yet</p>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if newsStore.feeds.length === 0}
|
||||
<p class="px-4 text-xs text-text-secondary italic">No feeds added yet</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-center pb-24 md:pb-6 space-y-4">
|
||||
<div class="flex flex-col items-center space-y-1">
|
||||
<a
|
||||
href="https://git.quad4.io/Quad4-Software/webnews"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-1.5 text-[11px] text-text-secondary hover:text-accent-blue transition-colors font-medium"
|
||||
<div class="flex flex-col items-center pb-24 md:pb-6 space-y-4">
|
||||
<div class="flex flex-col items-center space-y-1">
|
||||
<a
|
||||
href="https://git.quad4.io/Quad4-Software/webnews"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex items-center gap-1.5 text-[11px] text-text-secondary hover:text-accent-blue transition-colors font-medium"
|
||||
>
|
||||
<GitBranch size={13} />
|
||||
<span>v0.1.0</span>
|
||||
</a>
|
||||
<p class="text-[11px] text-text-secondary font-medium">
|
||||
Created by <a
|
||||
href="https://quad4.io"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="hover:text-accent-blue transition-colors">Quad4</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center gap-3 px-4 py-2 rounded-xl text-text-secondary hover:bg-bg-secondary transition-colors w-full max-w-[200px]"
|
||||
onclick={onOpenSettings}
|
||||
>
|
||||
<GitBranch size={13} />
|
||||
<span>v0.1.0</span>
|
||||
</a>
|
||||
<p class="text-[11px] text-text-secondary font-medium">
|
||||
Created by <a href="https://quad4.io" target="_blank" rel="noopener noreferrer" class="hover:text-accent-blue transition-colors">Quad4</a>
|
||||
</p>
|
||||
<SettingsIcon size={18} />
|
||||
<span class="font-medium text-sm">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-center gap-3 px-4 py-2 rounded-xl text-text-secondary hover:bg-bg-secondary transition-colors w-full max-w-[200px]"
|
||||
onclick={onOpenSettings}
|
||||
>
|
||||
<SettingsIcon size={18} />
|
||||
<span class="font-medium text-sm">Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -7,27 +7,31 @@
|
||||
info: Info,
|
||||
success: CheckCircle,
|
||||
error: AlertCircle,
|
||||
warning: AlertTriangle
|
||||
warning: AlertTriangle,
|
||||
};
|
||||
|
||||
const colors = {
|
||||
info: 'text-blue-500 bg-bg-secondary/95 border-blue-500/30',
|
||||
success: 'text-green-500 bg-bg-secondary/95 border-green-500/30',
|
||||
error: 'text-red-500 bg-bg-secondary/95 border-red-500/30',
|
||||
warning: 'text-yellow-500 bg-bg-secondary/95 border-yellow-500/30'
|
||||
warning: 'text-yellow-500 bg-bg-secondary/95 border-yellow-500/30',
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="fixed bottom-20 md:bottom-6 left-1/2 -translate-x-1/2 z-[200] flex flex-col gap-2 w-full max-w-sm px-4">
|
||||
<div
|
||||
class="fixed bottom-20 md:bottom-6 left-1/2 -translate-x-1/2 z-[200] flex flex-col gap-2 w-full max-w-sm px-4"
|
||||
>
|
||||
{#each toast.toasts as t (t.id)}
|
||||
<div
|
||||
class="flex items-center gap-3 p-4 rounded-2xl border backdrop-blur-xl shadow-2xl {colors[t.type]}"
|
||||
<div
|
||||
class="flex items-center gap-3 p-4 rounded-2xl border backdrop-blur-xl shadow-2xl {colors[
|
||||
t.type
|
||||
]}"
|
||||
in:fly={{ y: 20, duration: 300 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
<svelte:component this={icons[t.type]} size={20} class="flex-shrink-0" />
|
||||
<p class="text-sm font-medium flex-1 leading-snug">{t.message}</p>
|
||||
<button
|
||||
<button
|
||||
class="text-text-secondary hover:text-text-primary transition-colors p-1"
|
||||
onclick={() => toast.remove(t.id)}
|
||||
>
|
||||
@@ -36,4 +40,3 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user