From ddf35f6b406a2f2e8c27c2c65d15206eaa3ddbe6 Mon Sep 17 00:00:00 2001 From: zaultavangar Date: Sat, 16 Dec 2023 14:15:56 -0500 Subject: starting calendar feature --- src/client/util/CalendarManager.scss | 62 ++++++ src/client/util/CalendarManager.tsx | 229 ++++++++++++++++++++++ src/client/views/DocumentButtonBar.tsx | 21 +- src/client/views/MainView.tsx | 2 + src/client/views/nodes/MapBox/MapBox.scss | 3 +- src/client/views/nodes/MapBox/MapBox.tsx | 42 ++-- src/client/views/nodes/MapBox/MapboxApiUtility.ts | 98 ++++++--- 7 files changed, 407 insertions(+), 50 deletions(-) create mode 100644 src/client/util/CalendarManager.scss create mode 100644 src/client/util/CalendarManager.tsx (limited to 'src') diff --git a/src/client/util/CalendarManager.scss b/src/client/util/CalendarManager.scss new file mode 100644 index 000000000..60610f298 --- /dev/null +++ b/src/client/util/CalendarManager.scss @@ -0,0 +1,62 @@ +.calendar-interface{ + width: 600px; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 7px; + padding: 10px; + + .selected-doc-title{ + font-size: 1.4em; + } + + .creation-type-container{ + display: flex; + justify-content: center; + align-items: center; + align-self: center; + gap: 12px; + + .calendar-creation{ + cursor: pointer; + } + + .calendar-creation-selected{ + border-bottom: 2px solid white; + } + } + + .choose-calendar-container{ + margin-top: 10px; + align-self: center; + width: 60%; + + .MuiFilledInput-input{ + padding: 10px; + } + } + + .date-range-picker-container{ + margin-top: 5px; + align-self:center; + + .react-date-range{ + + } + } + + .create-button-container{ + + margin-top: 5px; + align-self: center; + + .button-content{ + font-size: 1.2em; + padding: 10px; + border-radius: 5px; + background-color: #EFF2F7; + } + + } +} diff --git a/src/client/util/CalendarManager.tsx b/src/client/util/CalendarManager.tsx new file mode 100644 index 000000000..39ba41652 --- /dev/null +++ b/src/client/util/CalendarManager.tsx @@ -0,0 +1,229 @@ +import * as React from 'react'; +import './CalendarManager.scss'; +import { observer } from "mobx-react"; +import { action, computed, observable, runInAction } from 'mobx'; +import { Doc } from '../../fields/Doc'; +import { DocumentView } from '../views/nodes/DocumentView'; +import { DictationOverlay } from '../views/DictationOverlay'; +import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; +import { MainViewModal } from '../views/MainViewModal'; +import { TextField } from '@material-ui/core'; +import Select from 'react-select'; +import { SettingsManager } from './SettingsManager'; +import { StrCast } from '../../fields/Types'; +import { SelectionManager } from './SelectionManager'; +import { DocumentManager } from './DocumentManager'; +import { DocData } from '../../fields/DocSymbols'; +import { DateRange, Range, RangeKeyDict } from 'react-date-range'; +import { Button } from 'browndash-components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconLookup, faPlus } from '@fortawesome/free-solid-svg-icons'; +import 'react-date-range/dist/styles.css'; +import 'react-date-range/dist/theme/default.css'; + +type CreationType = 'new-calendar' | 'existing-calendar' | 'manage-calendars'; + +@observer +export class CalendarManager extends React.Component<{}> { + public static Instance: CalendarManager; + @observable private isOpen = false; + @observable private targetDoc: Doc | undefined; // the target document + @observable private targetDocView: DocumentView | undefined; // the DocumentView of the target doc + @observable private dialogueBoxOpacity = 1; // for the modal + @observable private overlayOpacity = 0.4; // for the modal + + @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used + + @observable private creationType: CreationType = 'new-calendar'; + + @observable + calendarName: string = ""; + + @action + setInterationType = (type: CreationType) => { + this.creationType = type; + } + + + public open = (target?: DocumentView, target_doc?: Doc) => { + console.log('hi'); + runInAction(() => { + this.targetDoc = target_doc || target?.props.Document; + this.targetDocView = target; + DictationOverlay.Instance.hasActiveModal = true; + this.isOpen = this.targetDoc !== undefined; + }) + }; + + public close = action(() => { + this.isOpen = false; + TaskCompletionBox.taskCompleted = false; + setTimeout( + action(() => { + DictationOverlay.Instance.hasActiveModal = false; + this.targetDoc = undefined; + }), 500 + ); + this.layoutDocAcls = false; + }); + + constructor(props: {}) { + super(props); + CalendarManager.Instance = this; + } + + componentDidMount(): void { + + } + + private focusOn = (contents: string) => { + const title = this.targetDoc ? StrCast(this.targetDoc.title) : ''; + const docs = SelectionManager.Views().length > 1 ? SelectionManager.Views().map(docView => docView.props.Document) : [this.targetDoc]; + return ( + { + if (this.targetDoc && this.targetDocView && docs.length === 1) { + DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true }); + } + }} + onPointerEnter={action(() => { + if (docs.length) { + docs.forEach(doc => doc && Doc.BrushDoc(doc)); + this.dialogueBoxOpacity = 0.1; + this.overlayOpacity = 0.1; + } + })} + onPointerLeave={action(() => { + if (docs.length) { + docs.forEach(doc => doc && Doc.UnBrushDoc(doc)); + this.dialogueBoxOpacity = 1; + this.overlayOpacity = 0.4; + } + })}> + {contents} + + ); + }; + + @observable + selectedDateRange: Range[] = [{ + startDate: new Date(), + endDate: undefined, + key: 'selection' + }] + + @action + setSelectedDateRange = (range: Range[]) => { + this.selectedDateRange = range; + } + + @computed + get createButtonActive() { + if (this.calendarName.length === 0) return false // disabled if no calendar name + let startDate: Date | undefined; + let endDate: Date | undefined; + try { + startDate = this.selectedDateRange[0].startDate; + endDate = this.selectedDateRange[0].endDate; + } catch (e: any){ + return false; // disabled + } + if (!startDate || !endDate) return false; // disabled if any is undefined + return true; + } + + @computed + get calendarInterface(){ + let docs = SelectionManager.Views().length < 2 ? [this.targetDoc] : SelectionManager.Views().map(docView => docView.rootDoc); + const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; + + const currentDate = new Date(); + + return ( +
+

+ {this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')} +

+
+
this.setInterationType('new-calendar')} + > + Add to New Calendar +
+
this.setInterationType('existing-calendar')} + > + Add to Existing calendar +
+
+
+ {this.creationType === 'new-calendar' ? + + : + + } +
+
+ this.setSelectedDateRange([item.selection])} + ranges={this.selectedDateRange} + // onChange={this.handleSelect} + /> +
+
+
+ +
+ ) + } + + render() { + return ( + + ) + } +} \ No newline at end of file diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index bd82f7782..ccd459f0d 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -1,4 +1,4 @@ -import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, observable, runInAction } from 'mobx'; @@ -14,6 +14,7 @@ import { DragManager } from '../util/DragManager'; import { IsFollowLinkScript } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SharingManager } from '../util/SharingManager'; +import { CalendarManager } from '../util/CalendarManager'; import { undoBatch, UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { TabDocView } from './collections/TabDocView'; @@ -28,6 +29,8 @@ import { GoogleRef } from './nodes/formattedText/FormattedTextBox'; import { PinProps } from './nodes/trails'; import { TemplateMenu } from './TemplateMenu'; import React = require('react'); +import { faCalendarDays } from '@fortawesome/free-solid-svg-icons'; + const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -443,6 +446,21 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV ); } + @computed + get calendarButton(){ + const targetDoc = this.view0?.props.Document; + return !targetDoc ? null : ( + Open calendar menu}> +
{ + console.log('hi: ', CalendarManager.Instance) + CalendarManager.Instance.open(this.view0, targetDoc)} + }> + +
+
+ ) + } + @observable _isRecording = false; _stopFunc: () => void = emptyFunction; @computed @@ -606,6 +624,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV {!SelectionManager.Views()?.some(v => v.allLinks.length) ? null :
{this.followLinkButton}
}
{this.pinButton}
{this.recordButton}
+
{this.calendarButton}
{!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null :
{this.shareButton}
} {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : (
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 13f7dc896..d45f24930 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -72,6 +72,7 @@ import { PropertiesView } from './PropertiesView'; import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider'; import { TopBar } from './topbar/TopBar'; import { DirectionsAnchorMenu } from './nodes/MapBox/DirectionsAnchorMenu'; +import { CalendarManager } from '../util/CalendarManager'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -1038,6 +1039,7 @@ export class MainView extends React.Component { {this.inkResources} + diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index e25261729..ba1e99f84 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -88,12 +88,11 @@ flex-direction: column; justify-content: flex-start; align-items: flex-start; - position: absolute; background-color: rgb(187, 187, 187); padding: 10px; border-top-right-radius: 5px; border-bottom-right-radius: 5px; - width: 100%; + position: absolute; #route-to-animate-title { font-size: 1.25em; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index f4526c490..25299532a 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -895,19 +895,21 @@ export class MapBox extends ViewBoxAnnotatableComponent { - const mapRoute = Docs.Create.MapRouteDocument( - false, - [], - {title: `${origin} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates)}, - ); - this.addDocument(mapRoute, this.annotationKey); - if (createPinForDestination) { - this.createPushpin(destination.center[1], destination.center[0], destination.place_name); + createMapRoute = undoable((coordinates: Position[], originName: string, destination: any, createPinForDestination: boolean) => { + if (originName !== destination.place_name){ + const mapRoute = Docs.Create.MapRouteDocument( + false, + [], + {title: `${originName} --> ${destination.place_name}`, routeCoordinates: JSON.stringify(coordinates)}, + ); + this.addDocument(mapRoute, this.annotationKey); + if (createPinForDestination) { + this.createPushpin(destination.center[1], destination.center[0], destination.place_name); + } + return mapRoute; } - return mapRoute; - - // mapMarker.infoWindowOpen = true; + // TODO: Display error that can't create route to same location + }, 'createmaproute'); searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); @@ -981,6 +983,7 @@ export class MapBox extends ViewBoxAnnotatableComponent { + this.featuresFromGeocodeResults = []; if (this._mapRef.current){ const features = this._mapRef.current.queryRenderedFeatures( e.point, { @@ -1179,6 +1182,7 @@ export class MapBox extends ViewBoxAnnotatableComponent string[]) => null; return ( @@ -1686,11 +1691,12 @@ export class MapBox extends ViewBoxAnnotatableComponent + {/* style={{ transformOrigin: "top left", transform: `scale(${scale})`, width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> */}
{renderAnnotations(this.transparentFilter)}
{renderAnnotations(this.opaqueFilter)} {SnappingManager.GetIsDragging() ? null : renderAnnotations()} {!this.routeToAnimate && -
+
} {this.routeToAnimate && -
+
{StrCast(this.routeToAnimate.title)}
@@ -1809,7 +1815,13 @@ export class MapBox extends ViewBoxAnnotatableComponent | undefined> => { try { - const drivingQuery = await fetch( - `${MAPBOX_DIRECTIONS_BASE_URL}/driving/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); - const cyclingQuery = await fetch( - `${MAPBOX_DIRECTIONS_BASE_URL}/cycling/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + const directionsPromises: Promise[] = []; + const transportationTypes: TransportationType[] = ['driving', 'cycling', 'walking']; - const walkingQuery = await fetch( - `${MAPBOX_DIRECTIONS_BASE_URL}/walking/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + transportationTypes.forEach((type) => { + directionsPromises.push( + fetch( + `${MAPBOX_DIRECTIONS_BASE_URL}/${type}/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}` + ).then((response) => response.json()) + ); + }); - const drivingJson = await drivingQuery.json(); - const cyclingJson = await cyclingQuery.json(); - const walkingJson = await walkingQuery.json(); - - console.log("Driving: ", drivingJson); - console.log("Cycling: ", cyclingJson); - console.log("Waling: ", walkingJson); - - const routeMap = { - 'driving': drivingJson.routes[0], - 'cycling': cyclingJson.routes[0], - 'walking': walkingJson.routes[0] - } + const results = await Promise.all(directionsPromises); const routeInfoMap: Record = { 'driving': {}, 'cycling': {}, 'walking': {}, - }; - - Object.entries(routeMap).forEach(([key, routeData]) => { - const transportationTypeKey = key as TransportationType; - const geometry = routeData.geometry; - const coordinates = geometry.coordinates; - - console.log(coordinates); - - routeInfoMap[transportationTypeKey] = { + }; + + transportationTypes.forEach((type, index) => { + const routeData = results[index].routes[0]; + if (routeData) { + const geometry = routeData.geometry; + const coordinates = geometry.coordinates; + + routeInfoMap[type] = { duration: this.secondsToMinutesHours(routeData.duration), distance: this.metersToMiles(routeData.distance), - coordinates: coordinates + coordinates: coordinates, + }; } - }) + }); return routeInfoMap; @@ -102,4 +93,47 @@ export class MapboxApiUtility { return `${parseFloat((meters/1609.34).toFixed(2))} mi`; } -} \ No newline at end of file +} + +// const drivingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/driving/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const cyclingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/cycling/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const walkingQuery = await fetch( +// `${MAPBOX_DIRECTIONS_BASE_URL}/walking/${origin[0]},${origin[1]};${destination[0]},${destination[1]}?steps=true&geometries=geojson&access_token=${MAPBOX_ACCESS_TOKEN}`); + +// const drivingJson = await drivingQuery.json(); +// const cyclingJson = await cyclingQuery.json(); +// const walkingJson = await walkingQuery.json(); + +// console.log("Driving: ", drivingJson); +// console.log("Cycling: ", cyclingJson); +// console.log("Waling: ", walkingJson); + +// const routeMap = { +// 'driving': drivingJson.routes[0], +// 'cycling': cyclingJson.routes[0], +// 'walking': walkingJson.routes[0] +// } + +// const routeInfoMap: Record = { +// 'driving': {}, +// 'cycling': {}, +// 'walking': {}, +// }; + +// Object.entries(routeMap).forEach(([key, routeData]) => { +// const transportationTypeKey = key as TransportationType; +// const geometry = routeData.geometry; +// const coordinates = geometry.coordinates; + +// console.log(coordinates); + +// routeInfoMap[transportationTypeKey] = { +// duration: this.secondsToMinutesHours(routeData.duration), +// distance: this.metersToMiles(routeData.distance), +// coordinates: coordinates +// } +// }) \ No newline at end of file -- cgit v1.2.3-70-g09d2