fix: R3 bugs - TipTap focus leak, Quick Capture palette, Calendar day view
This commit is contained in:
@@ -162,7 +162,6 @@ function EventForm({ event, onClose }: { event?: CalendarEvent; onClose: () => v
|
|||||||
function CalendarPage() {
|
function CalendarPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [date, setDate] = useState(new Date());
|
const [date, setDate] = useState(new Date());
|
||||||
const [view, setView] = useState<string>("month");
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
const [selectedEvent, setSelectedEvent] = useState<CalendarEvent | null>(null);
|
||||||
const [eventDetailOpen, setEventDetailOpen] = useState(false);
|
const [eventDetailOpen, setEventDetailOpen] = useState(false);
|
||||||
@@ -177,12 +176,7 @@ function CalendarPage() {
|
|||||||
return () => window.removeEventListener("resize", check);
|
return () => window.removeEventListener("resize", check);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Auto-switch to agenda on mobile
|
// Mobile: defaultView is set to agenda via isMobile check
|
||||||
useEffect(() => {
|
|
||||||
if (isMobile && view === "month") {
|
|
||||||
setView("agenda");
|
|
||||||
}
|
|
||||||
}, [isMobile]);
|
|
||||||
|
|
||||||
const { data: eventsData } = useApiQuery<{ items: CalendarEvent[]; totalItems: number }>(
|
const { data: eventsData } = useApiQuery<{ items: CalendarEvent[]; totalItems: number }>(
|
||||||
["calendar-events", date.toISOString()],
|
["calendar-events", date.toISOString()],
|
||||||
@@ -241,8 +235,9 @@ function CalendarPage() {
|
|||||||
setDate(newDate);
|
setDate(newDate);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const viewRef = useRef<string>("month");
|
||||||
const handleViewChange = useCallback((newView: string) => {
|
const handleViewChange = useCallback((newView: string) => {
|
||||||
setView(newView);
|
viewRef.current = newView;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -267,8 +262,7 @@ function CalendarPage() {
|
|||||||
startAccessor="start"
|
startAccessor="start"
|
||||||
endAccessor="end"
|
endAccessor="end"
|
||||||
date={date}
|
date={date}
|
||||||
view={view}
|
defaultView={isMobile ? Views.AGENDA : Views.MONTH}
|
||||||
defaultView={Views.MONTH}
|
|
||||||
onNavigate={handleNavigate}
|
onNavigate={handleNavigate}
|
||||||
onView={handleViewChange}
|
onView={handleViewChange}
|
||||||
onSelectEvent={handleSelectEvent}
|
onSelectEvent={handleSelectEvent}
|
||||||
@@ -281,7 +275,7 @@ function CalendarPage() {
|
|||||||
showMultiDayTimes
|
showMultiDayTimes
|
||||||
components={{
|
components={{
|
||||||
event: EventComponent,
|
event: EventComponent,
|
||||||
toolbar: (props: any) => <CustomToolbar {...props} currentView={view} />,
|
toolbar: (props: any) => <CustomToolbar {...props} />,
|
||||||
}}
|
}}
|
||||||
views={["month", "week", "work_week", "day", "agenda"]}
|
views={["month", "week", "work_week", "day", "agenda"]}
|
||||||
step={30}
|
step={30}
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ function QuickCaptureWidget() {
|
|||||||
});
|
});
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
if (!text.trim()) return;
|
if (!text.trim()) return;
|
||||||
if (type === "task") createTask.mutate(text.trim());
|
if (type === "task") createTask.mutate(text.trim());
|
||||||
else createNote.mutate(text.trim());
|
else createNote.mutate(text.trim());
|
||||||
@@ -221,7 +222,7 @@ function QuickCaptureWidget() {
|
|||||||
</Select>
|
</Select>
|
||||||
<Input placeholder="Quick capture..." value={text} onChange={(e) => setText(e.target.value)} className="h-9" />
|
<Input placeholder="Quick capture..." value={text} onChange={(e) => setText(e.target.value)} className="h-9" />
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" size="sm" className="h-9" disabled={!text.trim() || createTask.isPending || createNote.isPending}>Add</Button>
|
<Button type="button" size="sm" className="h-9" disabled={!text.trim() || createTask.isPending || createNote.isPending} onClick={handleSubmit}>Add</Button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useRef, useEffect } from "react";
|
import { useState, useCallback, useRef, useEffect, memo } from "react";
|
||||||
import { createRoute } from "@tanstack/react-router";
|
import { createRoute } from "@tanstack/react-router";
|
||||||
import { Route as appRoute } from "../_app";
|
import { Route as appRoute } from "../_app";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -17,7 +17,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import type { Note, PaginatedResponse } from "@/lib/types";
|
import type { Note, PaginatedResponse } from "@/lib/types";
|
||||||
|
|
||||||
// Simple TipTap-like editor using contentEditable
|
// Simple TipTap-like editor using contentEditable
|
||||||
function NoteEditor({ content, onChange, placeholder = "Start writing..." }: { content: string; onChange: (html: string) => void; placeholder?: string }) {
|
const NoteEditor = memo(function NoteEditor({ content, onChange, placeholder = "Start writing..." }: { content: string; onChange: (html: string) => void; placeholder?: string }) {
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
const [isPlaceholder, setIsPlaceholder] = useState(!content);
|
const [isPlaceholder, setIsPlaceholder] = useState(!content);
|
||||||
|
|
||||||
@@ -47,14 +47,14 @@ function NoteEditor({ content, onChange, placeholder = "Start writing..." }: { c
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
function NotesPage() {
|
function NotesPage() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedNote, setSelectedNote] = useState<Note | null>(null);
|
const [selectedNote, setSelectedNote] = useState<Note | null>(null);
|
||||||
const [editorContent, setEditorContent] = useState("");
|
const [editorContent, setEditorContent] = useState("");
|
||||||
const [saveTimer, setSaveTimer] = useState<ReturnType<typeof setTimeout> | null>(null);
|
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const [showBacklinks, setShowBacklinks] = useState(false);
|
const [showBacklinks, setShowBacklinks] = useState(false);
|
||||||
const [showVersions, setShowVersions] = useState(false);
|
const [showVersions, setShowVersions] = useState(false);
|
||||||
|
|
||||||
@@ -104,14 +104,13 @@ function NotesPage() {
|
|||||||
|
|
||||||
const handleContentChange = useCallback((html: string) => {
|
const handleContentChange = useCallback((html: string) => {
|
||||||
setEditorContent(html);
|
setEditorContent(html);
|
||||||
if (saveTimer) clearTimeout(saveTimer);
|
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
|
||||||
const timer = setTimeout(() => {
|
saveTimerRef.current = setTimeout(() => {
|
||||||
if (selectedNote) {
|
if (selectedNote) {
|
||||||
updateMutation.mutate({ id: selectedNote.id, data: { content: html } });
|
updateMutation.mutate({ id: selectedNote.id, data: { content: html } });
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
setSaveTimer(timer);
|
}, [selectedNote, updateMutation]);
|
||||||
}, [selectedNote, updateMutation, saveTimer]);
|
|
||||||
|
|
||||||
const handleTitleChange = (title: string) => {
|
const handleTitleChange = (title: string) => {
|
||||||
if (selectedNote) {
|
if (selectedNote) {
|
||||||
|
|||||||
Reference in New Issue
Block a user