mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 21:41:09 +02:00
revert(apps): settings pages back to routes (not workbench cards)
User feedback: per-module settings/preferences as separate workbench
cards bloats the scene-picker with rarely-used configuration surfaces.
Cards are for daily workflows; one-time config belongs in routes that
open from the parent module's ⚙ button.
- Inline the ListView content back into each /settings route
- Delete lib/modules/{broadcast-settings,invoices-settings,uload-settings,news-preferences}/
- Remove the four registerApp entries
Kept: spaces card (operative member management, daily use).
Deferred: admin-* cards will fuse into a single admin card with tabs
in a follow-up commit, since merging 4 power-user surfaces into tabs
is a different shape than deleting settings cards.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8647bfd100
commit
3e65637fcb
9 changed files with 493 additions and 578 deletions
|
|
@ -1318,50 +1318,6 @@ registerApp({
|
|||
},
|
||||
});
|
||||
|
||||
// ── Module-Settings Cards ────────────────────────────
|
||||
// Per-module settings/preferences as workbench cards so they can be
|
||||
// dropped into any scene without a subroute.
|
||||
|
||||
registerApp({
|
||||
id: 'broadcast-settings',
|
||||
name: 'Broadcast · Settings',
|
||||
color: '#6366f1',
|
||||
icon: Gear,
|
||||
views: {
|
||||
list: { load: () => import('$lib/modules/broadcast-settings/ListView.svelte') },
|
||||
},
|
||||
});
|
||||
|
||||
registerApp({
|
||||
id: 'invoices-settings',
|
||||
name: 'Invoices · Settings',
|
||||
color: '#059669',
|
||||
icon: Gear,
|
||||
views: {
|
||||
list: { load: () => import('$lib/modules/invoices-settings/ListView.svelte') },
|
||||
},
|
||||
});
|
||||
|
||||
registerApp({
|
||||
id: 'uload-settings',
|
||||
name: 'uLoad · Settings',
|
||||
color: '#0EA5E9',
|
||||
icon: Gear,
|
||||
views: {
|
||||
list: { load: () => import('$lib/modules/uload-settings/ListView.svelte') },
|
||||
},
|
||||
});
|
||||
|
||||
registerApp({
|
||||
id: 'news-preferences',
|
||||
name: 'News · Preferences',
|
||||
color: '#10B981',
|
||||
icon: Gear,
|
||||
views: {
|
||||
list: { load: () => import('$lib/modules/news-preferences/ListView.svelte') },
|
||||
},
|
||||
});
|
||||
|
||||
registerApp({
|
||||
id: 'quiz',
|
||||
name: 'Quiz',
|
||||
|
|
|
|||
|
|
@ -1,40 +0,0 @@
|
|||
<!--
|
||||
Broadcast → Settings — workbench card.
|
||||
|
||||
Wraps the existing SettingsForm so both the standalone route
|
||||
(/broadcasts/settings) and the workbench card render the same UI.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import SettingsForm from '$lib/modules/broadcast/components/SettingsForm.svelte';
|
||||
</script>
|
||||
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>Broadcast-Einstellungen</strong>
|
||||
<span class="sub">Sender-Defaults, Impressum und Footer</span>
|
||||
</div>
|
||||
</header>
|
||||
<SettingsForm />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pane {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.bar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bar .title strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bar .sub {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<!--
|
||||
Invoices → Settings — workbench card.
|
||||
|
||||
Wraps SenderProfileForm so the standalone route
|
||||
(/invoices/settings) and the workbench card share the same UI.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import SenderProfileForm from '$lib/modules/invoices/components/SenderProfileForm.svelte';
|
||||
</script>
|
||||
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>Rechnungs-Einstellungen</strong>
|
||||
<span class="sub">Absender, Nummernkreis und Standards</span>
|
||||
</div>
|
||||
</header>
|
||||
<SenderProfileForm />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pane {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.bar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bar .title strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bar .sub {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,231 +0,0 @@
|
|||
<!--
|
||||
News → Preferences — workbench card.
|
||||
|
||||
Topics + languages selection, source-management link, reset learned
|
||||
weights, rerun onboarding.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { usePreferences } from '$lib/modules/news/queries';
|
||||
import { preferencesStore } from '$lib/modules/news/stores/preferences.svelte';
|
||||
import { ALL_TOPICS, type Topic, type Language } from '$lib/modules/news/types';
|
||||
import { TOPIC_LABELS } from '$lib/modules/news/sources-meta';
|
||||
|
||||
const prefs$ = usePreferences();
|
||||
const prefs = $derived(prefs$.value);
|
||||
|
||||
let topicWeightCount = $derived(Object.keys(prefs.topicWeights).length);
|
||||
let sourceWeightCount = $derived(Object.keys(prefs.sourceWeights).length);
|
||||
|
||||
async function toggleTopic(t: Topic) {
|
||||
const next = prefs.selectedTopics.includes(t)
|
||||
? prefs.selectedTopics.filter((x) => x !== t)
|
||||
: [...prefs.selectedTopics, t];
|
||||
await preferencesStore.setTopics(next);
|
||||
}
|
||||
async function toggleLang(l: Language) {
|
||||
const next = prefs.preferredLanguages.includes(l)
|
||||
? prefs.preferredLanguages.filter((x) => x !== l)
|
||||
: [...prefs.preferredLanguages, l];
|
||||
await preferencesStore.setLanguages(next);
|
||||
}
|
||||
async function resetWeights() {
|
||||
if (!confirm('Alle gelernten Gewichtungen zurücksetzen?')) return;
|
||||
await preferencesStore.resetWeights();
|
||||
}
|
||||
async function rerunOnboarding() {
|
||||
await preferencesStore.applyWeightDiff({});
|
||||
const { preferencesTable } = await import('$lib/modules/news/collections');
|
||||
const { encryptRecord } = await import('$lib/data/crypto');
|
||||
const { PREFERENCES_ID } = await import('$lib/modules/news/types');
|
||||
const diff = { onboardingCompleted: false, updatedAt: new Date().toISOString() };
|
||||
await encryptRecord('newsPreferences', diff);
|
||||
await preferencesTable.update(PREFERENCES_ID, diff);
|
||||
goto('/news');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>News-Einstellungen</strong>
|
||||
<span class="sub">Themen · Sprachen · Gewichtungen</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<h2>Themen</h2>
|
||||
<p class="hint">Welche Themen sollen im Feed auftauchen?</p>
|
||||
<div class="grid">
|
||||
{#each ALL_TOPICS as topic}
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
class:selected={prefs.selectedTopics.includes(topic)}
|
||||
onclick={() => toggleTopic(topic)}
|
||||
>
|
||||
<span class="emoji">{TOPIC_LABELS[topic].emoji}</span>
|
||||
<span>{TOPIC_LABELS[topic].de}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Sprachen</h2>
|
||||
<div class="row">
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
class:selected={prefs.preferredLanguages.includes('de')}
|
||||
onclick={() => toggleLang('de')}
|
||||
>
|
||||
🇩🇪 Deutsch
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
class:selected={prefs.preferredLanguages.includes('en')}
|
||||
onclick={() => toggleLang('en')}
|
||||
>
|
||||
🇬🇧 English
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Quellen</h2>
|
||||
<p class="hint">
|
||||
Du blockst aktuell <strong>{prefs.blockedSources.length}</strong> Quellen.
|
||||
</p>
|
||||
<a class="btn-link" href="/news/sources">Quellen verwalten →</a>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Gelernte Gewichtungen</h2>
|
||||
<p class="hint">
|
||||
Über Reaktionen lernt der Feed deine Vorlieben:
|
||||
{topicWeightCount} Themen-Gewichte, {sourceWeightCount} Quellen-Gewichte.
|
||||
</p>
|
||||
<button type="button" class="btn-secondary" onclick={resetWeights}>Zurücksetzen</button>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Onboarding</h2>
|
||||
<p class="hint">Themen, Sprachen und Quellen neu wählen.</p>
|
||||
<button type="button" class="btn-secondary" onclick={rerunOnboarding}>
|
||||
Onboarding neu starten
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pane {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.bar .title strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar .sub {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1.125rem;
|
||||
background: hsl(var(--color-card));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 10px;
|
||||
background: hsl(var(--color-background, var(--color-card)));
|
||||
border: 2px solid hsl(var(--color-border));
|
||||
color: hsl(var(--color-foreground));
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 120ms ease,
|
||||
border-color 120ms ease;
|
||||
}
|
||||
|
||||
.pill.selected {
|
||||
border-color: hsl(var(--color-primary, 230 80% 55%));
|
||||
background: hsl(var(--color-primary, 230 80% 55%) / 0.12);
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
align-self: flex-start;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--color-background, var(--color-card)));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
color: hsl(var(--color-foreground));
|
||||
font: inherit;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
align-self: flex-start;
|
||||
color: hsl(var(--color-primary, 230 80% 55%));
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
<!--
|
||||
uLoad → Settings — workbench card.
|
||||
|
||||
Data overview (counts), JSON export, and the "clear all local data"
|
||||
danger zone. Uses the uLoad module's collections directly because this
|
||||
is a local-data admin surface, not a feature.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Trash, DownloadSimple } from '@mana/shared-icons';
|
||||
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import { useAllLinks, useAllTags, useAllFolders } from '$lib/modules/uload';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const links = useAllLinks();
|
||||
const tags = useAllTags();
|
||||
const folders = useAllFolders();
|
||||
|
||||
async function clearAllData() {
|
||||
if (!confirm('Alle lokalen uLoad-Daten löschen? Dies kann nicht rückgängig gemacht werden.'))
|
||||
return;
|
||||
|
||||
await linkTable.clear();
|
||||
await uloadTagTable.clear();
|
||||
await uloadFolderTable.clear();
|
||||
await linkTagTable.clear();
|
||||
toast.success('Alle uLoad-Daten gelöscht');
|
||||
}
|
||||
|
||||
async function exportData() {
|
||||
const rawLinks = await linkTable.toArray();
|
||||
const allLinks = await decryptRecords('links', rawLinks);
|
||||
const allTags = await uloadTagTable.toArray();
|
||||
const allFolders = await uloadFolderTable.toArray();
|
||||
const allLinkTags = await linkTagTable.toArray();
|
||||
|
||||
const data = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
links: allLinks,
|
||||
tags: allTags,
|
||||
folders: allFolders,
|
||||
linkTags: allLinkTags,
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `uload-export-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success('Export heruntergeladen');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>uLoad-Einstellungen</strong>
|
||||
<span class="sub">Datenübersicht · Export · Gefahrenzone</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Daten</h2>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<p class="stat-value">{links.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Links</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<p class="stat-value">{tags.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Tags</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<p class="stat-value">{folders.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Ordner</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Daten exportieren</h2>
|
||||
<p class="hint">Alle Links, Tags und Ordner als JSON-Datei herunterladen.</p>
|
||||
<button type="button" class="btn" onclick={exportData}>
|
||||
<DownloadSimple size={16} />
|
||||
JSON exportieren
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="panel danger">
|
||||
<h2>Gefahrenzone</h2>
|
||||
<p class="hint">
|
||||
Löscht alle lokalen uLoad-Daten (Links, Tags, Ordner). Synchronisierte Daten auf dem Server
|
||||
bleiben erhalten.
|
||||
</p>
|
||||
<button type="button" class="btn danger" onclick={clearAllData}>
|
||||
<Trash size={16} />
|
||||
Alle Daten löschen
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pane {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.bar .title strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar .sub {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: hsl(var(--color-card));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 12px;
|
||||
padding: 1.125rem;
|
||||
}
|
||||
|
||||
.panel.danger {
|
||||
border-color: hsl(0 70% 55% / 0.3);
|
||||
background: hsl(0 70% 55% / 0.04);
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.panel.danger h2 {
|
||||
color: hsl(0 70% 55%);
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin: 0 0 0.875rem;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--color-background, var(--color-card)));
|
||||
color: hsl(var(--color-foreground));
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 120ms ease,
|
||||
color 120ms ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background: hsl(0 70% 55%);
|
||||
color: white;
|
||||
border-color: hsl(0 70% 45%);
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background: hsl(0 70% 50%);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,12 +1,42 @@
|
|||
<!--
|
||||
/broadcasts/settings — sender defaults, impressum, footer for new campaigns.
|
||||
Reached via the ⚙ button in the broadcast module; not a workbench card.
|
||||
-->
|
||||
<script lang="ts">
|
||||
/**
|
||||
* /broadcasts/settings — renders the workbench-card ListView.
|
||||
*/
|
||||
import ListView from '$lib/modules/broadcast-settings/ListView.svelte';
|
||||
import SettingsForm from '$lib/modules/broadcast/components/SettingsForm.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Broadcast-Einstellungen — Mana</title>
|
||||
</svelte:head>
|
||||
|
||||
<ListView />
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>Broadcast-Einstellungen</strong>
|
||||
<span class="sub">Sender-Defaults, Impressum und Footer</span>
|
||||
</div>
|
||||
</header>
|
||||
<SettingsForm />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pane {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.bar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bar .title strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bar .sub {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,42 @@
|
|||
<!--
|
||||
/invoices/settings — sender profile, number range, invoice defaults.
|
||||
Reached via the ⚙ button in the invoices module; not a workbench card.
|
||||
-->
|
||||
<script lang="ts">
|
||||
/**
|
||||
* /invoices/settings — renders the workbench-card ListView.
|
||||
*/
|
||||
import ListView from '$lib/modules/invoices-settings/ListView.svelte';
|
||||
import SenderProfileForm from '$lib/modules/invoices/components/SenderProfileForm.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Rechnungs-Einstellungen — Mana</title>
|
||||
</svelte:head>
|
||||
|
||||
<ListView />
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>Rechnungs-Einstellungen</strong>
|
||||
<span class="sub">Absender, Nummernkreis und Standards</span>
|
||||
</div>
|
||||
</header>
|
||||
<SenderProfileForm />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pane {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
.bar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.bar .title strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bar .sub {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,233 @@
|
|||
<!--
|
||||
/news/preferences — topics, languages, reset weights, onboarding rerun.
|
||||
Reached via the ⚙ button in the news module; not a workbench card.
|
||||
-->
|
||||
<script lang="ts">
|
||||
/**
|
||||
* /news/preferences — renders the workbench-card ListView.
|
||||
*/
|
||||
import ListView from '$lib/modules/news-preferences/ListView.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { usePreferences } from '$lib/modules/news/queries';
|
||||
import { preferencesStore } from '$lib/modules/news/stores/preferences.svelte';
|
||||
import { ALL_TOPICS, type Topic, type Language } from '$lib/modules/news/types';
|
||||
import { TOPIC_LABELS } from '$lib/modules/news/sources-meta';
|
||||
|
||||
const prefs$ = usePreferences();
|
||||
const prefs = $derived(prefs$.value);
|
||||
|
||||
let topicWeightCount = $derived(Object.keys(prefs.topicWeights).length);
|
||||
let sourceWeightCount = $derived(Object.keys(prefs.sourceWeights).length);
|
||||
|
||||
async function toggleTopic(t: Topic) {
|
||||
const next = prefs.selectedTopics.includes(t)
|
||||
? prefs.selectedTopics.filter((x) => x !== t)
|
||||
: [...prefs.selectedTopics, t];
|
||||
await preferencesStore.setTopics(next);
|
||||
}
|
||||
async function toggleLang(l: Language) {
|
||||
const next = prefs.preferredLanguages.includes(l)
|
||||
? prefs.preferredLanguages.filter((x) => x !== l)
|
||||
: [...prefs.preferredLanguages, l];
|
||||
await preferencesStore.setLanguages(next);
|
||||
}
|
||||
async function resetWeights() {
|
||||
if (!confirm('Alle gelernten Gewichtungen zurücksetzen?')) return;
|
||||
await preferencesStore.resetWeights();
|
||||
}
|
||||
async function rerunOnboarding() {
|
||||
await preferencesStore.applyWeightDiff({});
|
||||
const { preferencesTable } = await import('$lib/modules/news/collections');
|
||||
const { encryptRecord } = await import('$lib/data/crypto');
|
||||
const { PREFERENCES_ID } = await import('$lib/modules/news/types');
|
||||
const diff = { onboardingCompleted: false, updatedAt: new Date().toISOString() };
|
||||
await encryptRecord('newsPreferences', diff);
|
||||
await preferencesTable.update(PREFERENCES_ID, diff);
|
||||
goto('/news');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>News-Einstellungen — Mana</title>
|
||||
</svelte:head>
|
||||
|
||||
<ListView />
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>News-Einstellungen</strong>
|
||||
<span class="sub">Themen · Sprachen · Gewichtungen</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="card">
|
||||
<h2>Themen</h2>
|
||||
<p class="hint">Welche Themen sollen im Feed auftauchen?</p>
|
||||
<div class="grid">
|
||||
{#each ALL_TOPICS as topic}
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
class:selected={prefs.selectedTopics.includes(topic)}
|
||||
onclick={() => toggleTopic(topic)}
|
||||
>
|
||||
<span class="emoji">{TOPIC_LABELS[topic].emoji}</span>
|
||||
<span>{TOPIC_LABELS[topic].de}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Sprachen</h2>
|
||||
<div class="row">
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
class:selected={prefs.preferredLanguages.includes('de')}
|
||||
onclick={() => toggleLang('de')}
|
||||
>
|
||||
🇩🇪 Deutsch
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pill"
|
||||
class:selected={prefs.preferredLanguages.includes('en')}
|
||||
onclick={() => toggleLang('en')}
|
||||
>
|
||||
🇬🇧 English
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Quellen</h2>
|
||||
<p class="hint">
|
||||
Du blockst aktuell <strong>{prefs.blockedSources.length}</strong> Quellen.
|
||||
</p>
|
||||
<a class="btn-link" href="/news/sources">Quellen verwalten →</a>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Gelernte Gewichtungen</h2>
|
||||
<p class="hint">
|
||||
Über Reaktionen lernt der Feed deine Vorlieben:
|
||||
{topicWeightCount} Themen-Gewichte, {sourceWeightCount} Quellen-Gewichte.
|
||||
</p>
|
||||
<button type="button" class="btn-secondary" onclick={resetWeights}>Zurücksetzen</button>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Onboarding</h2>
|
||||
<p class="hint">Themen, Sprachen und Quellen neu wählen.</p>
|
||||
<button type="button" class="btn-secondary" onclick={rerunOnboarding}>
|
||||
Onboarding neu starten
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pane {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.bar .title strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar .sub {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1.125rem;
|
||||
background: hsl(var(--color-card));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 10px;
|
||||
background: hsl(var(--color-background, var(--color-card)));
|
||||
border: 2px solid hsl(var(--color-border));
|
||||
color: hsl(var(--color-foreground));
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 120ms ease,
|
||||
border-color 120ms ease;
|
||||
}
|
||||
|
||||
.pill.selected {
|
||||
border-color: hsl(var(--color-primary, 230 80% 55%));
|
||||
background: hsl(var(--color-primary, 230 80% 55%) / 0.12);
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
align-self: flex-start;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--color-background, var(--color-card)));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
color: hsl(var(--color-foreground));
|
||||
font: inherit;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
align-self: flex-start;
|
||||
color: hsl(var(--color-primary, 230 80% 55%));
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,204 @@
|
|||
<!--
|
||||
/uload/settings — uLoad data overview, JSON export, clear-local-data.
|
||||
Reached via the ⚙ button in the uLoad module; not a workbench card.
|
||||
-->
|
||||
<script lang="ts">
|
||||
/**
|
||||
* /uload/settings — renders the workbench-card ListView.
|
||||
*/
|
||||
import ListView from '$lib/modules/uload-settings/ListView.svelte';
|
||||
import { Trash, DownloadSimple } from '@mana/shared-icons';
|
||||
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
import { useAllLinks, useAllTags, useAllFolders } from '$lib/modules/uload';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const links = useAllLinks();
|
||||
const tags = useAllTags();
|
||||
const folders = useAllFolders();
|
||||
|
||||
async function clearAllData() {
|
||||
if (!confirm('Alle lokalen uLoad-Daten löschen? Dies kann nicht rückgängig gemacht werden.'))
|
||||
return;
|
||||
|
||||
await linkTable.clear();
|
||||
await uloadTagTable.clear();
|
||||
await uloadFolderTable.clear();
|
||||
await linkTagTable.clear();
|
||||
toast.success('Alle uLoad-Daten gelöscht');
|
||||
}
|
||||
|
||||
async function exportData() {
|
||||
const rawLinks = await linkTable.toArray();
|
||||
const allLinks = await decryptRecords('links', rawLinks);
|
||||
const allTags = await uloadTagTable.toArray();
|
||||
const allFolders = await uloadFolderTable.toArray();
|
||||
const allLinkTags = await linkTagTable.toArray();
|
||||
|
||||
const data = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
links: allLinks,
|
||||
tags: allTags,
|
||||
folders: allFolders,
|
||||
linkTags: allLinkTags,
|
||||
};
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `uload-export-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success('Export heruntergeladen');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>uLoad-Einstellungen — Mana</title>
|
||||
</svelte:head>
|
||||
|
||||
<ListView />
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>uLoad-Einstellungen</strong>
|
||||
<span class="sub">Datenübersicht · Export · Gefahrenzone</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Daten</h2>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<p class="stat-value">{links.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Links</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<p class="stat-value">{tags.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Tags</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<p class="stat-value">{folders.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Ordner</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Daten exportieren</h2>
|
||||
<p class="hint">Alle Links, Tags und Ordner als JSON-Datei herunterladen.</p>
|
||||
<button type="button" class="btn" onclick={exportData}>
|
||||
<DownloadSimple size={16} />
|
||||
JSON exportieren
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="panel danger">
|
||||
<h2>Gefahrenzone</h2>
|
||||
<p class="hint">
|
||||
Löscht alle lokalen uLoad-Daten (Links, Tags, Ordner). Synchronisierte Daten auf dem Server
|
||||
bleiben erhalten.
|
||||
</p>
|
||||
<button type="button" class="btn danger" onclick={clearAllData}>
|
||||
<Trash size={16} />
|
||||
Alle Daten löschen
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.pane {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
color: hsl(var(--color-foreground));
|
||||
}
|
||||
|
||||
.bar .title strong {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bar .sub {
|
||||
margin-left: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: hsl(var(--color-card));
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 12px;
|
||||
padding: 1.125rem;
|
||||
}
|
||||
|
||||
.panel.danger {
|
||||
border-color: hsl(0 70% 55% / 0.3);
|
||||
background: hsl(0 70% 55% / 0.04);
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.panel.danger h2 {
|
||||
color: hsl(0 70% 55%);
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.875rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin: 0 0 0.875rem;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.75rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
margin: 0.25rem 0 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.875rem;
|
||||
border: 1px solid hsl(var(--color-border));
|
||||
border-radius: 8px;
|
||||
background: hsl(var(--color-background, var(--color-card)));
|
||||
color: hsl(var(--color-foreground));
|
||||
font: inherit;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background 120ms ease,
|
||||
color 120ms ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: hsl(var(--color-muted) / 0.5);
|
||||
}
|
||||
|
||||
.btn.danger {
|
||||
background: hsl(0 70% 55%);
|
||||
color: white;
|
||||
border-color: hsl(0 70% 45%);
|
||||
}
|
||||
|
||||
.btn.danger:hover {
|
||||
background: hsl(0 70% 50%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue