feat(m5a): channel manager, tabbed settings panel, per-user sync status
Backend: /api/channels (list + PATCH priority/hidden + attach/detach user tags), user-tag CRUD on /api/tags, /api/sync/my-status (per-user channel sync counts). Frontend: feed|channels page nav (URL-synced) from the account menu; a slide-in tabbed Settings panel (Appearance, Notifications=6b sound+duration, Sync status + actions + admin pause/resume, Account); a channel manager with priority, hide, per-channel user tags, sync badges and 'view in feed'. Notifications now honor the configurable sound + auto-dismiss settings.
This commit is contained in:
parent
1630a3d8c9
commit
b46c8300d1
11 changed files with 1063 additions and 45 deletions
|
|
@ -8,17 +8,20 @@ import {
|
|||
saveLocalTheme,
|
||||
type ThemePrefs,
|
||||
} from "./lib/theme";
|
||||
import { hasFilterParams, paramsToFilters, syncUrl } from "./lib/urlState";
|
||||
import { hasFilterParams, paramsToFilters, readPage, syncUrl, type Page } from "./lib/urlState";
|
||||
import {
|
||||
loadLayout,
|
||||
normalizeLayout,
|
||||
saveLayoutLocal,
|
||||
type SidebarLayout,
|
||||
} from "./lib/sidebarLayout";
|
||||
import { configureNotifications } from "./lib/notifications";
|
||||
import Login from "./components/Login";
|
||||
import Header from "./components/Header";
|
||||
import Sidebar from "./components/Sidebar";
|
||||
import Feed from "./components/Feed";
|
||||
import Channels from "./components/Channels";
|
||||
import SettingsPanel from "./components/SettingsPanel";
|
||||
import Toaster from "./components/Toaster";
|
||||
|
||||
const DEFAULT_FILTERS: FeedFilters = {
|
||||
|
|
@ -54,11 +57,18 @@ export default function App() {
|
|||
const [filters, setFiltersState] = useState<FeedFilters>(loadInitialFilters);
|
||||
const [view, setView] = useState<"grid" | "list">("grid");
|
||||
const [sidebarLayout, setSidebarLayoutState] = useState<SidebarLayout>(loadLayout);
|
||||
const [page, setPageState] = useState<Page>(readPage);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
|
||||
function setFilters(next: FeedFilters) {
|
||||
setFiltersState(next);
|
||||
localStorage.setItem(FILTERS_KEY, JSON.stringify(next));
|
||||
syncUrl(next);
|
||||
syncUrl(next, page);
|
||||
}
|
||||
|
||||
function setPage(next: Page) {
|
||||
setPageState(next);
|
||||
syncUrl(filters, next);
|
||||
}
|
||||
|
||||
function setSidebarLayout(next: SidebarLayout) {
|
||||
|
|
@ -69,9 +79,8 @@ export default function App() {
|
|||
|
||||
useEffect(() => applyTheme(theme), [theme]);
|
||||
|
||||
// Reflect the initial filters (from localStorage or URL) into the address bar
|
||||
// so the URL is always shareable, even before the first filter change.
|
||||
useEffect(() => syncUrl(filters), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
// Reflect the initial filters + page into the address bar so the URL is always shareable.
|
||||
useEffect(() => syncUrl(filters, page), []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const meQuery = useQuery({ queryKey: ["me"], queryFn: api.me });
|
||||
|
||||
|
|
@ -90,6 +99,7 @@ export default function App() {
|
|||
setSidebarLayoutState(l);
|
||||
saveLayoutLocal(l);
|
||||
}
|
||||
if (prefs.notifications) configureNotifications(prefs.notifications);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [meQuery.data?.id]);
|
||||
|
||||
|
|
@ -124,18 +134,42 @@ export default function App() {
|
|||
setFilters={setFilters}
|
||||
view={view}
|
||||
setView={changeView}
|
||||
page={page}
|
||||
setPage={setPage}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
/>
|
||||
<div className="flex flex-1 min-h-0">
|
||||
<Sidebar
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
layout={sidebarLayout}
|
||||
setLayout={setSidebarLayout}
|
||||
/>
|
||||
{page === "feed" && (
|
||||
<Sidebar
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
layout={sidebarLayout}
|
||||
setLayout={setSidebarLayout}
|
||||
/>
|
||||
)}
|
||||
<main className="flex-1 min-w-0 overflow-y-auto">
|
||||
<Feed filters={filters} setFilters={setFilters} view={view} />
|
||||
{page === "feed" ? (
|
||||
<Feed filters={filters} setFilters={setFilters} view={view} />
|
||||
) : (
|
||||
<Channels
|
||||
onViewChannel={(id, name) => {
|
||||
setFilters({ ...filters, channelId: id, channelName: name, show: "all" });
|
||||
setPage("feed");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
{settingsOpen && (
|
||||
<SettingsPanel
|
||||
me={meQuery.data!}
|
||||
theme={theme}
|
||||
setTheme={setTheme}
|
||||
view={view}
|
||||
setView={changeView}
|
||||
onClose={() => setSettingsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue