fix: restore sync spinner, fix task hover menu, and add custom motivational quote sources

- Reverted RefreshCcw icon to original CSS loading spinner

- Fixed GridTaskBlock overflow to restore task actions menu visibility

- Implemented motivationalQuote state and fetcher with custom API source setting

- Removed redundant Goal of the Week setting from panel
This commit is contained in:
mARTin 2026-02-23 21:53:17 +01:00
parent f9d578ddf1
commit 6fed420375
2 changed files with 90 additions and 52 deletions

View File

@ -180,7 +180,7 @@ export function GridTaskBlock({
boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none", boxShadow: (isNotesOpen || isSubTasksOpen || isResizing) ? "0 1px 3px rgba(0,0,0,0.05)" : "none",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
overflow: isNotesOpen || isSubTasksOpen ? "visible" : "hidden", overflow: "visible",
}} }}
draggable={!editingTaskId && !isResizing} draggable={!editingTaskId && !isResizing}
onDragStart={(e) => handleDragStart(e, task)} onDragStart={(e) => handleDragStart(e, task)}

View File

@ -539,6 +539,7 @@ export default function WeeklyView() {
todayHighlightColor?: string; todayHighlightColor?: string;
pastDayColor?: string; pastDayColor?: string;
goalFallbackType?: "quote" | "next_todo" | "default"; goalFallbackType?: "quote" | "next_todo" | "default";
quoteSourceUrl?: string;
goalDefaultSentence?: string; goalDefaultSentence?: string;
goalFontFamily?: string; goalFontFamily?: string;
goalFontSize?: string; goalFontSize?: string;
@ -607,7 +608,9 @@ export default function WeeklyView() {
weekendColorSat: "#666666", weekendColorSat: "#666666",
weekendColorSun: "#dc2626", weekendColorSun: "#dc2626",
pastDayColor: "#a6a6a7", pastDayColor: "#a6a6a7",
quoteSourceUrl: "https://recite.vercel.app/api/random",
}); });
const [motivationalQuote, setMotivationalQuote] = useState("");
const [showSummary, setShowSummary] = useState(false); const [showSummary, setShowSummary] = useState(false);
const [isAddingSomedayList, setIsAddingSomedayList] = useState(false); const [isAddingSomedayList, setIsAddingSomedayList] = useState(false);
@ -940,14 +943,43 @@ export default function WeeklyView() {
} }
}; };
const fetchMotivationalQuote = useCallback(async () => {
if (profile.goalFallbackType !== "quote") return;
try {
const url = profile.quoteSourceUrl || "https://recite.vercel.app/api/random";
const res = await fetch(url);
if (!res.ok) throw new Error("Failed to fetch quote");
const data = await res.json();
// Handle different JSON formats (array or object)
let quoteText = "";
if (Array.isArray(data) && data.length > 0) {
const item = data[0];
quoteText = item.quote || item.text || item.content || (typeof item === 'string' ? item : "");
if (item.author) quoteText += ` - ${item.author}`;
} else if (data && typeof data === 'object') {
quoteText = data.quote || data.text || data.content || "";
if (data.author) quoteText += ` - ${data.author}`;
} else if (typeof data === 'string') {
quoteText = data;
}
if (quoteText) setMotivationalQuote(quoteText);
} catch (error) {
console.error("Error fetching motivational quote:", error);
setMotivationalQuote("Stay focused and productive.");
}
}, [profile.goalFallbackType, profile.quoteSourceUrl]);
// Fetch tasks on mount // Fetch tasks on mount
useEffect(() => { useEffect(() => {
if (session) { if (session) {
fetchTasks(); fetchTasks();
fetchConnections(); fetchConnections();
fetchCalendarEvents(); fetchCalendarEvents();
fetchMotivationalQuote();
} }
}, [session]); }, [session, fetchMotivationalQuote]);
// Periodic pull-sync from Google Tasks (every 2 minutes) // Periodic pull-sync from Google Tasks (every 2 minutes)
useEffect(() => { useEffect(() => {
@ -1104,18 +1136,21 @@ export default function WeeklyView() {
setGoal(newGoal); setGoal(newGoal);
try { try {
await fetch("/api/goal", { await fetch("/api/goal", {
method: "PUT", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: JSON.stringify({
weekStart: goalDateKey, weekStart: goalDateKey,
text: newGoal, goal: newGoal,
scope: profile.goalScope || "week",
}), }),
}); });
} catch (err) { } catch (error) {
console.error("Failed to save goal:", err); console.error("Error saving goal:", error);
} }
}; };
const handleSomedayWheel = (e: React.WheelEvent) => { const handleSomedayWheel = (e: React.WheelEvent) => {
if (e.currentTarget) { if (e.currentTarget) {
e.currentTarget.scrollLeft += e.deltaY; e.currentTarget.scrollLeft += e.deltaY;
@ -3277,10 +3312,10 @@ export default function WeeklyView() {
{/* Week & Year */} {/* Week & Year */}
<div className="whitespace-nowrap flex items-center gap-2"> <div className="whitespace-nowrap flex items-center gap-2">
{(isLoading || isSyncing || syncStatus === "syncing") && ( {(isLoading || isSyncing || syncStatus === "syncing") && (
<RefreshCcw <div
className="animate-spin text-blue-600 mr-2" className="animate-spin rounded-full h-4 w-4 border-2 border-gray-300 border-t-blue-600 mr-2"
size={18} title="Syncing..."
/> ></div>
)} )}
<span style={{ <span style={{
fontFamily: profile.cwFontFamily || "Inter", fontFamily: profile.cwFontFamily || "Inter",
@ -3364,9 +3399,10 @@ export default function WeeklyView() {
return a.order - b.order; return a.order - b.order;
}); });
const nextTask = todaysTasks[0]; const nextTask = todaysTasks[0];
return nextTask ? `Do this now: ${nextTask.title}` : goal; const nextTaskText = nextTask ? `Do this now: ${nextTask.title}` : (goal || (profile.goalFallbackType === "quote" ? motivationalQuote : goal));
return nextTaskText;
})() })()
: goal} : (goal || (profile.goalFallbackType === "quote" ? motivationalQuote : goal))}
</span> </span>
)} )}
<span className="text-gray-300 mx-2">-</span> <span className="text-gray-300 mx-2">-</span>
@ -4145,7 +4181,7 @@ export default function WeeklyView() {
onEdit={() => setEditingTaskId(task.id)} onEdit={() => setEditingTaskId(task.id)}
onUpdate={(newTitle) => updateTask(task.id, newTitle)} onUpdate={(newTitle) => updateTask(task.id, newTitle)}
onDelete={() => deleteTask(task.id)} onDelete={() => deleteTask(task.id)}
onNotes={(notes) => updateTaskNotes(task.id, notes)} onNotes={() => setSelectedTaskForNotes(task)}
onRollToggle={() => toggleTaskRolling(task.id)} onRollToggle={() => toggleTaskRolling(task.id)}
onRecurrence={() => onRecurrence={() =>
setSelectedTaskForRecurrence(task) setSelectedTaskForRecurrence(task)
@ -6274,6 +6310,7 @@ function SettingsSidebar({
todayHighlightColor?: string; todayHighlightColor?: string;
pastDayColor?: string; pastDayColor?: string;
goalFallbackType?: "quote" | "next_todo" | "default"; goalFallbackType?: "quote" | "next_todo" | "default";
quoteSourceUrl?: string;
goalDefaultSentence?: string; goalDefaultSentence?: string;
goalFontFamily?: string; goalFontFamily?: string;
goalFontSize?: string; goalFontSize?: string;
@ -6316,6 +6353,8 @@ function SettingsSidebar({
hourLabelFormat: "short", hourLabelFormat: "short",
showSubHourSlots: true, showSubHourSlots: true,
allDayPosition: "below", allDayPosition: "below",
goalFallbackType: "quote",
quoteSourceUrl: "https://recite.vercel.app/api/random",
headlineFont: "Inter", headlineFont: "Inter",
headlineFontSize: "1.25rem", headlineFontSize: "1.25rem",
headlineFontWeight: "900", headlineFontWeight: "900",
@ -8833,45 +8872,7 @@ function SettingsSidebar({
<div <div
style={{ display: "flex", flexDirection: "column", gap: "24px" }} style={{ display: "flex", flexDirection: "column", gap: "24px" }}
> >
{/* Goal of the Week */} {/* Replaced Goal of the Week settings block */}
<div
style={{
background: "var(--weekly-settings-item-bg)",
padding: "20px",
borderRadius: "12px",
border: "1px solid var(--weekly-border)",
}}
>
<label
style={{
display: "block",
fontSize: "0.9rem",
fontWeight: 600,
marginBottom: "8px",
color: "var(--weekly-settings-title)",
}}
>
{t.goalOfWeek}
</label>
<input
type="text"
value={goal}
onChange={(e) => setGoal(e.target.value)}
onBlur={() => saveGoal(goal)}
onKeyDown={(e) => e.key === "Enter" && e.currentTarget.blur()}
className="weekly-input"
placeholder={t.goalOfWeek}
style={{
width: "100%",
padding: "12px",
fontSize: "1rem",
borderRadius: "6px",
border: "1px solid var(--weekly-border)",
background: "var(--weekly-bg)",
color: "var(--weekly-settings-text)",
}}
/>
</div>
{/* "Do This Now" Toggle */} {/* "Do This Now" Toggle */}
<div <div
style={{ style={{
@ -9110,6 +9111,43 @@ function SettingsSidebar({
<option value="default">Standardtext</option> <option value="default">Standardtext</option>
</select> </select>
</div> </div>
{profile.goalFallbackType === "quote" && (
<div style={{ marginTop: "12px" }}>
<label
style={{
display: "block",
fontSize: "0.9rem",
marginBottom: "4px",
color: "var(--weekly-settings-label)",
}}
>
API-Datenquelle (URL)
</label>
<input
type="text"
value={profile.quoteSourceUrl || ""}
onChange={(e) =>
setProfile((p) => ({
...p,
quoteSourceUrl: e.target.value,
}))
}
className="weekly-input"
placeholder="https://recite.vercel.app/api/random"
style={{
width: "100%",
padding: "10px",
fontSize: "0.95rem",
borderRadius: "6px",
border: "1px solid var(--weekly-border)",
background: "var(--weekly-bg)",
}}
/>
<p style={{ fontSize: "0.75rem", color: "var(--weekly-text-light)", marginTop: "4px" }}>
URL that returns a JSON list or object of quotes (e.g. {'[{"quote":"...","author":"..."}]'} or {'{"quote":"..."}'}).
</p>
</div>
)}
{profile.goalFallbackType === "default" && ( {profile.goalFallbackType === "default" && (
<div style={{ marginTop: "12px" }}> <div style={{ marginTop: "12px" }}>
<label <label