668 lines
23 KiB
TypeScript
668 lines
23 KiB
TypeScript
import { Archive, Settings2 } from "lucide-react";
|
||
import { ArchivedHabits } from "../components/ArchivedHabits";
|
||
import { AccountSettings } from "../components/AccountSettings";
|
||
import { useCallback, useEffect, useRef, useState } from "react";
|
||
import { useAuth } from "../components/AuthProvider";
|
||
import { DiscordSignInButton } from "../components/DiscordSignInButton";
|
||
import { PageLayout } from "../components/design-system/PageLayout";
|
||
import {
|
||
Button,
|
||
ButtonLink,
|
||
Checkbox,
|
||
Counter,
|
||
SectionHeading,
|
||
} from "../components/design-system/primitives";
|
||
import { Card, CardGrid } from "../components/design-system/Card";
|
||
import { HabitChart } from "../components/design-system/HabitChart";
|
||
import { HABIT_COLORS } from "../components/design-system/calendar-model";
|
||
import { HabitForm } from "../components/HabitForm";
|
||
import { habitRequest, scheduleLabel, type TodayHabit, type TodayResponse } from "../lib/dashboard";
|
||
import { HabitHistory } from "../components/HabitHistory";
|
||
import { WelcomePanel } from "../components/design-system/WelcomePanel";
|
||
import type { PublicUser } from "../shared/user";
|
||
import { ItemActions } from "../components/design-system/ItemActions";
|
||
import { type ItemMode } from "../components/InlineItemForm";
|
||
import { InlineHabitEditor } from "../components/InlineHabitEditor";
|
||
import { InlineTaskEditor } from "../components/InlineTaskEditor";
|
||
import type { Schedule } from "../habits/contracts";
|
||
import { ShareProgress } from "../components/ShareProgress";
|
||
|
||
export function Home() {
|
||
const { user, loading, busy, error, accountError, signIn, signOut, retry } = useAuth();
|
||
return (
|
||
<PageLayout
|
||
header={
|
||
loading ? (
|
||
<span className="ds-library-label">Loading account…</span>
|
||
) : user ? (
|
||
<Button variant="text" disabled={busy} onClick={() => void signOut()}>
|
||
{busy ? "Signing out…" : "Sign out"}
|
||
</Button>
|
||
) : (
|
||
!accountError && <DiscordSignInButton variant="secondary" onClick={signIn} />
|
||
)
|
||
}
|
||
>
|
||
{error && (
|
||
<div className="ds-form-feedback" role="alert">
|
||
{error}
|
||
</div>
|
||
)}
|
||
{loading ? (
|
||
<section className="ds-section" aria-busy="true">
|
||
<p role="status">Getting things ready…</p>
|
||
</section>
|
||
) : accountError ? (
|
||
<section className="ds-section">
|
||
<h1 className="type-section">Let’s try that again.</h1>
|
||
<Card headingLevel={2} heading="Account unavailable">
|
||
<p>Your account couldn’t be loaded.</p>
|
||
<div className="ds-actions">
|
||
<Button onClick={retry}>Try again</Button>
|
||
</div>
|
||
</Card>
|
||
</section>
|
||
) : user ? (
|
||
<Dashboard key={user.id} user={user} onExpired={retry} />
|
||
) : (
|
||
<Welcome onSignIn={signIn} />
|
||
)}
|
||
</PageLayout>
|
||
);
|
||
}
|
||
|
||
function Welcome({ onSignIn }: { onSignIn: () => void }) {
|
||
const [water, setWater] = useState(3);
|
||
const [read, setRead] = useState(false);
|
||
return (
|
||
<>
|
||
<section className="ds-section ds-split-section" aria-labelledby="welcome-title">
|
||
<div className="ds-section-heading">
|
||
<p className="ds-eyebrow">A LITTLE, EVERY DAY</p>
|
||
<h1 id="welcome-title" className="type-section">
|
||
Small steps.
|
||
<br />
|
||
<em>Lasting rhythm.</em>
|
||
</h1>
|
||
</div>
|
||
<Card headingLevel={2} heading="Make room for what matters.">
|
||
<p>
|
||
Check in, count a little more, or work through a few tasks. See your progress grow, one
|
||
day at a time.
|
||
</p>
|
||
<div className="ds-actions">
|
||
<DiscordSignInButton onClick={onSignIn} />
|
||
<ButtonLink href="#try-it">Try it below ↓</ButtonLink>
|
||
</div>
|
||
<p className="type-small">Sign in with your Discord account to save your habits.</p>
|
||
</Card>
|
||
</section>
|
||
<section className="ds-section" id="try-it" aria-labelledby="try-it-title">
|
||
<div className="ds-section-top">
|
||
<SectionHeading
|
||
number="TRY A CHECK-IN"
|
||
id="try-it-title"
|
||
title={
|
||
<>
|
||
A little progress. <em>Made visible.</em>
|
||
</>
|
||
}
|
||
/>
|
||
<span className="ds-demo-note">Example · September 4, 2026 · not saved</span>
|
||
</div>
|
||
<div className="ds-habit-chart-grid">
|
||
<HabitChart
|
||
headingLevel={3}
|
||
name="Drink water"
|
||
method="Count target"
|
||
value={water}
|
||
target={8}
|
||
unit="glasses"
|
||
color={HABIT_COLORS.water}
|
||
>
|
||
<Counter label="glasses of water" value={water} target={8} onChange={setWater} />
|
||
</HabitChart>
|
||
<HabitChart
|
||
headingLevel={3}
|
||
name="Read a little"
|
||
method="Simple check-in"
|
||
value={Number(read)}
|
||
target={1}
|
||
unit="reading session"
|
||
color={HABIT_COLORS.reading}
|
||
>
|
||
<Checkbox
|
||
label="Reading done"
|
||
checked={read}
|
||
onChange={(event) => setRead(event.target.checked)}
|
||
/>
|
||
</HabitChart>
|
||
</div>
|
||
</section>
|
||
<section className="ds-section" aria-label="Your own rhythm">
|
||
<CardGrid>
|
||
<Card heading="Your habits. Your pace." eyebrow="MAKE IT FIT">
|
||
<p>
|
||
Pick a check-in, a count target, or a task list. Repeat daily, on selected weekdays,
|
||
or at your own interval.
|
||
</p>
|
||
</Card>
|
||
<Card heading="See the days add up." eyebrow="KEEP PERSPECTIVE" variant="outlined">
|
||
<p>
|
||
Explore your calendar to see the progress behind each square. Days off stay distinct
|
||
from missed days.
|
||
</p>
|
||
</Card>
|
||
</CardGrid>
|
||
</section>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => void }) {
|
||
const [today, setToday] = useState<TodayResponse | null>(null);
|
||
const [error, setError] = useState("");
|
||
const [notice, setNotice] = useState("");
|
||
const [revision, setRevision] = useState(0);
|
||
const [attempt, setAttempt] = useState(0);
|
||
const [loading, setLoading] = useState(true);
|
||
const [habitView, setHabitView] = useState<"today" | "all">("today");
|
||
const [adding, setAdding] = useState(false);
|
||
const [sharing, setSharing] = useState(false);
|
||
const [settings, setSettings] = useState(false);
|
||
const [archive, setArchive] = useState(false);
|
||
const [busy, setBusy] = useState(false);
|
||
const [needsRefresh, setNeedsRefresh] = useState(false);
|
||
const focusAddAfterRefresh = useRef(false);
|
||
const saving = useRef(false);
|
||
const mounted = useRef(true);
|
||
const expired = useRef(onExpired);
|
||
expired.current = onExpired;
|
||
useEffect(() => {
|
||
mounted.current = true;
|
||
return () => {
|
||
mounted.current = false;
|
||
};
|
||
}, []);
|
||
|
||
const reportError = useCallback((error: unknown) => {
|
||
if (error instanceof Error && error.message.includes("session has expired")) expired.current();
|
||
else
|
||
setError(
|
||
error instanceof Error ? error.message : "Could not load your habits. Please try again."
|
||
);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const controller = new AbortController();
|
||
setLoading(true);
|
||
setError("");
|
||
habitRequest<TodayResponse>("/today", { signal: controller.signal })
|
||
.then((data) => {
|
||
if (!controller.signal.aborted) {
|
||
setToday(data);
|
||
setNeedsRefresh(false);
|
||
setRevision((value) => value + 1);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
if (!controller.signal.aborted) reportError(error);
|
||
})
|
||
.finally(() => {
|
||
if (!controller.signal.aborted) {
|
||
setLoading(false);
|
||
if (focusAddAfterRefresh.current) {
|
||
focusAddAfterRefresh.current = false;
|
||
window.requestAnimationFrame(() => document.getElementById("add-habit")?.focus({ preventScroll: true }));
|
||
}
|
||
}
|
||
});
|
||
return () => controller.abort();
|
||
}, [attempt, reportError]);
|
||
|
||
// Refresh after returning to the page and across the account's local midnight.
|
||
useEffect(() => {
|
||
const refresh = () => {
|
||
if (!saving.current && document.visibilityState === "visible")
|
||
setAttempt((value) => value + 1);
|
||
};
|
||
const timer = window.setInterval(refresh, 60_000);
|
||
window.addEventListener("focus", refresh);
|
||
return () => {
|
||
window.clearInterval(timer);
|
||
window.removeEventListener("focus", refresh);
|
||
};
|
||
}, []);
|
||
|
||
async function update(
|
||
habit: TodayHabit,
|
||
body: { count: number } | { done: boolean },
|
||
taskId?: string
|
||
) {
|
||
if (saving.current || loading || needsRefresh || !today) return;
|
||
saving.current = true;
|
||
setBusy(true);
|
||
setError("");
|
||
setNotice("");
|
||
try {
|
||
const path = `/habits/${habit.habitId}/days/${today.date}/${taskId ? `tasks/${taskId}` : "progress"}`;
|
||
const updated = await habitRequest<TodayHabit>(path, {
|
||
method: "PUT",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
if (!mounted.current) return;
|
||
setToday((current) => {
|
||
if (!current) return current;
|
||
const habits = current.habits.map((item) =>
|
||
item.habitId === updated.habitId ? updated : item
|
||
);
|
||
return {
|
||
...current,
|
||
habits,
|
||
due: habits.filter((item) => item.due).length,
|
||
completed: habits.filter((item) => item.complete).length,
|
||
};
|
||
});
|
||
setRevision((value) => value + 1);
|
||
setNotice(`${habit.name} saved.`);
|
||
} catch (error) {
|
||
if (mounted.current) reportError(error);
|
||
} finally {
|
||
saving.current = false;
|
||
if (mounted.current) setBusy(false);
|
||
}
|
||
}
|
||
|
||
async function manage(
|
||
habit: TodayHabit,
|
||
patch: Record<string, unknown> | null,
|
||
taskId?: string,
|
||
createTask = false
|
||
) {
|
||
if (saving.current || loading || needsRefresh)
|
||
throw new Error("Please wait for the dashboard to refresh.");
|
||
saving.current = true;
|
||
setBusy(true);
|
||
setError("");
|
||
setNotice("");
|
||
try {
|
||
await habitRequest(
|
||
`/habits/${habit.habitId}${createTask ? "/tasks" : taskId ? `/tasks/${taskId}` : ""}`,
|
||
{
|
||
method: createTask ? "POST" : patch ? "PATCH" : "DELETE",
|
||
...(patch
|
||
? { headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch) }
|
||
: {}),
|
||
}
|
||
);
|
||
// A refresh failure must never turn a successful delete into a retryable delete.
|
||
try {
|
||
const refreshed = await habitRequest<TodayResponse>("/today");
|
||
if (mounted.current) {
|
||
setToday(refreshed);
|
||
// Keep a habit reachable when an edit makes it unscheduled (including an empty routine).
|
||
if (refreshed.habits.some(item => item.habitId === habit.habitId && !item.due)) setHabitView("all");
|
||
setNeedsRefresh(false);
|
||
setRevision((value) => value + 1);
|
||
}
|
||
} catch (refreshError) {
|
||
if (refreshError instanceof Error && refreshError.message.includes("session has expired"))
|
||
expired.current();
|
||
if (mounted.current) {
|
||
setNeedsRefresh(true);
|
||
setError(
|
||
"Your change was saved, but the dashboard could not refresh. Try again to load the latest data."
|
||
);
|
||
}
|
||
}
|
||
if (mounted.current) {
|
||
setNotice(
|
||
`${taskId || createTask ? "Task" : "Habit"} ${createTask ? "added" : patch ? "updated" : taskId ? "deleted" : "archived"}.`
|
||
);
|
||
if (!patch)
|
||
window.requestAnimationFrame(() => {
|
||
(
|
||
document.getElementById("habits-title") ?? document.getElementById("add-habit")
|
||
)?.focus();
|
||
});
|
||
}
|
||
} catch (error) {
|
||
if (error instanceof Error && error.message.includes("session has expired"))
|
||
expired.current();
|
||
throw error;
|
||
} finally {
|
||
saving.current = false;
|
||
if (mounted.current) setBusy(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<>
|
||
<WelcomePanel
|
||
compact
|
||
name={user.displayName || user.username}
|
||
avatarUrl={user.avatarUrl}
|
||
date={today?.date}
|
||
timezone={today?.timezone || user.timezone}
|
||
>
|
||
{today && (
|
||
<>
|
||
<div className="ds-welcome-progress" role="status">
|
||
{today.due > 0 ? (
|
||
<>
|
||
<span className="type-title">
|
||
{today.completed} / {today.due}
|
||
</span>
|
||
<span>
|
||
habits complete today
|
||
{today.completed === today.due && (
|
||
<span className="ds-welcome-complete">A little, all done.</span>
|
||
)}
|
||
</span>
|
||
</>
|
||
) : (
|
||
<p>
|
||
{today.habits.length
|
||
? "Nothing scheduled today. Enjoy a little breathing room."
|
||
: "Start with one habit. Your first small step starts here."}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<div className="ds-action-row ds-primary-actions">
|
||
<Button
|
||
id="add-habit"
|
||
aria-haspopup="dialog"
|
||
disabled={adding || busy || loading || needsRefresh}
|
||
onClick={() => setAdding(true)}
|
||
aria-expanded={adding}
|
||
aria-controls={adding ? "new-habit" : undefined}
|
||
>
|
||
Add a habit +
|
||
</Button>
|
||
{today.habits.length > 0 && (
|
||
<>
|
||
<Button id="open-sharing" aria-haspopup="dialog" variant="secondary" disabled={busy || loading || needsRefresh || sharing} aria-expanded={sharing} aria-controls={sharing ? "share-progress" : undefined} onClick={() => setSharing(true)}>Share progress</Button>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div className="ds-action-row ds-utility-row">
|
||
<Button variant="text" disabled={busy} aria-haspopup="dialog" aria-expanded={archive} aria-controls={archive ? "archived-habits" : undefined} onClick={() => setArchive(!archive)}><Archive size={16} aria-hidden="true" />Archived habits</Button>
|
||
<Button variant="text" disabled={busy} aria-haspopup="dialog" aria-expanded={settings} aria-controls={settings ? "account-settings" : undefined} onClick={() => setSettings(!settings)}><Settings2 size={16} aria-hidden="true" />Settings</Button>
|
||
</div>
|
||
</>
|
||
)}
|
||
</WelcomePanel>
|
||
{archive && today && <ArchivedHabits onClose={() => setArchive(false)} date={today.date} revision={revision} onExpired={onExpired} onChanged={() => setAttempt(value => value + 1)} />}
|
||
{settings && <AccountSettings user={user} onChanged={onExpired} onClose={() => setSettings(false)} />}
|
||
{sharing && today && <ShareProgress user={user} today={today} revision={revision} onClose={() => { setSharing(false); requestAnimationFrame(() => document.getElementById("open-sharing")?.focus()); }} />}
|
||
{error && (
|
||
<div className="ds-form-feedback" role="alert">
|
||
<p>{error}</p>
|
||
<Button
|
||
variant="secondary"
|
||
disabled={loading}
|
||
onClick={() => setAttempt((value) => value + 1)}
|
||
>
|
||
Try again
|
||
</Button>
|
||
</div>
|
||
)}
|
||
{!today && loading && (
|
||
<section className="ds-section">
|
||
<p role="status">Loading your habits…</p>
|
||
</section>
|
||
)}
|
||
{today && (
|
||
<>
|
||
{adding && (
|
||
<HabitForm
|
||
date={today.date}
|
||
onCancel={() => setAdding(false)}
|
||
onExpired={() => expired.current()}
|
||
onCreated={(name) => {
|
||
focusAddAfterRefresh.current = true;
|
||
setAdding(false);
|
||
setNotice(`${name} created.`);
|
||
setHabitView("all");
|
||
setAttempt((value) => value + 1);
|
||
}}
|
||
/>
|
||
)}
|
||
<p className="ds-form-feedback" role="status">
|
||
{notice}
|
||
</p>
|
||
{today.habits.length > 0 && (
|
||
<section
|
||
className="ds-section"
|
||
aria-labelledby="habits-title"
|
||
aria-busy={busy || loading}
|
||
>
|
||
<div className="ds-section-top">
|
||
<SectionHeading
|
||
number="YOUR HABITS"
|
||
id="habits-title"
|
||
title={
|
||
<>
|
||
Today, <em>at your pace.</em>
|
||
</>
|
||
}
|
||
/>
|
||
</div>
|
||
<div className="ds-action-row ds-habit-filters" role="group" aria-label="Show habits">
|
||
<Button variant={habitView === "today" ? "primary" : "text"} aria-pressed={habitView === "today"} onClick={() => setHabitView("today")}>Today · {today.due}</Button>
|
||
<Button variant={habitView === "all" ? "primary" : "text"} aria-pressed={habitView === "all"} onClick={() => setHabitView("all")}>All habits · {today.habits.length}</Button>
|
||
</div>
|
||
{habitView === "today" && today.due === 0 && <p className="ds-supporting-copy">You’re all set for today. Choose All habits to view or edit your routines.</p>}
|
||
<div className="ds-habit-chart-grid ds-habit-list">
|
||
{today.habits.filter(habit => habitView === "all" || habit.due).map((habit) => (
|
||
<SavedHabit
|
||
key={habit.habitId}
|
||
habit={habit}
|
||
date={today.date}
|
||
revision={revision}
|
||
disabled={busy || loading || needsRefresh}
|
||
onHistorySaved={() => setAttempt(value => value + 1)}
|
||
onUpdate={update}
|
||
onManage={manage}
|
||
onExpired={() => expired.current()}
|
||
/>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
</>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
function SavedHabit({
|
||
habit,
|
||
date,
|
||
revision,
|
||
disabled,
|
||
onUpdate,
|
||
onManage,
|
||
onHistorySaved,
|
||
onExpired,
|
||
}: {
|
||
habit: TodayHabit;
|
||
date: string;
|
||
revision: number;
|
||
disabled: boolean;
|
||
onUpdate: (
|
||
habit: TodayHabit,
|
||
body: { count: number } | { done: boolean },
|
||
taskId?: string
|
||
) => Promise<void>;
|
||
onExpired: () => void;
|
||
onHistorySaved: () => void;
|
||
onManage: (
|
||
habit: TodayHabit,
|
||
patch: Record<string, unknown> | null,
|
||
taskId?: string,
|
||
createTask?: boolean
|
||
) => Promise<void>;
|
||
}) {
|
||
const [editing, setEditing] = useState<{ mode: ItemMode; color: string } | null>(null);
|
||
const [addingTask, setAddingTask] = useState(false);
|
||
const blocked = disabled || !habit.due;
|
||
return (
|
||
<HabitHistory
|
||
habit={habit}
|
||
date={date}
|
||
revision={revision}
|
||
disabled={disabled}
|
||
onExpired={onExpired}
|
||
onHistorySaved={onHistorySaved}
|
||
onEdit={(color) => setEditing({ mode: "edit", color })}
|
||
onDelete={() => setEditing({ mode: "delete", color: "#196127" })}
|
||
editor={
|
||
editing && habit.requirements ? (
|
||
<InlineHabitEditor
|
||
key={editing.mode}
|
||
config={habit.requirements}
|
||
date={date}
|
||
color={editing.color}
|
||
mode={editing.mode}
|
||
disabled={disabled}
|
||
onClose={() => setEditing(null)}
|
||
onSave={(patch) => onManage(habit, patch)}
|
||
onDelete={() => onManage(habit, null)}
|
||
/>
|
||
) : undefined
|
||
}
|
||
tasks={
|
||
habit.requirements?.method === "tasks" ? (
|
||
<>
|
||
{habit.requirements.tasks.map((config) => {
|
||
const occurrence = habit.tasks.find((task) => task.taskId === config.id);
|
||
return (
|
||
<SavedTask
|
||
key={config.id}
|
||
task={{ taskId: config.id, name: config.name, done: occurrence?.done ?? false }}
|
||
disabled={disabled}
|
||
blocked={blocked || !occurrence}
|
||
scheduled={!!occurrence}
|
||
schedule={config.schedule}
|
||
habitSchedule={habit.requirements!.schedule}
|
||
date={date}
|
||
onCheck={(done) => void onUpdate(habit, { done }, config.id)}
|
||
onSave={(patch) => onManage(habit, patch, config.id)}
|
||
onDelete={() => onManage(habit, null, config.id)}
|
||
/>
|
||
);
|
||
})}
|
||
{addingTask && (
|
||
<InlineTaskEditor
|
||
name=""
|
||
schedule={{ type: "daily" }}
|
||
habitSchedule={habit.requirements.schedule}
|
||
date={date}
|
||
mode="edit"
|
||
creating
|
||
disabled={disabled}
|
||
onClose={() => setAddingTask(false)}
|
||
onSave={(patch) => onManage(habit, patch, undefined, true)}
|
||
onDelete={async () => {}}
|
||
/>
|
||
)}
|
||
<div className="ds-task-add-action">
|
||
{!habit.requirements.tasks.length && (
|
||
<p className="ds-footnote">Add a task to start checking in.</p>
|
||
)}
|
||
<Button
|
||
variant="text"
|
||
aria-haspopup="dialog"
|
||
aria-expanded={addingTask}
|
||
disabled={disabled || habit.requirements.tasks.length >= 100}
|
||
onClick={() => setAddingTask(true)}
|
||
>
|
||
Add task +
|
||
</Button>
|
||
</div>
|
||
</>
|
||
) : undefined
|
||
}
|
||
>
|
||
{habit.method === "count" ? (
|
||
<Counter
|
||
key={date}
|
||
label={habit.name ?? "habit count"}
|
||
value={habit.value}
|
||
target={habit.target ?? 0}
|
||
disabled={blocked}
|
||
onChange={(count) => void onUpdate(habit, { count })}
|
||
/>
|
||
) : habit.method === "manual" ? (
|
||
<Checkbox
|
||
label={`${habit.name} done`}
|
||
checked={habit.complete}
|
||
disabled={blocked}
|
||
onChange={(event) => void onUpdate(habit, { done: event.target.checked })}
|
||
/>
|
||
) : undefined}
|
||
</HabitHistory>
|
||
);
|
||
}
|
||
|
||
function SavedTask({
|
||
task,
|
||
schedule,
|
||
habitSchedule,
|
||
date,
|
||
disabled,
|
||
blocked,
|
||
scheduled,
|
||
onCheck,
|
||
onSave,
|
||
onDelete,
|
||
}: {
|
||
task: Pick<TodayHabit["tasks"][number], "taskId" | "name" | "done">;
|
||
disabled: boolean;
|
||
blocked: boolean;
|
||
scheduled: boolean;
|
||
schedule: Schedule;
|
||
habitSchedule: Schedule;
|
||
date: string;
|
||
onCheck: (done: boolean) => void;
|
||
onSave: (patch: { name?: string; schedule?: Schedule }) => Promise<void>;
|
||
onDelete: () => Promise<void>;
|
||
}) {
|
||
const [mode, setMode] = useState<ItemMode | null>(null);
|
||
return (
|
||
<div className="ds-editable-task">
|
||
<div className="ds-editable-task-row">
|
||
<Checkbox
|
||
label={task.name}
|
||
description={`${scheduleLabel(schedule)}${scheduled ? "" : " · Not scheduled today"}`}
|
||
checked={task.done}
|
||
disabled={blocked || !!mode}
|
||
onChange={(event) => onCheck(event.target.checked)}
|
||
/>
|
||
<ItemActions
|
||
name={task.name}
|
||
kind="task"
|
||
disabled={disabled || !!mode}
|
||
onEdit={() => setMode("edit")}
|
||
onDelete={() => setMode("delete")}
|
||
/>
|
||
</div>
|
||
{mode && (
|
||
<InlineTaskEditor
|
||
name={task.name}
|
||
schedule={schedule}
|
||
habitSchedule={habitSchedule}
|
||
date={date}
|
||
mode={mode}
|
||
disabled={disabled}
|
||
onSave={onSave}
|
||
onDelete={onDelete}
|
||
onClose={() => setMode(null)}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|