Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ebc43ef72 | |||
| b89e6ca2c3 | |||
| 3cbbc65c5c | |||
| b276345149 |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tasgrid",
|
"name": "tasgrid",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "1.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host 0.0.0.0 --port 4001",
|
"dev": "vite --host 0.0.0.0 --port 4001",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { type Component, createEffect, onCleanup } from 'solid-js';
|
import { type Component, createEffect, onCleanup } from 'solid-js';
|
||||||
import { useRegisterSW } from 'virtual:pwa-register/solid';
|
import { useRegisterSW } from 'virtual:pwa-register/solid';
|
||||||
|
import { toast } from 'solid-sonner';
|
||||||
|
|
||||||
// We export a function so other parts of the app can manually trigger an update
|
// We export a function so other parts of the app can manually trigger an update
|
||||||
let manualUpdateFn: (() => void) | undefined;
|
let manualUpdateFn: (() => void) | undefined;
|
||||||
|
let updateAndReloadFn: (() => void) | undefined;
|
||||||
|
|
||||||
export const forceServiceWorkerUpdate = () => {
|
export const forceServiceWorkerUpdate = () => {
|
||||||
if (manualUpdateFn) {
|
if (manualUpdateFn) {
|
||||||
@@ -12,6 +14,15 @@ export const forceServiceWorkerUpdate = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateAndReloadApp = () => {
|
||||||
|
if (updateAndReloadFn) {
|
||||||
|
updateAndReloadFn();
|
||||||
|
} else {
|
||||||
|
console.warn("PWA Updater not initialized yet; reloading directly.");
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const PwaUpdater: Component = () => {
|
export const PwaUpdater: Component = () => {
|
||||||
let registration: ServiceWorkerRegistration | undefined;
|
let registration: ServiceWorkerRegistration | undefined;
|
||||||
|
|
||||||
@@ -47,6 +58,33 @@ export const PwaUpdater: Component = () => {
|
|||||||
registration.update();
|
registration.update();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateAndReloadFn = () => {
|
||||||
|
if (needRefresh()) {
|
||||||
|
console.log('Update ready. Reloading now...');
|
||||||
|
updateServiceWorker(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registration) {
|
||||||
|
console.log('Checking for updates before reload...');
|
||||||
|
registration.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = Date.now();
|
||||||
|
const timeoutMs = 5000;
|
||||||
|
const poll = window.setInterval(() => {
|
||||||
|
if (needRefresh()) {
|
||||||
|
console.log('Update found. Reloading with new SW...');
|
||||||
|
window.clearInterval(poll);
|
||||||
|
updateServiceWorker(true);
|
||||||
|
} else if (Date.now() - start > timeoutMs) {
|
||||||
|
console.log('No update detected quickly. Reloading now...');
|
||||||
|
window.clearInterval(poll);
|
||||||
|
toast.info("No update found");
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type Component } from "solid-js";
|
import { type Component, For } from "solid-js";
|
||||||
import { CheckCircle2 } from "lucide-solid";
|
import { CheckCircle2 } from "lucide-solid";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
@@ -6,6 +6,7 @@ interface StatusCircleProps {
|
|||||||
status: number;
|
status: number;
|
||||||
size?: number;
|
size?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
recurrenceProgress?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatusCircle: Component<StatusCircleProps> = (props) => {
|
export const StatusCircle: Component<StatusCircleProps> = (props) => {
|
||||||
@@ -25,10 +26,39 @@ export const StatusCircle: Component<StatusCircleProps> = (props) => {
|
|||||||
return circumference() * (1 - percentage);
|
return circumference() * (1 - percentage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const dotCount = () => 12;
|
||||||
|
const dotRadius = () => Math.max(1.4, size() * 0.06);
|
||||||
|
const dotRingRadius = () => radius() - 1;
|
||||||
|
const filledDots = () => {
|
||||||
|
const progress = props.recurrenceProgress ?? 0;
|
||||||
|
return Math.floor(Math.max(0, Math.min(1, progress)) * dotCount());
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={cn("relative flex items-center justify-center", props.className)} style={{ width: `${size()}px`, height: `${size()}px` }}>
|
<div class={cn("relative flex items-center justify-center", props.className)} style={{ width: `${size()}px`, height: `${size()}px` }}>
|
||||||
{props.status >= 10 ? (
|
{props.status >= 10 ? (
|
||||||
<CheckCircle2 size={size()} class="text-green-500" />
|
props.recurrenceProgress != null ? (
|
||||||
|
<svg width={size()} height={size()}>
|
||||||
|
<For each={Array.from({ length: dotCount() })}>
|
||||||
|
{(_, index) => {
|
||||||
|
const angle = (index() / dotCount()) * Math.PI * 2 - Math.PI / 2;
|
||||||
|
const cx = size() / 2 + Math.cos(angle) * dotRingRadius();
|
||||||
|
const cy = size() / 2 + Math.sin(angle) * dotRingRadius();
|
||||||
|
const isFilled = index() < filledDots();
|
||||||
|
return (
|
||||||
|
<circle
|
||||||
|
cx={cx}
|
||||||
|
cy={cy}
|
||||||
|
r={dotRadius()}
|
||||||
|
class={isFilled ? "fill-green-500" : "fill-muted-foreground/30"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<CheckCircle2 size={size()} class="text-green-500" />
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
// Progress Circle
|
// Progress Circle
|
||||||
<div class="relative group">
|
<div class="relative group">
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export const TagPicker: Component<TagPickerProps> = (props) => {
|
|||||||
const allTagNames = () => {
|
const allTagNames = () => {
|
||||||
const definedTags = store.tagDefinitions.filter(d => !d.name.endsWith("_favorite__")).map(d => d.name);
|
const definedTags = store.tagDefinitions.filter(d => !d.name.endsWith("_favorite__")).map(d => d.name);
|
||||||
const bucketTags = store.buckets.map(b => `@${b.name}`);
|
const bucketTags = store.buckets.map(b => `@${b.name}`);
|
||||||
const noteTags = store.notes.map(n => `#${n.title}`);
|
const noteTags = store.notes
|
||||||
|
.filter(n => !(n.tags || []).some(t => t.startsWith("child-of-")))
|
||||||
|
.map(n => `#${n.title}`);
|
||||||
return [...new Set([...definedTags, ...bucketTags, ...noteTags])].sort();
|
return [...new Set([...definedTags, ...bucketTags, ...noteTags])].sort();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTheme } from "./ThemeProvider";
|
|||||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select";
|
||||||
import { getStatusOptionsForTask } from "@/lib/constants";
|
import { getStatusOptionsForTask } from "@/lib/constants";
|
||||||
|
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||||
import { StatusCircle } from "./StatusCircle";
|
import { StatusCircle } from "./StatusCircle";
|
||||||
|
|
||||||
export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props) => {
|
export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props) => {
|
||||||
@@ -54,6 +55,10 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
|||||||
});
|
});
|
||||||
|
|
||||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||||
|
const recurrenceProgress = createMemo(() => {
|
||||||
|
if (props.task.status !== 10) return null;
|
||||||
|
return getRecurrenceProgress(props.task.recurrence, props.task.updated, now());
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -93,7 +98,7 @@ export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props)
|
|||||||
updateTask(props.task.id, { status: 10 });
|
updateTask(props.task.id, { status: 10 });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StatusCircle status={props.task.status ?? 0} size={26} />
|
<StatusCircle status={props.task.status ?? 0} size={26} recurrenceProgress={recurrenceProgress()} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent />
|
<SelectContent />
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type Component, createEffect, createSignal, For, createMemo, onCleanup } from "solid-js";
|
import { type Component, createEffect, createSignal, For, createMemo, onCleanup } from "solid-js";
|
||||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||||
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag } from "@/store";
|
import { type Task, removeTask, restoreTask, updateTask, saveTaskAsTemplate, shareTask, revokeShare, splitTask, loadTaskContent, currentTaskContext, cleanupTaskAttachments, getFavoriteTag, now } from "@/store";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||||
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings, Star, Image as ImageIcon, Film, FileText } from "lucide-solid";
|
import { ArrowUpCircle, Clock, Calendar, Type, Trash2, X, Copy, MoreHorizontal, Gauge, Share2, UserMinus, GitBranch, Tag, Settings, Star, Image as ImageIcon, Film, FileText } from "lucide-solid";
|
||||||
@@ -17,6 +17,7 @@ import { lazy, Suspense } from "solid-js";
|
|||||||
import { useTheme } from "./ThemeProvider";
|
import { useTheme } from "./ThemeProvider";
|
||||||
import { getThemeAdjustedColor } from "@/lib/colors";
|
import { getThemeAdjustedColor } from "@/lib/colors";
|
||||||
import { pb } from "@/lib/pocketbase";
|
import { pb } from "@/lib/pocketbase";
|
||||||
|
import { getRecurrenceProgress } from "@/lib/recurrence";
|
||||||
|
|
||||||
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
const TaskEditor = lazy(() => import("./TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||||
|
|
||||||
@@ -208,6 +209,10 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
const statusOptions = createMemo(() => getStatusOptionsForTask(props.task.recurrence));
|
||||||
|
const recurrenceProgress = createMemo(() => {
|
||||||
|
if (props.task.status !== 10) return null;
|
||||||
|
return getRecurrenceProgress(props.task.recurrence, props.task.updated, now());
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={props.isOpen} onOpenChange={(open) => {
|
<Sheet open={props.isOpen} onOpenChange={(open) => {
|
||||||
@@ -251,7 +256,7 @@ export const TaskDetail: Component<TaskDetailProps> = (props) => {
|
|||||||
updateStatus("10");
|
updateStatus("10");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<StatusCircle status={props.task.status ?? 0} size={28} />
|
<StatusCircle status={props.task.status ?? 0} size={28} recurrenceProgress={recurrenceProgress()} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent />
|
<SelectContent />
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const noteSuggestion = {
|
|||||||
char: '#',
|
char: '#',
|
||||||
items: ({ query }: { query: string }) => {
|
items: ({ query }: { query: string }) => {
|
||||||
return store.notes
|
return store.notes
|
||||||
|
.filter(note => !(note.tags || []).some(t => t.startsWith("child-of-")))
|
||||||
.filter(note => note.title.toLowerCase().includes(query.toLowerCase()))
|
.filter(note => note.title.toLowerCase().includes(query.toLowerCase()))
|
||||||
.map(note => ({
|
.map(note => ({
|
||||||
id: note.id,
|
id: note.id,
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
export type Recurrence = {
|
||||||
|
type: 'daily' | 'weekly' | 'monthly';
|
||||||
|
days?: number[];
|
||||||
|
dayOfMonth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||||
|
|
||||||
|
const addDays = (date: Date, days: number) => {
|
||||||
|
const d = new Date(date);
|
||||||
|
d.setDate(d.getDate() + days);
|
||||||
|
return d;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextDaily = (completedAt: Date) => addDays(startOfDay(completedAt), 1);
|
||||||
|
|
||||||
|
const getNextWeekly = (completedAt: Date, days: number[]) => {
|
||||||
|
if (!days || days.length === 0) return null;
|
||||||
|
const base = startOfDay(completedAt);
|
||||||
|
for (let offset = 1; offset <= 7; offset++) {
|
||||||
|
const candidate = addDays(base, offset);
|
||||||
|
if (days.includes(candidate.getDay())) return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNextMonthly = (completedAt: Date, dayOfMonth?: number) => {
|
||||||
|
if (!dayOfMonth || dayOfMonth < 1 || dayOfMonth > 31) return null;
|
||||||
|
const base = startOfDay(completedAt);
|
||||||
|
const baseDay = base.getDate();
|
||||||
|
const startOffset = dayOfMonth > baseDay ? 0 : 1;
|
||||||
|
|
||||||
|
for (let offset = startOffset; offset <= 12; offset++) {
|
||||||
|
const year = base.getFullYear();
|
||||||
|
const monthIndex = base.getMonth() + offset;
|
||||||
|
const candidate = new Date(year, monthIndex, dayOfMonth);
|
||||||
|
if (candidate.getMonth() === (monthIndex % 12 + 12) % 12) {
|
||||||
|
if (candidate.getTime() > completedAt.getTime()) {
|
||||||
|
return startOfDay(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNextRecurrenceDate = (recurrence: Recurrence, completedAt: Date) => {
|
||||||
|
if (recurrence.type === 'daily') return getNextDaily(completedAt);
|
||||||
|
if (recurrence.type === 'weekly') return getNextWeekly(completedAt, recurrence.days || []);
|
||||||
|
if (recurrence.type === 'monthly') return getNextMonthly(completedAt, recurrence.dayOfMonth);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRecurrenceProgress = (
|
||||||
|
recurrence: Recurrence | null | undefined,
|
||||||
|
completedAtIso: string | null | undefined,
|
||||||
|
nowMs: number
|
||||||
|
) => {
|
||||||
|
if (!recurrence || !completedAtIso) return null;
|
||||||
|
const completedAt = new Date(completedAtIso);
|
||||||
|
if (isNaN(completedAt.getTime())) return null;
|
||||||
|
const next = getNextRecurrenceDate(recurrence, completedAt);
|
||||||
|
if (!next) return null;
|
||||||
|
const total = next.getTime() - completedAt.getTime();
|
||||||
|
if (total <= 0) return null;
|
||||||
|
const progress = (nowMs - completedAt.getTime()) / total;
|
||||||
|
return Math.max(0, Math.min(1, progress));
|
||||||
|
};
|
||||||
+70
-20
@@ -23,6 +23,8 @@ export const NotepadView: Component<{
|
|||||||
const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(false);
|
const [isLinkedTasksOpen, setIsLinkedTasksOpen] = createSignal(false);
|
||||||
const [isDragging, setIsDragging] = createSignal(false);
|
const [isDragging, setIsDragging] = createSignal(false);
|
||||||
const [editorInstance, setEditorInstance] = createSignal<any>(null);
|
const [editorInstance, setEditorInstance] = createSignal<any>(null);
|
||||||
|
const [noteActionsOpen, setNoteActionsOpen] = createSignal(false);
|
||||||
|
const [confirmDeleteOpen, setConfirmDeleteOpen] = createSignal(false);
|
||||||
|
|
||||||
const currentUserId = pb.authStore.model?.id;
|
const currentUserId = pb.authStore.model?.id;
|
||||||
|
|
||||||
@@ -332,13 +334,16 @@ export const NotepadView: Component<{
|
|||||||
const isOwner = note().user === currentUserId;
|
const isOwner = note().user === currentUserId;
|
||||||
const canEdit = isOwner || !note().isPrivate;
|
const canEdit = isOwner || !note().isPrivate;
|
||||||
|
|
||||||
const hasTopViewer = createMemo(() => {
|
const hasTopViewer = createMemo(() => {
|
||||||
const c = note()?.content;
|
const c = note()?.content;
|
||||||
if (!c) return false;
|
if (!c) return false;
|
||||||
const s = c.trim();
|
const s = c.trim();
|
||||||
return s.startsWith('<div data-type="file-viewer-wrapper"') ||
|
return s.startsWith('<div data-type="file-viewer-wrapper"') ||
|
||||||
(s.startsWith('<div data-type="file-attachment"') && s.includes('data-filename') && s.toLowerCase().includes('.pdf'));
|
(s.startsWith('<div data-type="file-attachment"') && s.includes('data-filename') && s.toLowerCase().includes('.pdf'));
|
||||||
});
|
});
|
||||||
|
const isChildNote = createMemo(() => {
|
||||||
|
return (note()?.tags || []).some(t => t.startsWith("child-of-"));
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -707,11 +712,17 @@ export const NotepadView: Component<{
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Sticky Footer Actions (Match TaskDetail style) */}
|
{/* Sticky Footer Actions (Match TaskDetail style) */}
|
||||||
<div class="px-4 py-3 border-t border-border/50 md:hidden flex items-center justify-between shrink-0 bg-background/95 backdrop-blur-sm z-40 w-full sticky bottom-0">
|
<div class="px-4 py-4 border-t border-border/50 md:hidden flex items-center justify-between shrink-0 bg-background/95 backdrop-blur-sm z-40 w-full sticky bottom-0">
|
||||||
<div class="flex items-center gap-2 w-16 shrink-0">
|
<div class="flex items-center gap-2 w-16 shrink-0">
|
||||||
{/* "..." popover menu containing Public/Private toggle and Delete */}
|
{/* "..." popover menu containing Public/Private toggle and Delete */}
|
||||||
<Show when={isOwner}>
|
<Show when={isOwner}>
|
||||||
<Popover>
|
<Popover
|
||||||
|
open={noteActionsOpen()}
|
||||||
|
onOpenChange={(nextOpen) => {
|
||||||
|
setNoteActionsOpen(nextOpen);
|
||||||
|
if (!nextOpen) setConfirmDeleteOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PopoverTrigger
|
<PopoverTrigger
|
||||||
as={Button}
|
as={Button}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@@ -735,18 +746,57 @@ export const NotepadView: Component<{
|
|||||||
<><Lock size={14} /> Private</>
|
<><Lock size={14} /> Private</>
|
||||||
</Show>
|
</Show>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Show
|
||||||
variant="ghost"
|
when={!confirmDeleteOpen() || isChildNote()}
|
||||||
size="sm"
|
fallback={
|
||||||
class="w-full justify-start text-[0.625rem] font-bold uppercase tracking-widest gap-2 hover:bg-destructive/10 hover:text-destructive text-muted-foreground transition-all"
|
<div class="rounded-lg border border-border/50 bg-muted/30 p-2 text-[0.625rem] uppercase tracking-widest text-muted-foreground flex flex-col gap-2">
|
||||||
onClick={(e) => {
|
<span>Delete note? This cannot be undone.</span>
|
||||||
e.stopPropagation();
|
<div class="flex gap-2">
|
||||||
handleDeleteNote(note().id);
|
<Button
|
||||||
}}
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="flex-1 h-7 text-[0.625rem] uppercase tracking-widest"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setConfirmDeleteOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="flex-1 h-7 text-[0.625rem] uppercase tracking-widest hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteNote(note().id);
|
||||||
|
setConfirmDeleteOpen(false);
|
||||||
|
setNoteActionsOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Trash2 size={14} />
|
<Button
|
||||||
Delete
|
variant="ghost"
|
||||||
</Button>
|
size="sm"
|
||||||
|
class="w-full justify-start text-[0.625rem] font-bold uppercase tracking-widest gap-2 hover:bg-destructive/10 hover:text-destructive text-muted-foreground transition-all"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (isChildNote()) {
|
||||||
|
handleDeleteNote(note().id);
|
||||||
|
} else {
|
||||||
|
setConfirmDeleteOpen(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
{isChildNote() ? "Delete Tab" : "Delete Note"}
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|||||||
+44
-14
@@ -1088,20 +1088,50 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="pt-40 pb-20 flex flex-col items-center gap-4">
|
<footer class="pt-40 pb-20 flex flex-col items-center gap-4">
|
||||||
<Button
|
<div class="flex items-center gap-2">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
class="h-8 gap-2 text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground opacity-30 hover:opacity-100 transition-opacity"
|
size="sm"
|
||||||
onClick={async () => {
|
class="h-8 gap-2 text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground opacity-70 hover:opacity-100 transition-opacity"
|
||||||
const { forceServiceWorkerUpdate } = await import('@/components/PwaUpdater');
|
onClick={async () => {
|
||||||
forceServiceWorkerUpdate();
|
const { updateAndReloadApp } = await import('@/components/PwaUpdater');
|
||||||
toast.success("Checking for updates...");
|
toast.success("Updating app...");
|
||||||
}}
|
updateAndReloadApp();
|
||||||
>
|
}}
|
||||||
<Upload class="rotate-180" size={14} />
|
>
|
||||||
Check for Updates
|
<Upload class="rotate-180" size={14} />
|
||||||
</Button>
|
Update App
|
||||||
<p class="text-[0.5rem] font-mono text-muted-foreground/30 uppercase tracking-[0.2em]">Tasgrid v1.0.1</p> {/* Versioning System: v[breaking update].[user-facing update].[patch/backend updated] */}
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 gap-2 text-[0.625rem] font-bold uppercase tracking-widest text-muted-foreground hover:text-foreground opacity-70 hover:opacity-100 transition-opacity"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!confirm("Reset PWA? This will clear offline data and reload.")) return;
|
||||||
|
try {
|
||||||
|
const registrations = await navigator.serviceWorker?.getRegistrations?.();
|
||||||
|
if (registrations) {
|
||||||
|
await Promise.all(registrations.map(r => r.unregister()));
|
||||||
|
}
|
||||||
|
if ('caches' in window) {
|
||||||
|
const keys = await caches.keys();
|
||||||
|
await Promise.all(keys.map(k => caches.delete(k)));
|
||||||
|
}
|
||||||
|
toast.success("PWA reset. Reloading...");
|
||||||
|
window.location.reload();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to reset PWA", err);
|
||||||
|
toast.error("Failed to reset PWA");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
Reset PWA
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p class="text-[0.5rem] font-mono text-muted-foreground/60 uppercase tracking-[0.2em]">
|
||||||
|
Tasgrid v{__APP_VERSION__}
|
||||||
|
</p> {/* Versioning System: v[breaking update].[user-facing update].[patch/backend updated] */}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare const __APP_VERSION__: string;
|
||||||
+9
-1
@@ -3,8 +3,16 @@ import solid from 'vite-plugin-solid'
|
|||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(
|
||||||
|
readFileSync(new URL('./package.json', import.meta.url), 'utf-8')
|
||||||
|
);
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(packageJson.version),
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
solid(),
|
solid(),
|
||||||
tailwindcss(),
|
tailwindcss(),
|
||||||
@@ -76,4 +84,4 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "./src")
|
"@": path.resolve(__dirname, "./src")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user