aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/Main.tsx2
-rw-r--r--src/client/views/nodes/TaskManagerTask.scss72
-rw-r--r--src/client/views/nodes/TaskManagerTask.tsx251
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.scss19
-rw-r--r--src/client/views/nodes/calendarBox/CalendarBox.tsx68
-rw-r--r--src/client/views/nodes/formattedText/DailyJournal.tsx61
6 files changed, 464 insertions, 9 deletions
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b884eb8c8..ecbef3497 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -65,6 +65,7 @@ import { PresBox, PresSlideBox } from './nodes/trails';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
import { SearchBox } from './search/SearchBox';
import { StickerPalette } from './smartdraw/StickerPalette';
+import { TaskManagerTask } from './nodes/TaskManagerTask';
import { ScrapbookBox } from './nodes/scrapbook/ScrapbookBox';
dotenv.config();
@@ -120,6 +121,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
StickerPalette: StickerPalette,
FormattedTextBox,
DailyJournal, // AARAV
+ TaskManagerTask, // AARAV
ImageBox,
FontIconBox,
LabelBox,
diff --git a/src/client/views/nodes/TaskManagerTask.scss b/src/client/views/nodes/TaskManagerTask.scss
new file mode 100644
index 000000000..0fcc2f955
--- /dev/null
+++ b/src/client/views/nodes/TaskManagerTask.scss
@@ -0,0 +1,72 @@
+.task-manager-container {
+ display: flex;
+ flex-direction: column;
+ padding: 8px;
+ gap: 10px;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.task-manager-title {
+ width: 100%;
+ font-size: 1.25rem;
+ font-weight: 600;
+ padding: 6px 10px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ box-sizing: border-box;
+}
+
+.task-manager-description {
+ width: 100%;
+ font-size: 1rem;
+ padding: 8px 10px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ min-height: 40px;
+ box-sizing: border-box;
+ vertical-align: top;
+ text-align: start;
+ resize: none;
+ line-height: 1.4;
+ resize: none;
+ flex-grow: 1
+}
+
+.task-manager-checkboxes {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+}
+
+.task-manager-allday, .task-manager-complete {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.95rem;
+}
+
+.task-manager-times {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+}
+
+.task-manager-times label {
+ display: flex;
+ flex-direction: column;
+ font-size: 0.9rem;
+ font-weight: 500;
+ gap: 4px;
+}
+
+input[type="datetime-local"] {
+ width: 100%;
+ font-size: 0.9rem;
+ padding: 6px 8px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ box-sizing: border-box;
+}
diff --git a/src/client/views/nodes/TaskManagerTask.tsx b/src/client/views/nodes/TaskManagerTask.tsx
new file mode 100644
index 000000000..2d2444275
--- /dev/null
+++ b/src/client/views/nodes/TaskManagerTask.tsx
@@ -0,0 +1,251 @@
+import { action, observable, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { FieldView } from './FieldView';
+import { DateField } from '../../../fields/DateField';
+import { Doc } from '../../../fields/Doc';
+
+import './TaskManagerTask.scss';
+
+interface TaskManagerProps {
+ Document: Doc;
+}
+
+@observer
+export class TaskManagerTask extends React.Component<TaskManagerProps> {
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(TaskManagerTask, fieldStr);
+ }
+
+ @action
+ updateText = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
+ this.props.Document.text = e.target.value;
+ };
+
+ @action
+ updateTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.Document.title = e.target.value;
+ };
+
+ @action
+ updateAllDay = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.Document.allDay = e.target.checked;
+
+ if (e.target.checked) {
+ delete this.props.Document.startTime;
+ delete this.props.Document.endTime;
+ }
+
+ this.setTaskDateRange();
+ };
+
+ @action
+ updateStart = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const newStart = new Date(e.target.value);
+
+ this.props.Document.startTime = new DateField(newStart);
+
+ const endDate = this.props.Document.endTime instanceof DateField ? this.props.Document.endTime.date : undefined;
+ if (endDate && newStart > endDate) {
+ // Alert user
+ alert('Start time cannot be after end time. End time has been adjusted.');
+
+ // Fix end time
+ const adjustedEnd = new Date(newStart.getTime() + 60 * 60 * 1000);
+ this.props.Document.endTime = new DateField(adjustedEnd);
+ }
+
+ this.setTaskDateRange();
+ };
+
+
+
+ @action
+ updateEnd = (e: React.ChangeEvent<HTMLInputElement>) => {
+ const newEnd = new Date(e.target.value);
+
+ this.props.Document.endTime = new DateField(newEnd);
+
+ const startDate = this.props.Document.startTime instanceof DateField ? this.props.Document.startTime.date : undefined;
+ if (startDate && newEnd < startDate) {
+ // Alert user
+ alert('End time cannot be before start time. Start time has been adjusted.');
+
+ // Fix start time
+ const adjustedStart = new Date(newEnd.getTime() - 60 * 60 * 1000);
+ this.props.Document.startTime = new DateField(adjustedStart);
+ }
+
+ this.setTaskDateRange();
+ };
+
+
+
+
+ @action
+ setTaskDateRange() {
+ const doc = this.props.Document;
+
+ if (doc.allDay) {
+ // All-day task → use date only
+ if (!doc.title) return;
+
+ const parsedDate = new Date(doc.title as string);
+ if (!isNaN(parsedDate.getTime())) {
+ const localStart = new Date(parsedDate.getFullYear(), parsedDate.getMonth(), parsedDate.getDate());
+ const localEnd = new Date(localStart);
+ doc.date_range = `${localStart.toISOString()}|${localEnd.toISOString()}`;
+ doc.allDay = true;
+ }
+ } else {
+ // Timed task → use full startTime and endTime
+ const startField = doc.startTime;
+ const endField = doc.endTime;
+ const startDate = startField instanceof DateField ? startField.date : null;
+ const endDate = endField instanceof DateField ? endField.date : null;
+
+ if (startDate && endDate && !isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
+ doc.date_range = `${startDate.toISOString()}|${endDate.toISOString()}`;
+ doc.allDay = false;
+ } else {
+ console.warn('startTime or endTime is invalid');
+ }
+ }
+ }
+
+ @action
+ toggleComplete = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.Document.completed = e.target.checked;
+ };
+
+ constructor(props: TaskManagerProps) {
+ super(props);
+ makeObservable(this);
+ }
+
+ componentDidMount() {
+ this.setTaskDateRange();
+ }
+
+
+ render() {
+
+ function toLocalDateTimeString(date: Date): string {
+ const pad = (n: number) => n.toString().padStart(2, '0');
+ return (
+ date.getFullYear() +
+ '-' +
+ pad(date.getMonth() + 1) +
+ '-' +
+ pad(date.getDate()) +
+ 'T' +
+ pad(date.getHours()) +
+ ':' +
+ pad(date.getMinutes())
+ );
+ }
+
+ const doc = this.props.Document;
+
+ const taskDesc = typeof doc.text === 'string' ? doc.text : '';
+ const taskTitle = typeof doc.title === 'string' ? doc.title : '';
+ const allDay = !!doc.allDay;
+ const isCompleted = !!this.props.Document.completed;
+
+ const startTime = doc.startTime instanceof DateField && doc.startTime.date instanceof Date
+ ? toLocalDateTimeString(doc.startTime.date)
+ : '';
+
+ const endTime = doc.endTime instanceof DateField && doc.endTime.date instanceof Date
+ ? toLocalDateTimeString(doc.endTime.date)
+ : '';
+
+
+
+ return (
+ <div className="task-manager-container">
+ <input
+ className="task-manager-title"
+ type="text"
+ placeholder="Task Title"
+ value={taskTitle}
+ onChange={this.updateTitle}
+ disabled={isCompleted}
+ style={{opacity: isCompleted ? 0.7 : 1,}}
+ />
+
+ <textarea
+ className="task-manager-description"
+ placeholder="What’s your task?"
+ value={taskDesc}
+ onChange={this.updateText}
+ disabled={isCompleted}
+ style={{opacity: isCompleted ? 0.7 : 1,}}
+ />
+
+ <div className="task-manager-checkboxes">
+
+ <label className="task-manager-allday" style={{opacity: isCompleted ? 0.7 : 1,}}>
+ <input
+ type="checkbox"
+ checked={allDay}
+ onChange={this.updateAllDay}
+ disabled={isCompleted}
+ />
+ All day
+ </label>
+
+ <label className="task-manager-complete">
+ <input type="checkbox" checked={isCompleted} onChange={this.toggleComplete} />
+ Complete
+ </label>
+
+ </div>
+
+ {!allDay && (
+ <div
+ className="task-manager-times"
+ style={{ opacity: isCompleted ? 0.7 : 1 }}
+ >
+ <label>
+ Start:
+ <input
+ type="datetime-local"
+ value={startTime}
+ onChange={this.updateStart}
+ disabled={isCompleted}
+ />
+ </label>
+ <label>
+ End:
+ <input
+ type="datetime-local"
+ value={endTime}
+ onChange={this.updateEnd}
+ disabled={isCompleted}
+ />
+ </label>
+ </div>
+ )}
+ </div>
+ );
+ }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.TASK, {
+ layout: { view: TaskManagerTask, dataField: 'text' },
+ options: {
+ acl: '',
+ _height: 35,
+ _xMargin: 10,
+ _yMargin: 10,
+ _layout_autoHeight: true,
+ _layout_nativeDimEditable: true,
+ _layout_reflowVertical: true,
+ _layout_reflowHorizontal: true,
+ defaultDoubleClick: 'ignore',
+ systemIcon: 'BsCheckSquare', // or whatever icon you like
+ },
+});
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.scss b/src/client/views/nodes/calendarBox/CalendarBox.scss
index f8ac4b2d1..f5d3613e3 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.scss
+++ b/src/client/views/nodes/calendarBox/CalendarBox.scss
@@ -23,3 +23,22 @@
}
}
}
+
+// AARAV ADD
+
+/* Existing styles */
+.fc-event.mother {
+ font-weight: 500;
+ border-radius: 4px;
+ padding: 2px 4px;
+ border-width: 2px;
+ }
+
+ /* New styles for completed tasks */
+ .fc-event.completed-task {
+ opacity: 1;
+ filter: grayscale(70%) brightness(90%);
+ text-decoration: line-through;
+ color: #ffffff;
+ }
+ \ No newline at end of file
diff --git a/src/client/views/nodes/calendarBox/CalendarBox.tsx b/src/client/views/nodes/calendarBox/CalendarBox.tsx
index fa863e123..84fa25dba 100644
--- a/src/client/views/nodes/calendarBox/CalendarBox.tsx
+++ b/src/client/views/nodes/calendarBox/CalendarBox.tsx
@@ -1,4 +1,5 @@
import { Calendar, EventClickArg, EventDropArg, EventSourceInput } from '@fullcalendar/core';
+import { EventResizeDoneArg } from '@fullcalendar/interaction';
import dayGridPlugin from '@fullcalendar/daygrid';
import interactionPlugin from '@fullcalendar/interaction';
import multiMonthPlugin from '@fullcalendar/multimonth';
@@ -17,6 +18,7 @@ import { ContextMenu } from '../../ContextMenu';
import { DocumentView } from '../DocumentView';
import { OpenWhere } from '../OpenWhere';
import './CalendarBox.scss';
+import { DateField } from '../../../../fields/DateField';
type CalendarView = 'multiMonth' | 'dayGridMonth' | 'timeGridWeek' | 'timeGridDay';
@@ -66,6 +68,7 @@ export class CalendarBox extends CollectionSubView() {
@computed get calendarEvents(): EventSourceInput | undefined {
return this.childDocs.map(doc => {
const { start, end } = dateRangeStrToDates(StrCast(doc.date_range));
+ const isCompleted = BoolCast(doc.completed); // AARAV ADD
return {
title: StrCast(doc.title),
start,
@@ -74,7 +77,7 @@ export class CalendarBox extends CollectionSubView() {
startEditable: true,
endEditable: true,
allDay: BoolCast(doc.allDay),
- classNames: ['mother'], // will determine the style
+ classNames: ['mother', isCompleted ? 'completed-task' : ''], // will determine the style
editable: true, // subject to change in the future
backgroundColor: this.eventToColor(doc),
borderColor: this.eventToColor(doc),
@@ -105,7 +108,11 @@ export class CalendarBox extends CollectionSubView() {
// TODO: Return a different color based on the event type
eventToColor = (event: Doc): string => {
- return 'red' + event;
+ const docType = StrCast(event.type);
+ if (docType === 'task') {
+ return '#20B2AA'; // teal for tasks
+ }
+ return 'red';
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -123,9 +130,30 @@ export class CalendarBox extends CollectionSubView() {
return false;
};
- handleEventDrop = (arg: EventDropArg) => {
+ handleEventDrop = (arg: EventDropArg | EventResizeDoneArg) => {
const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
- doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString());
+ // doc && arg.event.start && (doc.date_range = arg.event.start?.toString() + '|' + (arg.event.end ?? arg.event.start).toString());
+ if (!doc || !arg.event.start) return;
+
+ // get the new start and end dates
+ const startDate = new Date(arg.event.start);
+ const endDate = new Date(arg.event.end ?? arg.event.start);
+
+ // update date range, time range, and all day status
+ doc.date_range = `${startDate.toISOString()}|${endDate.toISOString()}`;
+
+ const allDayStatus = arg.event.allDay ?? false;
+ if (doc.allDay !== allDayStatus) {
+ doc.allDay = allDayStatus;
+ }
+
+ if (doc.allDay) {
+ delete doc.startTime;
+ delete doc.endTime;
+ } else {
+ doc.startTime = new DateField(startDate);
+ doc.endTime = new DateField(endDate);
+ }
};
handleEventClick = (arg: EventClickArg) => {
@@ -171,7 +199,39 @@ export class CalendarBox extends CollectionSubView() {
events: this.calendarEvents,
eventClick: this.handleEventClick,
eventDrop: this.handleEventDrop,
+ eventResize: this.handleEventDrop,
eventDidMount: arg => {
+ const doc = DocServer.GetCachedRefField(arg.event._def.groupId ?? '');
+ if (!doc) return;
+
+ if (doc.type === 'task') {
+ const checkButton = document.createElement('button');
+ checkButton.innerText = doc.completed ? '✅' : '⬜';
+ checkButton.style.position = 'absolute';
+ checkButton.style.right = '5px';
+ checkButton.style.top = '50%';
+ checkButton.style.transform = 'translateY(-50%)';
+ checkButton.style.background = 'transparent';
+ checkButton.style.border = 'none';
+ checkButton.style.cursor = 'pointer';
+ checkButton.style.fontSize = '18px';
+ checkButton.style.zIndex = '1000';
+ checkButton.style.padding = '0';
+ checkButton.style.margin = '0';
+
+ checkButton.onclick = ev => {
+ ev.stopPropagation();
+ doc.completed = !doc.completed;
+ this._calendar?.refetchEvents();
+ };
+
+ // Make sure the parent box is positioned relative
+ arg.el.style.position = 'relative';
+ arg.el.appendChild(checkButton);
+ }
+
+ // (keep your other pointerup/contextmenu handlers here)
+
arg.el.addEventListener('pointerdown', ev => {
ev.button && ev.stopPropagation();
});
diff --git a/src/client/views/nodes/formattedText/DailyJournal.tsx b/src/client/views/nodes/formattedText/DailyJournal.tsx
index 871c556e6..d6d30dc13 100644
--- a/src/client/views/nodes/formattedText/DailyJournal.tsx
+++ b/src/client/views/nodes/formattedText/DailyJournal.tsx
@@ -9,6 +9,7 @@ import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
import { RichTextField } from '../../../../fields/RichTextField';
import { Plugin } from 'prosemirror-state';
import { RTFCast } from '../../../../fields/Types';
+import { Mark } from 'prosemirror-model';
export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>() {
@observable journalDate: string;
@@ -17,6 +18,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
_ref = React.createRef<FormattedTextBox>(); // reference to the formatted textbox
predictiveTextRange: { from: number; to: number } | null = null; // where predictive text starts and ends
private predictiveText: string | null = ' ... why?';
+ private prePredictiveMarks: Mark[] = [];
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(DailyJournal, fieldStr);
@@ -84,6 +86,27 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
}
/**
+ * Method to set initial date of document in the calendar view
+ */
+
+ @action setInitialDateRange() {
+ if (!this.dataDoc.date_range && this.journalDate) {
+ const parsedDate = new Date(this.journalDate);
+ if (!isNaN(parsedDate.getTime())) {
+ const localStart = new Date(parsedDate.getFullYear(), parsedDate.getMonth(), parsedDate.getDate());
+ const localEnd = new Date(localStart); // same day
+
+ this.dataDoc.date_range = `${localStart.toISOString()}|${localEnd.toISOString()}`;
+ this.dataDoc.allDay = true;
+
+ console.log('Set date_range and allDay on journal (from local date):', this.dataDoc.date_range);
+ } else {
+ console.log('Could not parse journalDate:', this.journalDate);
+ }
+ }
+ }
+
+ /**
* Tracks user typing text inout into the node, to call the insert predicted
* text function when appropriate (i.e. when the user stops typing)
*/
@@ -94,6 +117,22 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
if (this.typingTimeout) clearTimeout(this.typingTimeout);
+ const { state } = editorView;
+ const cursorPos = state.selection.from;
+
+ // characters before cursor
+ const triggerText = state.doc.textBetween(Math.max(0, cursorPos - 4), cursorPos);
+
+ if (triggerText === '/ask') {
+ // remove /ask text
+ const tr = state.tr.delete(cursorPos - 4, cursorPos);
+ editorView.dispatch(tr);
+
+ // insert predicted question
+ this.insertPredictiveQuestion();
+ return;
+ }
+
this.typingTimeout = setTimeout(() => {
this.insertPredictiveQuestion();
}, 3500);
@@ -129,11 +168,17 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
// Only insert if we're at end of node, or there's a newline node after
if (!isAtEndOfParent && !hasNewlineAfter) return;
- const fontSizeMark = schema.marks.pFontSize.create({ fontSize: '14px' });
+ // Save current marks at cursor
+ const currentMarks = state.storedMarks || resolvedPos.marks();
+ this.prePredictiveMarks = [...currentMarks];
+
+ // color and italics are preset for predictive question, font and size are adaptive
const fontColorMark = schema.marks.pFontColor.create({ fontColor: 'lightgray' });
const fontItalicsMark = schema.marks.em.create();
+ const fontSizeMark = this.prePredictiveMarks.find(m => m.type.name === 'pFontSize');
+ const fontFamilyMark = this.prePredictiveMarks.find(m => m.type.name === 'pFontFamily'); // if applicable
- this.predictiveText = ' ...'; // placeholder for now
+ this.predictiveText = ' ...'; // placeholder
const fullTextUpToCursor = state.doc.textBetween(0, state.selection.to, '\n', '\n');
const gptPrompt = `Given the following incomplete journal entry, generate a single 2-5 word question that continues the user's thought:\n\n"${fullTextUpToCursor}"`;
@@ -142,10 +187,10 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
// styled text node
const text = ` ... ${res.trim()}`;
- const predictedText = schema.text(text, [fontSizeMark, fontColorMark, fontItalicsMark]);
+ const predictedText = schema.text(text, [fontColorMark, fontItalicsMark, ...(fontSizeMark ? [fontSizeMark] : []), ...(fontFamilyMark ? [fontFamilyMark] : [])]);
// Insert styled text at cursor position
- const transaction = state.tr.insert(insertPos, predictedText).setStoredMarks([state.schema.marks.pFontColor.create({ fontColor: 'gray' })]); // should probably instead inquire marks before predictive prompt
+ const transaction = state.tr.insert(insertPos, predictedText).setStoredMarks(this.prePredictiveMarks);
dispatch(transaction);
this.predictiveText = text;
@@ -172,11 +217,16 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
const fontSizeMark = state.schema.marks.pFontSize.create({ fontSize: '14px' });
const fontColorMark = state.schema.marks.pFontColor.create({ fontColor: 'gray' });
tr.setStoredMarks([]);
- tr.setStoredMarks([fontSizeMark, fontColorMark]);
+ if (this.prePredictiveMarks.length > 0) {
+ tr.setStoredMarks(this.prePredictiveMarks);
+ } else {
+ tr.setStoredMarks([fontSizeMark, fontColorMark]);
+ }
dispatch(tr);
this.predictiveText = null;
+ this.prePredictiveMarks = [];
return false;
}
return true;
@@ -217,6 +267,7 @@ export class DailyJournal extends ViewBoxAnnotatableComponent<FieldViewProps>()
console.log('Journal title and text are default. Initializing...');
this.setDailyTitle();
this.setDailyText();
+ this.setInitialDateRange();
} else {
console.log('Journal already has content. Skipping initialization.');
}