Consolidate Weather/Stocks/Bookmarks/PoE2 into a single admin "Widgets" tab
Each widget now has an independent enable/disable checkbox that gates its visibility in the sidebar (new global_settings.widgets columns + GET /api/widgets), and is collapsed by default behind a WidgetSection wrapper so the tab stays manageable as more widgets get added. The four existing tab components (WeatherTab/StocksTab/BookmarksTab/Poe2Tab) are reused unmodified as each section's expanded content.
This commit is contained in:
@@ -60,6 +60,11 @@ export async function registerPublicRoutes(app: FastifyInstance) {
|
||||
return categories.filter((c) => !c.isPrivate);
|
||||
});
|
||||
|
||||
// Per-widget sidebar visibility — see the admin panel's consolidated "Widgets" tab.
|
||||
// Each widget keeps polling/config regardless of this; it only gates whether the
|
||||
// sidebar renders it at all.
|
||||
app.get('/api/widgets', async () => settingsDb.getSettings().widgets);
|
||||
|
||||
// Sidebar widgets — see WeatherTab/StocksTab/BookmarksTab in the admin panel.
|
||||
app.get('/api/weather', async () => settingsDb.getSettings().weather);
|
||||
|
||||
|
||||
@@ -185,6 +185,10 @@ export function migrate() {
|
||||
nitter_media_mode TEXT NOT NULL DEFAULT 'proxy', -- self-host | proxy | direct
|
||||
fxtwitter_base_url TEXT NOT NULL DEFAULT 'https://api.fxtwitter.com',
|
||||
telegram_media_mode TEXT NOT NULL DEFAULT 'self-host', -- self-host | proxy (no "direct" — Telegram has no public hotlinkable media URL)
|
||||
widget_weather_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
widget_stocks_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
widget_bookmarks_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
widget_poe2_enabled INTEGER NOT NULL DEFAULT 1,
|
||||
weather_location_name TEXT,
|
||||
weather_latitude REAL,
|
||||
weather_longitude REAL,
|
||||
@@ -348,6 +352,12 @@ export function migrate() {
|
||||
db.exec('ALTER TABLE global_settings ADD COLUMN poe2_league_name TEXT');
|
||||
db.exec('ALTER TABLE global_settings ADD COLUMN poe2_updated_at TEXT');
|
||||
}
|
||||
if (!hasColumn('global_settings', 'widget_weather_enabled')) {
|
||||
db.exec('ALTER TABLE global_settings ADD COLUMN widget_weather_enabled INTEGER NOT NULL DEFAULT 1');
|
||||
db.exec('ALTER TABLE global_settings ADD COLUMN widget_stocks_enabled INTEGER NOT NULL DEFAULT 1');
|
||||
db.exec('ALTER TABLE global_settings ADD COLUMN widget_bookmarks_enabled INTEGER NOT NULL DEFAULT 1');
|
||||
db.exec('ALTER TABLE global_settings ADD COLUMN widget_poe2_enabled INTEGER NOT NULL DEFAULT 1');
|
||||
}
|
||||
|
||||
// Seed a handful of sensible default tickers so the Stocks widget isn't empty on a
|
||||
// fresh install — the admin can remove/replace any of them via the Stocks tab.
|
||||
|
||||
@@ -16,6 +16,12 @@ function rowToSettings(row: any): GlobalSettings {
|
||||
nitterMediaMode: row.nitter_media_mode,
|
||||
fxtwitterBaseUrl: row.fxtwitter_base_url,
|
||||
telegramMediaMode: row.telegram_media_mode,
|
||||
widgets: {
|
||||
weather: !!row.widget_weather_enabled,
|
||||
stocks: !!row.widget_stocks_enabled,
|
||||
bookmarks: !!row.widget_bookmarks_enabled,
|
||||
poe2: !!row.widget_poe2_enabled
|
||||
},
|
||||
retention: {
|
||||
publishedArticleMaxAgeDays: row.published_article_max_age_days,
|
||||
rawItemMaxAgeDays: row.raw_item_max_age_days,
|
||||
@@ -58,7 +64,8 @@ export function updateSettings(patch: Partial<GlobalSettings>): GlobalSettings {
|
||||
retention: { ...current.retention, ...(patch.retention ?? {}) },
|
||||
selectedModels: { ...current.selectedModels, ...(patch.selectedModels ?? {}) },
|
||||
weather: { ...current.weather, ...(patch.weather ?? {}) },
|
||||
poe2: { ...current.poe2, ...(patch.poe2 ?? {}) }
|
||||
poe2: { ...current.poe2, ...(patch.poe2 ?? {}) },
|
||||
widgets: { ...current.widgets, ...(patch.widgets ?? {}) }
|
||||
};
|
||||
// Named params (rather than positional `?`) so this list can be reordered or
|
||||
// extended without the column list and the bound-values list silently drifting
|
||||
@@ -71,6 +78,8 @@ export function updateSettings(patch: Partial<GlobalSettings>): GlobalSettings {
|
||||
follow_up_min_hours_since_last=$follow_up_min_hours_since_last, follow_up_min_new_sources=$follow_up_min_new_sources,
|
||||
ai_service_host=$ai_service_host, ai_service_port=$ai_service_port, selected_models=$selected_models,
|
||||
nitter_media_mode=$nitter_media_mode, fxtwitter_base_url=$fxtwitter_base_url, telegram_media_mode=$telegram_media_mode,
|
||||
widget_weather_enabled=$widget_weather_enabled, widget_stocks_enabled=$widget_stocks_enabled,
|
||||
widget_bookmarks_enabled=$widget_bookmarks_enabled, widget_poe2_enabled=$widget_poe2_enabled,
|
||||
published_article_max_age_days=$published_article_max_age_days, raw_item_max_age_days=$raw_item_max_age_days,
|
||||
storage_cap_enabled=$storage_cap_enabled, storage_cap_value=$storage_cap_value, storage_cap_unit=$storage_cap_unit,
|
||||
weather_location_name=$weather_location_name, weather_latitude=$weather_latitude, weather_longitude=$weather_longitude,
|
||||
@@ -93,6 +102,10 @@ export function updateSettings(patch: Partial<GlobalSettings>): GlobalSettings {
|
||||
$nitter_media_mode: merged.nitterMediaMode,
|
||||
$fxtwitter_base_url: merged.fxtwitterBaseUrl,
|
||||
$telegram_media_mode: merged.telegramMediaMode,
|
||||
$widget_weather_enabled: merged.widgets.weather ? 1 : 0,
|
||||
$widget_stocks_enabled: merged.widgets.stocks ? 1 : 0,
|
||||
$widget_bookmarks_enabled: merged.widgets.bookmarks ? 1 : 0,
|
||||
$widget_poe2_enabled: merged.widgets.poe2 ? 1 : 0,
|
||||
$published_article_max_age_days: merged.retention.publishedArticleMaxAgeDays,
|
||||
$raw_item_max_age_days: merged.retention.rawItemMaxAgeDays,
|
||||
$storage_cap_enabled: merged.retention.storageCapEnabled ? 1 : 0,
|
||||
|
||||
@@ -280,6 +280,13 @@ export interface GlobalSettings {
|
||||
fxtwitterBaseUrl: string;
|
||||
/** How Telegram message media (attached photos/videos, channel avatars) is served — see pipeline/publish.ts's resolveTelegramMedia. No "direct" option: Telegram has no public hotlinkable media URL, bytes only come from the authenticated MTProto session. */
|
||||
telegramMediaMode: 'self-host' | 'proxy';
|
||||
/** Per-widget sidebar visibility — see admin/settings' consolidated "Widgets" tab. Each widget keeps polling/config regardless (disabling doesn't pause its poller), this only gates whether GET /api/widgets tells the sidebar to render it. */
|
||||
widgets: {
|
||||
weather: boolean;
|
||||
stocks: boolean;
|
||||
bookmarks: boolean;
|
||||
poe2: boolean;
|
||||
};
|
||||
retention: {
|
||||
publishedArticleMaxAgeDays: number | null;
|
||||
rawItemMaxAgeDays: number | null;
|
||||
|
||||
@@ -118,6 +118,13 @@ export interface AdminPoe2Settings {
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface AdminWidgetsEnabled {
|
||||
weather: boolean;
|
||||
stocks: boolean;
|
||||
bookmarks: boolean;
|
||||
poe2: boolean;
|
||||
}
|
||||
|
||||
export interface AdminSettings {
|
||||
mergeStrictness: 1 | 2 | 3 | 4 | 5;
|
||||
defaultPollIntervalMinutes: number;
|
||||
@@ -132,6 +139,7 @@ export interface AdminSettings {
|
||||
nitterMediaMode: 'self-host' | 'proxy' | 'direct';
|
||||
fxtwitterBaseUrl: string;
|
||||
telegramMediaMode: 'self-host' | 'proxy';
|
||||
widgets: AdminWidgetsEnabled;
|
||||
retention: RetentionSettings;
|
||||
categoryPriority: CategoryPriority[];
|
||||
weather: AdminWeatherSettings;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getBackendUrl } from './config';
|
||||
import type { MergedArticle, Tag, TrackedEventPublic, Category, Weather, StockTicker, Bookmark, Poe2Data } from './types';
|
||||
import type { MergedArticle, Tag, TrackedEventPublic, Category, Weather, StockTicker, Bookmark, Poe2Data, WidgetsEnabled } from './types';
|
||||
|
||||
async function get<T>(path: string, fetchFn: typeof fetch = fetch): Promise<T> {
|
||||
// credentials: 'include' so the private-access cookie (see lib/privateAccess.ts)
|
||||
@@ -57,3 +57,7 @@ export function getBookmarks(fetchFn?: typeof fetch): Promise<Bookmark[]> {
|
||||
export function getPoe2(fetchFn?: typeof fetch): Promise<Poe2Data> {
|
||||
return get<Poe2Data>('/api/poe2', fetchFn);
|
||||
}
|
||||
|
||||
export function getWidgetsEnabled(fetchFn?: typeof fetch): Promise<WidgetsEnabled> {
|
||||
return get<WidgetsEnabled>('/api/widgets', fetchFn);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
// Minimized by default — Weather/Stocks/Bookmarks/PoE2 stacked at full height would
|
||||
// make the consolidated "Widgets" tab unwieldy as more get added over time. Enabled
|
||||
// state is independent of expanded state: disabling a widget only hides it from the
|
||||
// sidebar (see Sidebar.svelte's widgetsEnabled gate), it doesn't stop the admin from
|
||||
// expanding this section to keep configuring it.
|
||||
let {
|
||||
title,
|
||||
enabled,
|
||||
onToggle,
|
||||
children
|
||||
}: {
|
||||
title: string;
|
||||
enabled: boolean;
|
||||
onToggle: () => void;
|
||||
children: Snippet;
|
||||
} = $props();
|
||||
|
||||
let expanded = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<button class="head-btn" onclick={() => (expanded = !expanded)} aria-expanded={expanded}>
|
||||
<span class="chevron" class:open={expanded}>▸</span>
|
||||
<span class="title">{title}</span>
|
||||
{#if !enabled}<span class="disabled-tag">Hidden</span>{/if}
|
||||
</button>
|
||||
<label class="enable-toggle" title={enabled ? 'Hide from sidebar' : 'Show in sidebar'}>
|
||||
<input type="checkbox" checked={enabled} onchange={onToggle} />
|
||||
</label>
|
||||
</div>
|
||||
{#if expanded}
|
||||
<div class="section-body">
|
||||
{@render children()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.section {
|
||||
background: var(--surface-1);
|
||||
border-radius: 12px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
.head-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.chevron {
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.chevron.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.disabled-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
border: 0.5px solid var(--border);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
.enable-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.enable-toggle input {
|
||||
width: auto;
|
||||
}
|
||||
.section-body {
|
||||
padding: 14px;
|
||||
padding-top: 0;
|
||||
border-top: 0.5px solid var(--border);
|
||||
margin-top: 0;
|
||||
}
|
||||
.section-body > :global(*:first-child) {
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import type { AdminSettings, AdminStockTicker, AdminBookmark, AdminPoe2Entry } from '$lib/adminTypes';
|
||||
import { updateSettings } from '$lib/adminApi';
|
||||
import WidgetSection from './WidgetSection.svelte';
|
||||
import WeatherTab from './WeatherTab.svelte';
|
||||
import StocksTab from './StocksTab.svelte';
|
||||
import BookmarksTab from './BookmarksTab.svelte';
|
||||
import Poe2Tab from './Poe2Tab.svelte';
|
||||
|
||||
let {
|
||||
settings,
|
||||
stockTickers,
|
||||
bookmarks,
|
||||
poe2Watchlist
|
||||
}: {
|
||||
settings: AdminSettings;
|
||||
stockTickers: AdminStockTicker[];
|
||||
bookmarks: AdminBookmark[];
|
||||
poe2Watchlist: AdminPoe2Entry[];
|
||||
} = $props();
|
||||
|
||||
// Local copy so each checkbox flips immediately — same idiom as BookmarksTab's
|
||||
// per-row "Private" toggle, just for widget visibility instead.
|
||||
let widgets = $state({ ...settings.widgets });
|
||||
|
||||
async function toggle(key: keyof typeof widgets) {
|
||||
widgets[key] = !widgets[key];
|
||||
await updateSettings({ widgets });
|
||||
}
|
||||
</script>
|
||||
|
||||
<p class="hint">
|
||||
Each widget can be shown or hidden from the sidebar independently. Hiding one only affects
|
||||
whether it's visible on the site — its own settings and data below keep working either way.
|
||||
</p>
|
||||
|
||||
<WidgetSection title="Weather" enabled={widgets.weather} onToggle={() => toggle('weather')}>
|
||||
<WeatherTab {settings} />
|
||||
</WidgetSection>
|
||||
|
||||
<WidgetSection title="Stocks" enabled={widgets.stocks} onToggle={() => toggle('stocks')}>
|
||||
<StocksTab tickers={stockTickers} />
|
||||
</WidgetSection>
|
||||
|
||||
<WidgetSection title="Bookmarks" enabled={widgets.bookmarks} onToggle={() => toggle('bookmarks')}>
|
||||
<BookmarksTab {bookmarks} />
|
||||
</WidgetSection>
|
||||
|
||||
<WidgetSection title="PoE2" enabled={widgets.poe2} onToggle={() => toggle('poe2')}>
|
||||
<Poe2Tab {settings} watchlist={poe2Watchlist} />
|
||||
</WidgetSection>
|
||||
|
||||
<style>
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { tick } from 'svelte';
|
||||
import type { Weather, StockTicker, Bookmark, Poe2Data } from '$lib/types';
|
||||
import type { Weather, StockTicker, Bookmark, Poe2Data, WidgetsEnabled } from '$lib/types';
|
||||
import WeatherWidget from './WeatherWidget.svelte';
|
||||
import StocksWidget from './StocksWidget.svelte';
|
||||
import BookmarksWidget from './BookmarksWidget.svelte';
|
||||
import Poe2Widget from './Poe2Widget.svelte';
|
||||
|
||||
let { weather, stocks, bookmarks, poe2 }: { weather: Weather; stocks: StockTicker[]; bookmarks: Bookmark[]; poe2: Poe2Data } = $props();
|
||||
let {
|
||||
weather,
|
||||
stocks,
|
||||
bookmarks,
|
||||
poe2,
|
||||
widgetsEnabled
|
||||
}: {
|
||||
weather: Weather;
|
||||
stocks: StockTicker[];
|
||||
bookmarks: Bookmark[];
|
||||
poe2: Poe2Data;
|
||||
widgetsEnabled: WidgetsEnabled;
|
||||
} = $props();
|
||||
|
||||
// Weather + Stocks + PoE2 + Bookmarks stacked can be taller than the viewport. Plain
|
||||
// `position: sticky` alone can only pin a box at a constant offset — it can't reveal
|
||||
@@ -55,11 +67,13 @@
|
||||
|
||||
$effect(() => {
|
||||
// Widget data changing the sidebar's natural height needs a remeasure, not just a
|
||||
// scroll-position update.
|
||||
// scroll-position update. widgetsEnabled changes it too — a disabled widget is
|
||||
// removed from the flow entirely, not just emptied.
|
||||
void weather;
|
||||
void stocks;
|
||||
void bookmarks;
|
||||
void poe2;
|
||||
void widgetsEnabled;
|
||||
remeasure();
|
||||
});
|
||||
|
||||
@@ -76,10 +90,10 @@
|
||||
<div class="sidebar-track" bind:this={trackEl} style:height="{trackHeight}px">
|
||||
<aside class="sidebar-viewport" style:height="{viewportHeight}px">
|
||||
<div class="sidebar-content" bind:this={contentEl} style:transform="translateY(-{progress}px)">
|
||||
<WeatherWidget {weather} />
|
||||
<StocksWidget {stocks} />
|
||||
<Poe2Widget {poe2} />
|
||||
<BookmarksWidget {bookmarks} />
|
||||
{#if widgetsEnabled.weather}<WeatherWidget {weather} />{/if}
|
||||
{#if widgetsEnabled.stocks}<StocksWidget {stocks} />{/if}
|
||||
{#if widgetsEnabled.poe2}<Poe2Widget {poe2} />{/if}
|
||||
{#if widgetsEnabled.bookmarks}<BookmarksWidget {bookmarks} />{/if}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -171,3 +171,11 @@ export interface Poe2Data {
|
||||
updatedAt: string | null;
|
||||
entries: Poe2WatchlistEntry[];
|
||||
}
|
||||
|
||||
/** Per-widget sidebar visibility, admin-toggled from the consolidated "Widgets" tab. */
|
||||
export interface WidgetsEnabled {
|
||||
weather: boolean;
|
||||
stocks: boolean;
|
||||
bookmarks: boolean;
|
||||
poe2: boolean;
|
||||
}
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
{@render children()}
|
||||
</div>
|
||||
{#if showSidebar}
|
||||
<Sidebar weather={data.weather} stocks={data.stocks} bookmarks={data.bookmarks} poe2={data.poe2} />
|
||||
<Sidebar weather={data.weather} stocks={data.stocks} bookmarks={data.bookmarks} poe2={data.poe2} widgetsEnabled={data.widgetsEnabled} />
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { LayoutLoad } from './$types';
|
||||
import { getCategories, getEvents, getWeather, getStocks, getBookmarks, getPoe2 } from '$lib/api';
|
||||
import { getCategories, getEvents, getWeather, getStocks, getBookmarks, getPoe2, getWidgetsEnabled } from '$lib/api';
|
||||
import { getPrivateAccessStatus } from '$lib/privateAccess';
|
||||
|
||||
// Named so the layout can be re-fetched on its own (see +layout.svelte's periodic
|
||||
@@ -7,14 +7,15 @@ import { getPrivateAccessStatus } from '$lib/privateAccess';
|
||||
// own pagination state, which a blanket invalidateAll() would reset every refresh.
|
||||
export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
depends('app:sidebar');
|
||||
const [categories, events, privateAccess, weather, stocks, bookmarks, poe2] = await Promise.all([
|
||||
const [categories, events, privateAccess, weather, stocks, bookmarks, poe2, widgetsEnabled] = await Promise.all([
|
||||
getCategories(fetch),
|
||||
getEvents(fetch),
|
||||
getPrivateAccessStatus(fetch),
|
||||
getWeather(fetch),
|
||||
getStocks(fetch),
|
||||
getBookmarks(fetch),
|
||||
getPoe2(fetch)
|
||||
getPoe2(fetch),
|
||||
getWidgetsEnabled(fetch)
|
||||
]);
|
||||
// Tracked events are a displayed category like any other (see MergeTab/EventsTab) —
|
||||
// only active ones show up as browsable, same as a paused/disabled category wouldn't.
|
||||
@@ -26,6 +27,7 @@ export const load: LayoutLoad = async ({ fetch, data, depends }) => {
|
||||
weather,
|
||||
stocks,
|
||||
bookmarks,
|
||||
poe2
|
||||
poe2,
|
||||
widgetsEnabled
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,10 +5,7 @@
|
||||
import ModelsTab from '$lib/components/admin/ModelsTab.svelte';
|
||||
import RetentionTab from '$lib/components/admin/RetentionTab.svelte';
|
||||
import EventsTab from '$lib/components/admin/EventsTab.svelte';
|
||||
import WeatherTab from '$lib/components/admin/WeatherTab.svelte';
|
||||
import StocksTab from '$lib/components/admin/StocksTab.svelte';
|
||||
import BookmarksTab from '$lib/components/admin/BookmarksTab.svelte';
|
||||
import Poe2Tab from '$lib/components/admin/Poe2Tab.svelte';
|
||||
import WidgetsTab from '$lib/components/admin/WidgetsTab.svelte';
|
||||
import ConnectionsTab from '$lib/components/admin/ConnectionsTab.svelte';
|
||||
import LogsTab from '$lib/components/admin/LogsTab.svelte';
|
||||
|
||||
@@ -20,10 +17,7 @@
|
||||
{ id: 'models', label: 'Models' },
|
||||
{ id: 'retention', label: 'Retention' },
|
||||
{ id: 'events', label: 'Tracked events' },
|
||||
{ id: 'weather', label: 'Weather' },
|
||||
{ id: 'stocks', label: 'Stocks' },
|
||||
{ id: 'bookmarks', label: 'Bookmarks' },
|
||||
{ id: 'poe2', label: 'PoE2' },
|
||||
{ id: 'widgets', label: 'Widgets' },
|
||||
{ id: 'connections', label: 'Connections' },
|
||||
{ id: 'logs', label: 'Logs' }
|
||||
];
|
||||
@@ -53,14 +47,8 @@
|
||||
<RetentionTab settings={data.settings} />
|
||||
{:else if active === 'events'}
|
||||
<EventsTab events={data.events} sources={data.sources} />
|
||||
{:else if active === 'weather'}
|
||||
<WeatherTab settings={data.settings} />
|
||||
{:else if active === 'stocks'}
|
||||
<StocksTab tickers={data.stockTickers} />
|
||||
{:else if active === 'bookmarks'}
|
||||
<BookmarksTab bookmarks={data.bookmarks} />
|
||||
{:else if active === 'poe2'}
|
||||
<Poe2Tab settings={data.settings} watchlist={data.poe2Watchlist} />
|
||||
{:else if active === 'widgets'}
|
||||
<WidgetsTab settings={data.settings} stockTickers={data.stockTickers} bookmarks={data.bookmarks} poe2Watchlist={data.poe2Watchlist} />
|
||||
{:else if active === 'connections'}
|
||||
<ConnectionsTab settings={data.settings} aiStatus={data.aiStatus} telegramStatus={data.telegramStatus} />
|
||||
{:else if active === 'logs'}
|
||||
|
||||
Reference in New Issue
Block a user