feat: Phase 3 - filter bar, keyboard navigation, notifications, comments
- FilterBar component with text search, label chips, due date and priority dropdowns - "/" keyboard shortcut and toolbar button to toggle filter bar - Keyboard card navigation with J/K/H/L keys, Enter to open, Escape to clear - Focus ring on keyboard-selected cards with auto-scroll - Desktop notifications for due/overdue cards via tauri-plugin-notification - CommentsSection component with add/delete and relative timestamps - Filtered card count display in column headers
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { create } from "zustand";
|
||||
import type { AppSettings } from "@/types/settings";
|
||||
import type { AppSettings, BoardSortOrder } from "@/types/settings";
|
||||
import type { BoardMeta, ColumnWidth } from "@/types/board";
|
||||
import { loadSettings, saveSettings, listBoards, ensureDataDirs } from "@/lib/storage";
|
||||
import { loadSettings, saveSettings, listBoards, ensureDataDirs, loadBoard } from "@/lib/storage";
|
||||
import { isPermissionGranted, requestPermission, sendNotification } from "@tauri-apps/plugin-notification";
|
||||
|
||||
export type View = { type: "board-list" } | { type: "board"; boardId: string };
|
||||
|
||||
@@ -20,6 +21,9 @@ interface AppState {
|
||||
setView: (view: View) => void;
|
||||
refreshBoards: () => Promise<void>;
|
||||
addRecentBoard: (boardId: string) => void;
|
||||
setBoardSortOrder: (order: BoardSortOrder) => void;
|
||||
setBoardManualOrder: (ids: string[]) => void;
|
||||
getSortedBoards: () => BoardMeta[];
|
||||
}
|
||||
|
||||
function applyTheme(theme: AppSettings["theme"]): void {
|
||||
@@ -63,6 +67,9 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
density: "comfortable",
|
||||
defaultColumnWidth: "standard",
|
||||
windowState: null,
|
||||
boardSortOrder: "updated",
|
||||
boardManualOrder: [],
|
||||
lastNotificationCheck: null,
|
||||
},
|
||||
boards: [],
|
||||
view: { type: "board-list" },
|
||||
@@ -75,6 +82,45 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
set({ settings, boards, initialized: true });
|
||||
applyTheme(settings.theme);
|
||||
applyAppearance(settings);
|
||||
|
||||
// Due date notifications (once per hour)
|
||||
const lastCheck = settings.lastNotificationCheck;
|
||||
const hourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
||||
if (!lastCheck || lastCheck < hourAgo) {
|
||||
try {
|
||||
let granted = await isPermissionGranted();
|
||||
if (!granted) {
|
||||
const perm = await requestPermission();
|
||||
granted = perm === "granted";
|
||||
}
|
||||
if (granted) {
|
||||
let dueToday = 0;
|
||||
let overdue = 0;
|
||||
const today = new Date();
|
||||
const todayStr = today.toDateString();
|
||||
|
||||
for (const meta of boards) {
|
||||
try {
|
||||
const board = await loadBoard(meta.id);
|
||||
for (const card of Object.values(board.cards)) {
|
||||
if (!card.dueDate) continue;
|
||||
const due = new Date(card.dueDate);
|
||||
if (due.toDateString() === todayStr) dueToday++;
|
||||
else if (due < today) overdue++;
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
|
||||
if (dueToday > 0) {
|
||||
sendNotification({ title: "OpenPylon", body: `You have ${dueToday} card${dueToday > 1 ? "s" : ""} due today` });
|
||||
}
|
||||
if (overdue > 0) {
|
||||
sendNotification({ title: "OpenPylon", body: `You have ${overdue} overdue card${overdue > 1 ? "s" : ""}` });
|
||||
}
|
||||
}
|
||||
updateAndSave(get, set, { lastNotificationCheck: new Date().toISOString() });
|
||||
} catch { /* notification plugin not available */ }
|
||||
}
|
||||
},
|
||||
|
||||
setTheme: (theme) => {
|
||||
@@ -117,4 +163,44 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
].slice(0, 10);
|
||||
updateAndSave(get, set, { recentBoardIds: recent });
|
||||
},
|
||||
|
||||
setBoardSortOrder: (boardSortOrder) => {
|
||||
// When switching to manual for the first time, snapshot current order
|
||||
if (boardSortOrder === "manual" && get().settings.boardManualOrder.length === 0) {
|
||||
const currentSorted = get().getSortedBoards();
|
||||
updateAndSave(get, set, {
|
||||
boardSortOrder,
|
||||
boardManualOrder: currentSorted.map((b) => b.id),
|
||||
});
|
||||
} else {
|
||||
updateAndSave(get, set, { boardSortOrder });
|
||||
}
|
||||
},
|
||||
|
||||
setBoardManualOrder: (boardManualOrder) => {
|
||||
updateAndSave(get, set, { boardManualOrder });
|
||||
},
|
||||
|
||||
getSortedBoards: () => {
|
||||
const { boards, settings } = get();
|
||||
const order = settings.boardSortOrder;
|
||||
|
||||
if (order === "manual") {
|
||||
const manualOrder = settings.boardManualOrder;
|
||||
const orderMap = new Map(manualOrder.map((id, i) => [id, i]));
|
||||
return [...boards].sort((a, b) => {
|
||||
const ai = orderMap.get(a.id) ?? Infinity;
|
||||
const bi = orderMap.get(b.id) ?? Infinity;
|
||||
return ai - bi;
|
||||
});
|
||||
}
|
||||
if (order === "title") {
|
||||
return [...boards].sort((a, b) => a.title.localeCompare(b.title));
|
||||
}
|
||||
if (order === "created") {
|
||||
return [...boards].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
}
|
||||
// "updated" — default, already sorted from listBoards
|
||||
return boards;
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user