diff --git a/packages/app/package.json b/packages/app/package.json index 76dfa73..d651270 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@morten-olsen/bob", - "version": "3.0.0-alpha.1", + "version": "3.0.0", "main": "./src/index.ts", "homepage": "/bob-the-algorithm", "scripts": { @@ -37,7 +37,7 @@ "expo-font": "~10.0.4", "expo-linking": "~3.0.0", "expo-location": "~14.0.1", - "expo-random": "^12.1.2", + "expo-random": "~12.1.1", "expo-splash-screen": "~0.14.0", "expo-status-bar": "~1.2.0", "expo-task-manager": "~10.1.0", @@ -50,8 +50,8 @@ "react-native-calendar-strip": "^2.2.5", "react-native-calendars": "^1.1284.0", "react-native-collapsible": "^1.6.0", - "react-native-gesture-handler": "^2.4.2", - "react-native-get-random-values": "^1.8.0", + "react-native-gesture-handler": "~2.1.0", + "react-native-get-random-values": "~1.7.0", "react-native-safe-area-context": "3.3.2", "react-native-screens": "~3.10.1", "react-native-web": "0.17.1", diff --git a/packages/app/src/features/appointments/provider.tsx b/packages/app/src/features/appointments/provider.tsx index c8188c2..2a80d09 100644 --- a/packages/app/src/features/appointments/provider.tsx +++ b/packages/app/src/features/appointments/provider.tsx @@ -1,7 +1,9 @@ import { useAsync } from "#/features/async"; -import { ReactNode } from "react" +import { ReactNode, useState } from "react" import { Platform } from "react-native"; import { AppointmentsContext, AppointmentsContextValue, AppointmentsStatus } from './context'; +import { NativeIntegtration } from "./providers/native"; +import { IntegrationProvider } from "./providers/provider"; type AppointmentsProviderProps = { children: ReactNode; @@ -10,12 +12,22 @@ type AppointmentsProviderProps = { const AppointmentsProvider: React.FC = ({ children, }) => { + const [provider, setProvider] = useState(); const [value] = useAsync( async () => { if (Platform.OS !== 'ios') { return { status: AppointmentsStatus.unavailable }; } - return { status: AppointmentsStatus.unavailable }; + const iosProvider = new NativeIntegtration(); + const setupSuccess = await iosProvider.setup(); + if (!setupSuccess) { + return { status: AppointmentsStatus.unavailable }; + } + setProvider(iosProvider); + return { + status: AppointmentsStatus.approved, + getDay: iosProvider.getDay, + }; }, [], ); diff --git a/packages/app/src/features/appointments/providers/native.ts b/packages/app/src/features/appointments/providers/native.ts new file mode 100644 index 0000000..45ec0a9 --- /dev/null +++ b/packages/app/src/features/appointments/providers/native.ts @@ -0,0 +1,55 @@ +import { Appointment, TaskType, timeUtils } from "#/features/data"; +import { Day, dayUtils } from "#/features/day"; +import { set } from "date-fns"; +import { EntityTypes, getCalendarsAsync, getEventsAsync, requestCalendarPermissionsAsync } from "expo-calendar"; +import { Platform } from "react-native"; +import { IntegrationProvider } from "./provider"; + +class NativeIntegtration implements IntegrationProvider { + getAllCalendars = async () => { + let calendars = await getCalendarsAsync(EntityTypes.EVENT); + return calendars; + }; + + public setup = async () => { + if (Platform.OS !== 'ios') { + return false; + } + const { status } = await requestCalendarPermissionsAsync(); + if (status !== 'granted') { + return false; + } + return true; + } + + public getDay = async (day: Day) => { + const selectedCalendars = await this.getAllCalendars(); + const start = dayUtils.dayToDate(day) + const end = set(start, { + hours: 24, + minutes: 0, + seconds: 0, + milliseconds: 0, + }); + const events = await getEventsAsync(selectedCalendars.map(c => c.id), start, end!) + return events.filter(a => !a.allDay).map(e => { + const startTime = timeUtils.dateToTime(new Date(e.startDate)); + const endTime = timeUtils.dateToTime(new Date(e.endDate)); + const duration = timeUtils.timeToMinutes(endTime) - timeUtils.timeToMinutes(startTime); + return { + id: e.id, + type: TaskType.appointment, + calendarId: e.calendarId, + title: e.title, + required: true, + startTime: { + min: startTime, + max: startTime, + }, + duration, + } + }); + } +} + +export { NativeIntegtration }; diff --git a/packages/app/src/features/appointments/providers/provider.ts b/packages/app/src/features/appointments/providers/provider.ts new file mode 100644 index 0000000..156cb3c --- /dev/null +++ b/packages/app/src/features/appointments/providers/provider.ts @@ -0,0 +1,10 @@ +import { Appointment } from "#/features/data"; +import { Day } from "#/features/day"; + +interface IntegrationProvider { + getAllCalendars: () => Promise; + setup: () => Promise; + getDay: (day: Day) => Promise; +} + +export { IntegrationProvider }; diff --git a/packages/app/src/features/data/utils.ts b/packages/app/src/features/data/utils.ts index cc24d39..c8a506b 100644 --- a/packages/app/src/features/data/utils.ts +++ b/packages/app/src/features/data/utils.ts @@ -47,6 +47,13 @@ const timeToDate = (time: Time) => { return new Date(0, 0, 0, time.hour, time.minute); } +const dateToTime = (date: Date) => { + return { + hour: date.getHours(), + minute: date.getMinutes(), + }; +} + const add = (a: Time, b: Time | number) => { const toAdd = typeof b === 'number' ? b : b.hour * 60 + b.minute const current = a.hour * 60 + a.minute + toAdd; @@ -60,6 +67,7 @@ const timeUtils = { largerThan, timeToMinutes, timeToDate, + dateToTime, max, add, }; diff --git a/packages/app/src/features/planner/hooks.ts b/packages/app/src/features/planner/hooks.ts index 44ab009..9c56096 100644 --- a/packages/app/src/features/planner/hooks.ts +++ b/packages/app/src/features/planner/hooks.ts @@ -1,5 +1,5 @@ import { buildGraph, Status, Strategies } from "./algorithm/build-graph"; -import { useContext } from "react"; +import { useContext, useMemo, useState } from "react"; import { add } from 'date-fns'; import { PlannerContext } from "./context"; import { Task, Time, UserLocation } from "../data"; @@ -103,6 +103,7 @@ const usePreparePlan = () => { }, duration: firstValue(override?.duration, task.duration), required: firstValue(override?.required, task.required), + locations: firstValue(override?.locations, task.locations) || undefined, } return result; }).filter(Boolean).map(a => a as Exclude); @@ -137,6 +138,7 @@ export const useSetPlanOptions = () => { export const usePlan = () => { const [preparePlan] = usePreparePlan(); + const [result, setResult] = useState(undefined); const getTransition = useGetTransition(); const options = usePlanOptions(); const createPlan = useAsyncCallback( @@ -154,7 +156,9 @@ export const usePlan = () => { }), {} as {[name: string]: PlanResultDay}) } const update = (next: PlanResult) => { + setResult(next); result = next; + return next; } for (let day of prepared.days) { const id = dayUtils.toId(day.day); @@ -211,12 +215,19 @@ export const usePlan = () => { }) } - return { + return update({ ...result, impossible: prepared.goals, - }; + }); }, [preparePlan, getTransition, options], ); - return createPlan; + const output = useMemo( + () => [ + createPlan[0], + { ...createPlan[1], result }, + ], + [createPlan, result], + ); + return output; } diff --git a/packages/app/src/ui/containers/plan/day/index.tsx b/packages/app/src/ui/containers/plan/day/index.tsx index 866852a..171d08c 100644 --- a/packages/app/src/ui/containers/plan/day/index.tsx +++ b/packages/app/src/ui/containers/plan/day/index.tsx @@ -1,18 +1,25 @@ import { dayUtils } from "#/features/day"; import { PlanResultDay } from "#/features/planner" import { Body1, Jumbo } from "@morten-olsen/ui"; +import styled from "styled-components/native"; import { PlanDayTask } from "./task"; type Props = { day: PlanResultDay; }; +const Transit = styled(Body1)` + align-items: center; + justify-content: center; + text-align: center; +`; + const PlanDay: React.FC = ({ day }) => { if (day.status === 'waiting') { return <> } if (day.status === 'running') { - return Running + return Running, nodes: {day.nodes}, strategy: {day.strategy} } return ( <> @@ -20,7 +27,7 @@ const PlanDay: React.FC = ({ day }) => { if (item.type === 'task') { return } - return Transit {item.from.title} to {item.to.title} + return {item.from.title} → {item.to.title} })} ) diff --git a/packages/app/src/ui/containers/plan/day/task.tsx b/packages/app/src/ui/containers/plan/day/task.tsx index a4d165b..c84d810 100644 --- a/packages/app/src/ui/containers/plan/day/task.tsx +++ b/packages/app/src/ui/containers/plan/day/task.tsx @@ -11,47 +11,6 @@ type Props = { onPress?: () => void; }; -const Time = styled.Text<{background : string}>` - font-size: 10px; - font-weight: bold; -`; - -const TimeBox = styled.View<{ - background: string; -}>` - margin-right: 10px; - width: 50px; - height: 100%; - align-items: center; - justify-content: center; -`; - -const Filler = styled.View` - margin: 10px; - width: 50px; - height: 50px; - align-items: center; - justify-content: center; -`; - -const Block = styled.View<{ - background: string; - height: number; -}>` - background: ${({ background }) => background}; - height: ${({ height }) => height / 3}px; - max-height: 100px; - margin: 5px; - flex-direction: row; - align-items: center; - border-radius: 15px; - overflow: hidden; - border: solid 1px ${({ background }) => background === 'transparent' ? background : chroma(background).darken(0.3).hex()}; -`; - -const Main = styled.View` - flex: 1; -` const Touch = styled.TouchableOpacity` `; @@ -68,7 +27,6 @@ const PlanDayTask: React.FC = ({ task, onPress }) => { const view = ( { value={startTimeOverride} onChange={setStartTimeOverride} /> - {appointmentStatus === AppointmentsStatus.rejected && ( + {appointmentStatus === AppointmentsStatus.approved && ( )} diff --git a/packages/app/src/ui/screens/task/overrides.tsx b/packages/app/src/ui/screens/task/overrides.tsx index a6ea8a4..86ef0e3 100644 --- a/packages/app/src/ui/screens/task/overrides.tsx +++ b/packages/app/src/ui/screens/task/overrides.tsx @@ -49,8 +49,6 @@ const OverrideSetScreen: React.FC = () => { const [duration, setDuration] = useState(''); const [hasLocation, setHasLocation] = useState(false); const [selectedLocations, setSelectedLocations] = useState([]); - const [hasDays, setHasDays] = useState(false); - const [selectedDays, setSelectedDays] = useState([]); useEffect( () => { @@ -88,8 +86,6 @@ const OverrideSetScreen: React.FC = () => { duration, hasLocation, selectedLocations, - hasDays, - selectedDays, ], ); @@ -101,6 +97,10 @@ const OverrideSetScreen: React.FC = () => { [id, clearOverrides], ); + if (!task) { + return <>; + } + return ( @@ -139,20 +139,6 @@ const OverrideSetScreen: React.FC = () => { disabledText="Anywhere" enabledText="Specific location" /> - ({ - title: day.name - })} - getKey={day => day.id.toString()} - setEnabled={setHasDays} - disabledText="Any day" - enabledText="Specific days" - /> - {location} - {title} + {!!location && {location}} + {title} {time} ); diff --git a/packages/ui/src/components/date/strip/index.tsx b/packages/ui/src/components/date/strip/index.tsx index ee7bc4d..71a93d1 100644 --- a/packages/ui/src/components/date/strip/index.tsx +++ b/packages/ui/src/components/date/strip/index.tsx @@ -70,7 +70,6 @@ const CalendarStrip: React.FC = ({ start, selected, onSelect }) => { }, [firstDayOfWeek]); const monthLabel = useMemo(() => { - console.log(months); if (months[0] === months[1]) { return new Date(0, months[0], 1).toLocaleString('en-us', { month: 'long' }); } else { diff --git a/packages/ui/src/theme/light.ts b/packages/ui/src/theme/light.ts index c8b7b7b..5fc1d2f 100644 --- a/packages/ui/src/theme/light.ts +++ b/packages/ui/src/theme/light.ts @@ -50,7 +50,7 @@ const light: Theme = { }, font: { family: Platform.OS === 'web' ? 'Montserrat' : undefined, - baseSize: Platform.OS === 'web' ? 12 : 10, + baseSize: Platform.OS === 'web' ? 12 : 11, }, }; diff --git a/yarn.lock b/yarn.lock index 76a11c7..823853b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4430,7 +4430,7 @@ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== -"@types/react-dom@^18.0.3": +"@types/react-dom@^18.0.3", "@types/react-dom@~18.0.3": version "18.0.4" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.4.tgz#dcbcadb277bcf6c411ceff70069424c57797d375" integrity sha512-FgTtbqPOCI3dzZPZoC2T/sx3L34qxy99ITWn4eoSA95qPyXDMH0ALoAqUp49ITniiJFsXUVBtalh/KffMpg21Q== @@ -4451,16 +4451,7 @@ dependencies: "@types/react" "*" -"@types/react@*": - version "18.0.9" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" - integrity sha512-9bjbg1hJHUm4De19L1cHiW0Jvx3geel6Qczhjd0qY5VKVE2X5+x77YxAepuCwVh4vrgZJdgEJw48zrhRIeF4Nw== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/react@~17.0.21": +"@types/react@*", "@types/react@~17.0.21": version "17.0.45" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.45.tgz#9b3d5b661fd26365fefef0e766a1c6c30ccf7b3f" integrity sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg== @@ -9026,10 +9017,10 @@ expo-pwa@0.0.119: commander "2.20.0" update-check "1.5.3" -expo-random@^12.1.2: - version "12.2.0" - resolved "https://registry.yarnpkg.com/expo-random/-/expo-random-12.2.0.tgz#a3c8a9ce84ef2c85900131d96eea6c7123285482" - integrity sha512-SihCGLmDyDOALzBN8XXpz2hCw0RSx9c4/rvjcS4Bfqhw6luHjL2rHNTLrFYrPrPRmG1jHM6dXXJe/Zm8jdu+2g== +expo-random@~12.1.1: + version "12.1.2" + resolved "https://registry.yarnpkg.com/expo-random/-/expo-random-12.1.2.tgz#ed4e7f85728b609bcb373bc4c4a1a05192ec44b9" + integrity sha512-ajB+Mwff9PdglsyLliaU4K9BtVwKvAVVI2hQhnvlS3QgsAhHf+jQVUfAysQJHuioF6ADMEsab/kRUy4Dy03aoQ== dependencies: base64-js "^1.3.0" @@ -16126,10 +16117,10 @@ react-element-to-jsx-string@^14.3.4: is-plain-object "5.0.0" react-is "17.0.2" -react-error-overlay@^6.0.9: - version "6.0.11" - resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.11.tgz#92835de5841c5cf08ba00ddd2d677b6d17ff9adb" - integrity sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg== +react-error-overlay@6.0.9, react-error-overlay@^6.0.9: + version "6.0.9" + resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" + integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== react-feather@^2.0.9: version "2.0.9" @@ -16219,6 +16210,17 @@ react-native-gesture-handler@^2.4.2: lodash "^4.17.21" prop-types "^15.7.2" +react-native-gesture-handler@~2.1.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.1.3.tgz#b96f1e61932d5062cb1023259e1649d65f78338b" + integrity sha512-y5W2MVB+J6vjIT/mUidDv0BqVRbWXn0cP7R2o6fsSYsHh9M0btT979+bCI7nPuhmRHjkhg5xCm4HNMIH0IQO4w== + dependencies: + "@egjs/hammerjs" "^2.0.17" + hoist-non-react-statics "^3.3.0" + invariant "^2.2.4" + lodash "^4.17.21" + prop-types "^15.7.2" + react-native-get-random-values@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16" @@ -16226,6 +16228,13 @@ react-native-get-random-values@^1.8.0: dependencies: fast-base64-decode "^1.0.0" +react-native-get-random-values@~1.7.0: + version "1.7.2" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.7.2.tgz#60a9b6497d22e713779b71139f016a5fcec7ac04" + integrity sha512-28KRYGpIG/upV8+k/qFA+TwGW+yGjmtOHaCduJHpOQK1QUTyhiA6E2IgL4UvvU2dybeCTYFmUi9wcEQ0GiWe5g== + dependencies: + fast-base64-decode "^1.0.0" + react-native-safe-area-context@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-3.3.2.tgz#9549a2ce580f2374edb05e49d661258d1b8bcaed"