This commit is contained in:
Morten Olsen
2022-05-10 22:45:38 +02:00
committed by Morten Olsen
parent 8259232a83
commit 6181eeb0c8
44 changed files with 1099 additions and 220 deletions

61
.github/workflows/expo-main.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Expo Publish
on:
workflow_dispatch:
push:
branches:
- main
- v3
jobs:
publish-web:
name: Publish web version
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1 # If you're using actions/checkout@v2 you must set persist-credentials to false in most cases for the deployment to work correctly.
with:
persist-credentials: false
- name: Install and Build 🔧
run: |
yarn install
yarn expo build:web
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
folder: web-build
publish-native:
name: Publish native versions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14.x
- uses: expo/expo-github-action@v6
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: |
git config --global user.email "github-action@example.com"
git config --global user.name "Github Bot"
yarn version --new-version=$BUILD_VERSION
env:
BUILD_VERSION: 1.${{ github.run_id }}.${{ github.run_number }}
- run: yarn install
- run: echo $BUILD_VERSION
# - run: eas build -p android --non-interactive
- run: eas build -p ios --non-interactive
env:
EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }}
- run: eas submit --platform ios --non-interactive --latest
env:
EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }}

View File

@@ -1,25 +0,0 @@
name: Build and Deploy web
on:
workflow_dispatch:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2.3.1
with:
persist-credentials: false
- name: Install and Build 🔧
run: |
yarn install
NODE_ENV=production yarn expo build:web
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
folder: web-build

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/*.log
node_modules/
.expo/
dist/

51
app.config.js Normal file
View File

@@ -0,0 +1,51 @@
const pkg = require('./package.json');
const config = {
expo: {
name: 'Bob',
slug: 'bob',
version: pkg.version,
orientation: 'portrait',
icon: './assets/images/icon.png',
scheme: 'bobthealgorithm',
userInterfaceStyle: 'automatic',
splash: {
image: './assets/images/splash.png',
resizeMode: 'contain',
backgroundColor: '#ffffff',
},
updates: {
fallbackToCacheTimeout: 0,
},
assetBundlePatterns: ['**/*'],
ios: {
supportsTablet: true,
bundleIdentifier: 'pro.mortenolsen.bob',
buildNumber: pkg.version,
config: {
usesNonExemptEncryption: false,
},
},
android: {
adaptiveIcon: {
foregroundImage: './assets/images/adaptive-icon.png',
backgroundColor: '#ffffff',
},
package: 'pro.mortenolsen.bob',
},
web: {
favicon: './assets/images/favicon.png',
},
// hooks: {
// postPublish: [
// {
// file: 'sentry-expo/upload-sourcemaps',
// config: {
// setCommits: true,
// },
// },
// ],
// },
},
};
module.exports = config;

View File

@@ -1,34 +0,0 @@
{
"expo": {
"name": "bob",
"slug": "bob",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "myapp",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/images/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/images/adaptive-icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/images/favicon.png"
}
}
}

25
eas.json Normal file
View File

@@ -0,0 +1,25 @@
{
"cli": {
"version": ">= 0.42.4"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
}
},
"submit": {
"production": {
"ios": {
"appleId": "morten@olsen.pro",
"appleTeamId": "D2944KU2BE",
"ascAppId": "1623552387"
}
}
}
}

View File

@@ -46,6 +46,7 @@
"react-dom": "17.0.1",
"react-native": "0.64.3",
"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",
@@ -61,7 +62,6 @@
"@types/chroma-js": "^2.1.3",
"@types/react": "~17.0.21",
"@types/react-dom": "^18.0.3",
"@types/react-native": "^0.67.6",
"@types/styled-components-react-native": "^5.1.3",
"babel-plugin-module-resolver": "^4.1.0",
"expo-cli": "^5.4.3",

View File

@@ -11,7 +11,7 @@ const App: React.FC = () => {
async (from: any, to: any) => ({
to,
from,
time: 45 * 60 * 1000,
time: 45,
usableTime: 0,
}),
[],

View File

@@ -47,7 +47,7 @@ const useAsyncCallback = <
setLoading(false);
}
},
[setLoading, setError, setResult, action],
[setLoading, setError, setResult, action, ...deps],
);
const options = useMemo(
@@ -63,7 +63,7 @@ const useAsyncCallback = <
];
return output;
},
[invoke, result, loading, error, prevArgs],
[invoke, result, loading, error, prevArgs, ...deps],
);
return options;

View File

@@ -26,12 +26,37 @@ const stringToTime = (input: string) => {
return result;
};
const largerThan = (a: Time, b: Time) => {
return timeToMinutes(a) > timeToMinutes(b);
}
const max = (a: Time, b: Time) => largerThan(a, b) ? a : b;
const min = (a: Time, b: Time) => largerThan(a, b) ? b : a;
const timeToString = (input: Time) => `${input.hour}:${input.minute}`;
const timeToMinutes = (time: Time) => time.hour * 60 + time.minute;
const minutesToTime = (minutes: number): Time => {
const hour = Math.floor(minutes / 60);
const minute = minutes % 60;
return { hour, minute };
}
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;
return minutesToTime(current);
}
const timeUtils = {
timeToString,
stringToTime,
equal,
largerThan,
timeToMinutes,
max,
add,
};
export { timeUtils };

View File

@@ -17,7 +17,7 @@ const dateToDay = (input: Date) => {
}
const toId = (day: Day) => {
return `${day.year}-${day.month}-${day.date}`;
return `${day.year.toString().padStart(4, '0')}-${day.month.toString().padStart(2, '0')}-${day.date.toString().padStart(2, '0')}`;
}
const dayUtils = {

View File

@@ -1,4 +1,4 @@
import { UserLocation } from "../data";
import { Time, UserLocation } from "../data";
import { createContext } from "react"
type Transition = {
@@ -11,7 +11,7 @@ type Transition = {
type GetTransition = (
from: UserLocation,
to: UserLocation,
time: Date,
time: Time,
) => Promise<Transition>;
type LocationContextValue = {

View File

@@ -1,7 +1,7 @@
import { useContext } from "react"
import { useAsyncCallback } from "../async";
import { Time } from "../data";
import { OverrideContext } from "./context"
import { Override, OverrideContext } from "./context"
export const useOverrides = () => {
const { overrides } = useContext(OverrideContext);
@@ -13,6 +13,46 @@ export const useSetOverride = () => {
return set;
}
export const useGetOverride = () => {
const { get } = useContext(OverrideContext);
return get;
}
export const useSetTaskOverride = () => {
const { set } = useContext(OverrideContext);
const setTaskOverride = useAsyncCallback(
async (id: string, overrides: Override) => {
set(current => ({
...current,
tasks: {
...current.tasks,
[id]: overrides,
},
}));
},
[set],
);
return setTaskOverride;
}
export const useClearTaskOverride = () => {
const { set } = useContext(OverrideContext);
const clearTaskOverride = useAsyncCallback(
async (id: string) => {
set(current => {
const tasks = {...current.tasks};
delete tasks[id]
return {
...current,
tasks,
};
});
},
[set],
);
return clearTaskOverride;
}
export const useStartTimeOverride = () => {
const { overrides } = useContext(OverrideContext);
return overrides.startTime;

View File

@@ -1,6 +1,5 @@
import { Context, GraphNode } from "#/types/graph";
import { UserLocation } from "#/types/location";
import { Task } from "#/types/task";
import { Task, Time, UserLocation } from "#/features/data";
import { Context, GraphNode } from "../types";
import { getImpossible, getNext } from "./get-next";
enum Strategies {
@@ -29,7 +28,7 @@ type Status = RunningStatus | CompletedStatus;
type BuildGraphOptions = {
location: UserLocation;
time: Date;
time: Time;
tasks: Task[];
context: Context;
strategy?: Strategies;
@@ -49,7 +48,7 @@ const fil = <T>(
for (let b = 0; b < fn.length; b++) {
if (fn[b](input[i])) {
output[b].push(input[i]);
continue;
break;
}
}
}
@@ -150,8 +149,10 @@ const buildGraph = async ({
return complete([fullComplete]);
}
}
if (strategy !== Strategies.all) {
deadList.push(...dead);
}
}
return complete(completedList);
}

View File

@@ -1,20 +1,16 @@
import { GraphNode } from "#/types/graph";
import { PlanItem } from "#/types/plans";
import { timeUtils } from "#/features/data";
import { GraphNode, PlannedEntry } from "../types";
const constructDay = (node: GraphNode) => {
let current: GraphNode | undefined = node;
const plans: PlanItem[] = [];
const plans: PlannedEntry[] = [];
while(current) {
if (current.task) {
plans.push({
type: 'task',
name: current.task?.name || 'start',
external: current.task?.external,
start: new Date(
current.time.start.getTime()
+ (current.transition?.time || 0),
),
name: current.task?.title || 'start',
start: timeUtils.add(current.time.start, (current.transition?.time || 0)),
end: current.time.end,
score: current.score,
})
@@ -23,10 +19,7 @@ const constructDay = (node: GraphNode) => {
plans.push({
type: 'transition',
start: current.time.start,
end: new Date(
current.time.start.getTime()
+ current.transition.time,
),
end: timeUtils.add(current.time.start, current.transition.time),
from: current.transition.from,
to: current.transition.to,
})

View File

@@ -1,8 +1,10 @@
import { GraphNode, Context } from '#/types/graph';
import { Transition } from '#/types/location';
import { Task } from '#/types/task';
import { Task, Time, timeUtils } from '#/features/data';
import { Transition } from '#/features/location';
import { Context, GraphNode } from '../types';
import { getRemainingLocations, listContainLocation } from './utils';
const DEFAULT_PRIORITY = 50;
const isDead = (impossible: Task[]) => {
const missingRequered = impossible.find(t => t.required);
return !!missingRequered;
@@ -15,7 +17,7 @@ type GetImpossibleResult = {
export const getImpossible = (
tasks: Task[],
time: Date,
time: Time,
) => {
const result: GetImpossibleResult = {
remaining: [],
@@ -23,7 +25,7 @@ export const getImpossible = (
}
for (let task of tasks) {
if (time > task.start.max) {
if (timeUtils.largerThan(time, task.startTime.max)) {
result.impossible.push(task);
} else {
result.remaining.push(task);
@@ -47,17 +49,17 @@ const calculateScore = ({
let score = 0;
tasks?.forEach((task) => {
score += task.priority * 10;
score += (task.priority || DEFAULT_PRIORITY) * 10;
impossible.forEach((task) => {
if (task.required) {
score -= 10000 + (1 * task.priority);
score -= 10000 + (1 * (task.priority || DEFAULT_PRIORITY));
} else {
score -= 100 + (1 * task.priority);
score -= 100 + (1 * (task.priority || DEFAULT_PRIORITY));
}
});
});
if (transition) {
const minutes = transition.time / 1000 / 60
const minutes = transition.time;
score -= 10 + (1 * minutes);
}
return score;
@@ -71,7 +73,7 @@ const getNext = async (
const remainingLocations = getRemainingLocations(currentNode.remainingTasks, currentNode.location);
await Promise.all(remainingLocations.map(async(location) => {
const transition = await context.getTransition(currentNode.location, location, currentNode.time.end);
const endTime = new Date(currentNode.time.end.getTime() + transition.time);
const endTime = timeUtils.add(currentNode.time.end, transition.time);
const { remaining, impossible } = getImpossible(currentNode.remainingTasks, endTime);
const score = calculateScore({
transition,
@@ -89,7 +91,7 @@ const getNext = async (
score: currentNode.score + score,
status: {
completed: false,
dead: isDead(impossible),
dead: false, // TODO: fix isDead(impossible),
},
time: {
start: currentNode.time.end,
@@ -101,21 +103,14 @@ const getNext = async (
const possibleTasks = currentNode.remainingTasks.filter(task => !task.locations || listContainLocation(task.locations, currentNode.location))
await Promise.all(possibleTasks.map(async (orgTask) => {
const task = {...orgTask};
task.count = (task.count || 1) - 1
let startTime = new Date(
Math.max(
currentNode.time.end.getTime(),
task.start.min.getTime(),
),
let startTime =
timeUtils.max(
currentNode.time.end,
task.startTime.min,
);
const parentRemainging = currentNode.remainingTasks.filter(t => t !== orgTask);
let endTime = new Date(startTime.getTime() + task.duration.min);
const { remaining, impossible } = getImpossible(
task.count > 0
? [...parentRemainging, task]
: parentRemainging,
endTime,
);
let endTime = timeUtils.add(startTime, task.duration);
const { remaining, impossible } = getImpossible(parentRemainging, endTime);
const score = calculateScore({
tasks: [task],
impossible,

View File

@@ -5,12 +5,12 @@ export const locationEqual = (a: UserLocation, b: UserLocation) => {
if (a === b) {
return true;
}
if (a.location === b.location) {
return true;
}
if (a.location && b.location && a.location.latitude === b.location.latitude && a.location.longitute === b.location.longitute) {
return true;
}
// if (a.location === b.location) {
// return true;
// }
// if (a.location && b.location && a.location.latitude === b.location.latitude && a.location.longitute === b.location.longitute) {
// return true;
// }
if (a.title === b.title) {
return true;
}

View File

@@ -1,8 +1,10 @@
import { createDataContext } from '#/utils/data-context';
import { Time } from '../data';
import { Strategies } from "./algorithm/build-graph";
type PlannerOptions = {
strategy: Strategies;
startTime: Time;
}
const {
@@ -10,6 +12,7 @@ const {
Provider: PlannerProvider,
} = createDataContext<PlannerOptions>({
createDefault: () => ({
startTime: { hour: 7, minute: 0 },
strategy: Strategies.firstComplet,
}),
});

View File

@@ -1,22 +1,129 @@
import { buildGraph, Status, Strategies } from "./algorithm/build-graph";
import { useContext } from "react";
import { PlanItem } from "#/types/plans";
import { add } from 'date-fns';
import { PlannerContext } from "./context";
import { Task, UserLocation } from "../data";
import { Task, Time, UserLocation } from "../data";
import { useRoutines } from "../routines";
import { useGoals } from "../goals/hooks";
import { useAsyncCallback } from "../async";
import { Day, dayUtils } from "../day";
import { useGetOverride } from "../overrides";
import { useGetAppointments } from "../appointments";
import { useGetTransition } from "../location";
import { PlannedEntry } from "./types";
import { constructDay } from "./algorithm/construct-day";
export type UsePlanOptions = {
export type PreparePlanOptions = {
start: Day;
end: Day;
}
export type PlanOptions = PreparePlanOptions & {
location: UserLocation;
}
export type UsePlan = [
(start?: Date) => Promise<any>,
{
result?: { agenda: PlanItem[], impossible: Task[] };
status?: Status;
loading: boolean;
error?: any;
export type PlanResultDay = {
day: Day;
start: Time;
} & ({
status: 'waiting',
} | {
status: 'running',
nodes: number;
strategy: Strategies;
} | {
status: 'done';
nodes: number;
strategy: Strategies;
plan: PlannedEntry[];
impossible: Task[];
});
export type PlanResult = {
impossible: Task[];
days: {
[day: string]: PlanResultDay;
}
}
const getDays = (start: Day, end: Day): Day[] => {
const result: Day[] = [];
let currentDate = dayUtils.dayToDate(start);
const stopDate = dayUtils.dayToDate(end);
while (currentDate <= stopDate) {
result.push(dayUtils.dateToDay(currentDate));
currentDate = add(currentDate, {
days: 1,
});
}
return result;
}
const firstValue = <T>(...args: (T | undefined)[]): T => {
for (let arg of args) {
if (typeof arg !== 'undefined') {
return arg;
}
}
return undefined as unknown as T;
}
export const useOptions = () => {
const { data } = useContext(PlannerContext);
return data;
}
export const useSetOptions = () => {
const { setData } = useContext(PlannerContext);
return setData;
}
const usePreparePlan = () => {
const routines = useRoutines();
const goals = useGoals();
const getOverrides = useGetOverride();
const [getAppontments] = useGetAppointments();
const preparePlan = useAsyncCallback(
async ({ start, end }: PreparePlanOptions) => {
const days = await Promise.all(getDays(start, end).map(async (day) => {
const overrides = await getOverrides(day);
const start: Time = firstValue(overrides.startTime, { hour: 7, minute: 0 });
const appointments = await getAppontments(day);
const tasks = [...routines, ...appointments].map<Task | undefined>((task) => {
const override = overrides.tasks[task.id];
if (override?.enabled === false) {
return undefined;
}
const result: Task = {
...task,
startTime: {
min: firstValue(override?.startMin, task.startTime.min),
max: firstValue(override?.startMax, task.startTime.max),
},
duration: firstValue(override?.duration, task.duration),
required: firstValue(override?.required, task.required),
}
return result;
}).filter(Boolean).map(a => a as Exclude<typeof a, undefined>);
return {
day,
start,
tasks,
}
}));
return {
goals: [...goals],
days,
}
},
[routines, goals, getOverrides, getAppontments],
);
return preparePlan;
}
]
export const usePlanOptions = () => {
const { data } = useContext(PlannerContext);
@@ -29,6 +136,87 @@ export const useSetPlanOptions = () => {
}
export const usePlan = () => {
const [preparePlan] = usePreparePlan();
const getTransition = useGetTransition();
const options = usePlanOptions();
const createPlan = useAsyncCallback(
async ({ location, ...prepareOptions}: PlanOptions) => {
const prepared = await preparePlan(prepareOptions);
let result: PlanResult = {
impossible: [],
days: prepared.days.reduce((output, current) => ({
...output,
[dayUtils.toId(current.day)]: {
day: current.day,
start: current.start,
status: 'waiting',
},
}), {} as {[name: string]: PlanResultDay})
}
const update = (next: PlanResult) => {
result = next;
}
for (let day of prepared.days) {
const id = dayUtils.toId(day.day);
const dayGoal = prepared.goals;
const graph = await buildGraph({
location,
time: day.start,
tasks: [...day.tasks, ...dayGoal],
strategy: options.strategy,
context: {
getTransition,
},
callback: (status) => {
update({
...result,
days: {
...result.days,
[id]: {
day: day.day,
start: day.start,
status: 'running',
nodes: status.nodes,
strategy: status.strategy,
}
}
});
}
});
const [winner] = graph;
if (!winner) {
continue;
}
const plan = constructDay(winner);
update({
...result,
days: {
...result.days,
[id]: {
...result.days[id],
impossible: winner.impossibeTasks,
status: 'done',
plan,
}
}
})
prepared.goals = prepared.goals.filter((goal) => {
if (!dayGoal.find(d => d.id === goal.id)) {
return true;
}
if (!winner.impossibeTasks.find(d => d.id === goal.id)) {
return false;
}
return true;
})
}
return {
...result,
impossible: prepared.goals,
};
},
[preparePlan, getTransition, options],
);
return createPlan;
}

View File

@@ -1,3 +1,5 @@
import { Task, Time, UserLocation } from "../data";
import { GetTransition, Transition } from "../location";
type Context = {
getTransition: GetTransition;
@@ -6,20 +8,22 @@ type Context = {
export type PlannedTask = {
type: 'task';
name: string;
start: Date;
start: Time;
external?: boolean;
end: Date;
end: Time;
score: number;
}
export type PlannedTransition = {
type: 'transition';
start: Date;
end: Date;
start: Time;
end: Time;
from: UserLocation;
to: UserLocation;
};
export type PlannedEntry = PlannedTask | PlannedTransition;
type GraphNode = {
location: UserLocation;
task?: Task;
@@ -29,8 +33,8 @@ type GraphNode = {
impossibeTasks: Task[];
score: number;
time: {
start: Date;
end: Date;
start: Time;
end: Time;
};
status: {
dead: boolean;

View File

@@ -2,8 +2,8 @@ import { useMemo } from "react";
import { useAppointments } from "../appointments";
import { useAsyncCallback } from "../async";
import { Task, TaskType } from "../data";
import { useGoals, useSetGoals } from "../goals/hooks";
import { useRoutines, useSetRoutine } from "../routines";
import { useGoals, useRemoveGoal, useSetGoals } from "../goals/hooks";
import { useRemoveRoutine, useRoutines, useSetRoutine } from "../routines";
export const useTasks = (type?: TaskType) => {
const [appointments] = useAppointments();
@@ -44,3 +44,20 @@ export const useSetTask = () => {
);
return result;
};
export const useRemoveTask = () => {
const removeRoutine = useRemoveRoutine();
const removeGoal = useRemoveGoal();
const result = useAsyncCallback(
async (task: Task) => {
if (task.type === TaskType.routine) {
removeRoutine(task.id);
} else if (task.type === TaskType.goal) {
removeGoal(task.id);
}
},
[removeRoutine, removeGoal],
);
return result;
};

View File

@@ -5,3 +5,4 @@ export * from './popup';
export * from './row';
export * from './button';
export * from './group';
export * from './list';

View File

@@ -0,0 +1,53 @@
import { FlatList } from "react-native";
import { Button } from "../button";
import { Icon } from "../icon";
import { Cell, Row, RowProps } from "../row";
type ListProps<T> = {
add?: () => void;
remove?: (item: T) => any;
getKey: (item: T) => string;
items: T[];
render: (item: T) => RowProps;
}
function List<T>({
add,
remove,
getKey,
items,
render,
}: ListProps<T>) {
return (
<>
{!!add && <Button title="Add" onPress={add}/>}
<FlatList
data={items}
keyExtractor={item => getKey(item)}
renderItem={({ item }) => {
const {right, ...props} = render(item);
return (
<Row
{...props}
right={(
<>
{right}
{!!remove && (
<Cell onPress={() => remove(item)}>
<Icon
name="trash"
color="destructive"
/>
</Cell>
)}
</>
)}
/>
);
}}
/>
</>
);
}
export { List };

View File

@@ -1,9 +1,10 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useCallback, useRef } from 'react';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import styled from 'styled-components/native';
import { Icon } from '../icon';
import { Row, Cell, RowProps } from '../row';
import { Page } from '../page';
import { ScrollView } from 'react-native';
type Props = RowProps & {
onClose?: () => void;
@@ -54,7 +55,9 @@ const Popup: React.FC<Props> = ({ children, onClose, right, ...rowProps }) => {
</>
}
/>
<Content>
<Content
alwaysBounceVertical={false}
>
{children}
</Content>
</Wrapper>

View File

@@ -32,7 +32,8 @@ const Wrapper = styled.View<{
${({ opacity }) => (opacity? `opacity: ${opacity};` : '')}
`;
const Touch = styled.TouchableOpacity``;
const Touch = styled.TouchableOpacity`
`;
const Cell: React.FC<CellProps> = ({ children, onPress, ...props}) => {
const {

View File

@@ -4,7 +4,6 @@ import { Title1, Body1, Overline } from '#/ui/typography';
import { Cell, CellProps } from './cell';
type RowProps = CellProps & {
background?: string;
top?: ReactNode;
left?: ReactNode;
right?: ReactNode;
@@ -12,8 +11,6 @@ type RowProps = CellProps & {
overline?: ReactNode;
description?: ReactNode;
children?: ReactNode;
opacity?: number;
onPress?: () => any;
}
const Children = styled.View``;
@@ -32,19 +29,16 @@ const componentOrString = (
};
const Row: React.FC<RowProps> = ({
background,
top,
left,
right,
title,
opacity,
overline,
description,
children,
onPress,
...cellProps
}) => (
<Cell {...cellProps} background={background} opacity={opacity} onPress={onPress}>
<Cell {...cellProps}>
{left}
<Cell flex={1} direction="column" align="stretch">
{!!top}

View File

@@ -0,0 +1,64 @@
import React, { useState } from 'react';
import { Calendar } from 'react-native-calendars';
import { useTheme } from 'styled-components/native';
import { Row, Button, Modal } from '#/ui/components/base';
import { Day, dayUtils } from '#/features/day';
type Props = {
label: string;
selected?: Day;
} & ({
allowClear: true,
onSelect: (input?: Day) => void;
} | {
allowClear?: false,
onSelect: (input: Day) => void;
})
const DateInput: React.FC<Props> = ({ label, selected, onSelect, allowClear }) => {
const theme = useTheme();
const [visible, setVisible] = useState(false);
const marked: any = {};
if (selected) {
marked[dayUtils.toId(selected)] = {
selected: true,
marked: true,
selectedColor: theme.colors.primary,
};
}
return (
<Row
overline={label}
onPress={() => setVisible(true)}
title={selected ? dayUtils.toId(selected) : 'Not set'}
>
<Modal visible={visible} onClose={() => setVisible(false)}>
{visible && (<Calendar
showWeekNumbers={true}
markedDates={marked}
hideArrows={false}
enableSwipeMonths={true}
onDayPress={({ year, month, day }) => {
onSelect({ year, month, date: day });
setVisible(false);
}}
current={selected ? dayUtils.toId(selected) : undefined}
/>)}
{allowClear && (
<Row>
<Button
title="Clear"
onPress={() => {
onSelect(undefined as any);
setVisible(false);
}}
/>
</Row>
)}
</Modal>
</Row>
);
};
export default DateInput;

View File

@@ -1,4 +1,5 @@
export * from './input';
export * from './checkbox';
export * from './time';
export * from './date';
export * from './optional-selector';

View File

@@ -20,32 +20,13 @@ const TimeField = styled.TextInput`
`;
const TimeInput: React.FC<Props> = ({ label, placeholder, value, onChange, children, ...row }) => {
const [innerValue, setValue] = useState(value ? timeUtils.timeToString(value) : '');
useEffect(
() => {
if (!innerValue && value) {
onChange(undefined);
return;
}
const parsed = timeUtils.stringToTime(innerValue)
if (!parsed) {
return;
}
if (value && timeUtils.equal(parsed, value)) {
return;
}
onChange(parsed);
},
[innerValue, value, onChange],
)
return (
<Row overline={label} {...row}>
<TimeField
placeholder={placeholder}
value={innerValue}
onChangeText={setValue}
value={value ? timeUtils.timeToString(value) : ''}
onChangeText={(text) => onChange(text ? timeUtils.stringToTime(text) : undefined)}
/>
{children}
</Row>

View File

@@ -0,0 +1,30 @@
import { dayUtils } from "#/features/day";
import { PlanResultDay } from "#/features/planner"
import { Body1, Jumbo } from "#/ui/typography";
import { PlanDayTask } from "./task";
type Props = {
day: PlanResultDay;
};
const PlanDay: React.FC<Props> = ({ day }) => {
if (day.status === 'waiting') {
return <></>
}
if (day.status === 'running') {
return <Body1>Running</Body1>
}
return (
<>
<Jumbo>{dayUtils.toId(day.day)}</Jumbo>
{day.plan.map((item) => {
if (item.type === 'task') {
return <PlanDayTask task={item} />
}
return <Body1>Transit {item.from.title} to {item.to.title}</Body1>
})}
</>
)
}
export { PlanDay };

View File

@@ -0,0 +1,91 @@
import { useMemo } from 'react';
import { PlannedTask } from "#/features/planner/types";
import chroma from 'chroma-js';
import styled from "styled-components/native";
import stringToColor from 'string-to-color';
import { timeUtils } from '#/features/data';
import { Body1 } from '#/ui/typography';
type Props = {
task: PlannedTask;
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`
`;
const PlanDayTask: React.FC<Props> = ({ task, onPress }) => {
const color = useMemo(
() => chroma(stringToColor(task.name)).luminance(0.7).saturate(1).brighten(0.6).hex(),
[task.name],
);
const height = useMemo(
() => timeUtils.timeToMinutes(task.end) - timeUtils.timeToMinutes(task.start),
[task.start, task.end],
);
const view = (
<Block height={Math.max(70, height * 10)} background={color}>
<TimeBox background={color}>
<Time background={color}>{timeUtils.timeToString(task.start)}</Time>
<Time background={color}>{timeUtils.timeToString(task.end)}</Time>
</TimeBox>
<Main>
<Body1>{task.name}</Body1>
</Main>
<Filler />
</Block>
);
if (onPress) {
return (
<Touch onPress={onPress}>
{view}
</Touch>
);
}
return view;
};
export { PlanDayTask };

View File

@@ -0,0 +1 @@
export * from './day';

View File

@@ -33,7 +33,7 @@ const TaskGroup: React.FC<Props> = ({ type }) => {
<TaskListItem
item={task}
onPress={() => {
navigate('add-task', { id: task.id });
navigate('set-override', { id: task.id });
}}
/>
)}

View File

@@ -12,6 +12,9 @@ import { Platform } from 'react-native';
import { MoreScreen } from '../screens/more';
import { LocationListScreen } from '../screens/locations/list';
import { LocationSetScreen } from '../screens/locations/set';
import { TaskListScreen } from '../screens/task/list';
import { OverrideSetScreen } from '../screens/task/overrides';
import { PlanScreen } from '../screens/plan';
const MoreStackNavigator = createNativeStackNavigator();
@@ -19,6 +22,7 @@ const MoreStack: React.FC = () => (
<MoreStackNavigator.Navigator>
<MoreStackNavigator.Screen name="more-main" component={MoreScreen} />
<MoreStackNavigator.Screen name="locations" component={LocationListScreen} />
<MoreStackNavigator.Screen name="tasks" component={TaskListScreen} />
</MoreStackNavigator.Navigator>
);
@@ -41,6 +45,15 @@ const MainTabs: React.FC = () => {
name="day"
component={DayScreen}
/>
<MainTabsNvaigator.Screen
options={{
headerShown: false,
tabBarLabel: 'Plan',
tabBarIcon: ({ focused }) => <Icon color={focused ? 'primary' : 'text'} name="check-square" />,
}}
name="plan"
component={PlanScreen}
/>
<MainTabsNvaigator.Screen
options={{
headerShown: false,
@@ -66,6 +79,7 @@ const Root: React.FC = () => (
<RootNavigator.Group screenOptions={{ presentation: 'transparentModal' }}>
<RootNavigator.Screen name="add-task" component={TaskAddScreen} />
<RootNavigator.Screen name="set-location" component={LocationSetScreen} />
<RootNavigator.Screen name="set-override" component={OverrideSetScreen} />
</RootNavigator.Group>
</RootNavigator.Navigator>
);

View File

@@ -12,7 +12,27 @@ export type RootStackParamList = {
'set-location': {
id?: string;
};
'set-override': {
id: string;
};
};
export type MainTabParamList = {
day: NavigatorScreenParams<RootStackParamList>;
plan: NavigatorScreenParams<RootStackParamList>;
more: NavigatorScreenParams<RootStackParamList>;
}
export type MoreStackParamList = {
locations: undefined;
tasks: {
type: TaskType;
};
}
export type MoreScreenNavigationProps = NativeStackNavigationProp<
MoreStackParamList
>;
export type RootRouteProp = RouteProp<RootStackParamList>;
@@ -26,9 +46,6 @@ export type TaskAddScreenNavigationProp = NativeStackNavigationProp<
'add-task'
>;
export type MainTabParamList = {
day: NavigatorScreenParams<RootStackParamList>;
more: NavigatorScreenParams<RootStackParamList>;
}
export type TaskListScreenRouteProp = RouteProp<MoreStackParamList, 'tasks'>;
export type DayScreenRouteProp = RouteProp<MainTabParamList, 'day'>;

View File

@@ -1,7 +1,6 @@
import { useLocations, useRemoveLocation } from "#/features/location"
import { Button, Cell, Icon, Page, Row } from "#/ui/components/base";
import { List, Page } from "#/ui/components/base";
import { useNavigation } from "@react-navigation/native";
import { FlatList } from "react-native";
const LocationListScreen: React.FC = () => {
const { navigate } = useNavigation();
@@ -10,23 +9,14 @@ const LocationListScreen: React.FC = () => {
return (
<Page>
<Button title="Add" onPress={() => navigate('set-location', {})}/>
<FlatList
data={locations}
keyExtractor={item => item.id}
renderItem={({ item }) => (
<Row
title={item.title}
right={(
<Cell onPress={() => removeLocation(item.id)}>
<Icon
name="trash"
color="destructive"
/>
</Cell>
)}
/>
)}
<List
items={locations}
remove={l => removeLocation(l.id)}
getKey={l => l.id}
add={() => navigate('set-location', {})}
render={(item) => ({
title: item.title,
})}
/>
</Page>
);

View File

@@ -1,8 +1,10 @@
import { TaskType } from "#/features/data";
import { Page, Row } from "#/ui/components/base";
import { MoreScreenNavigationProps } from "#/ui/router";
import { useNavigation } from "@react-navigation/native";
const MoreScreen: React.FC = () => {
const { navigate } = useNavigation();
const { navigate } = useNavigation<MoreScreenNavigationProps>();
return (
<Page>
@@ -15,11 +17,11 @@ const MoreScreen: React.FC = () => {
/>
<Row
title="Routines"
onPress={() => navigate('routines')}
onPress={() => navigate('tasks', { type: TaskType.routine })}
/>
<Row
title="Goals"
onPress={() => navigate('goals')}
onPress={() => navigate('tasks', { type: TaskType.goal })}
/>
</Page>
);

View File

@@ -0,0 +1,57 @@
import { Day, dayUtils, useDate } from "#/features/day"
import { usePlan } from "#/features/planner";
import { Button } from "#/ui/components/base";
import DateInput from "#/ui/components/form/date";
import { PlanDay } from "#/ui/components/plan";
import { Body1 } from "#/ui/typography";
import { useCallback, useState } from "react";
import styled from "styled-components/native";
const Scroll = styled.ScrollView`
flex: 1;
`
const Wrapper = styled.View`
margin: 60px 0;
`
const PlanScreen: React.FC = () => {
const [start, setStart] = useState<Day>(dayUtils.today());
const [end, setEnd] = useState<Day>(dayUtils.today());
const [plan, { result }] = usePlan();
const runPlanning = useCallback(
() => plan({
start,
end,
location: { id: 'sdf', title: 'sdf' },
}),
[start, end, plan],
);
return (
<Scroll>
<Wrapper>
<DateInput
label="Start date"
selected={start}
onSelect={setStart}
/>
<DateInput
label="End date"
selected={end}
onSelect={setEnd}
/>
<Button onPress={runPlanning} title="Plan" />
{!!result && (
<>
{Object.entries(result.days).map(([key, day]) => (
<PlanDay day={day} />
))}
</>
)}
</Wrapper>
</Scroll>
);
}
export { PlanScreen }

View File

@@ -47,13 +47,14 @@ const TaskAddScreen: React.FC = () => {
const [selectedLocations, setSelectedLocations] = useState<UserLocation[]>([]);
const [hasDays, setHasDays] = useState(false);
const [selectedDays, setSelectedDays] = useState<typeof days>([]);
const [required, setRequired] = useState(false);
useEffect(
() => {
if (!id) {
return;
}
const current = tasks.find(t => t.id);
const current = tasks.find(t => t.id === id);
if (!current) {
return;
}
@@ -64,6 +65,7 @@ const TaskAddScreen: React.FC = () => {
setHasLocation(!!current.locations);
setSelectedLocations(current.locations || []);
setCurrentType(current.type || TaskType.goal);
setRequired(current.required);
if (current.type === TaskType.goal || current.type === TaskType.routine) {
setHasDays(!!current.days);
}
@@ -77,7 +79,7 @@ const TaskAddScreen: React.FC = () => {
id: currentId,
title,
type: currentType,
required: true,
required,
startTime: {
max: maxStart!,
min: minStart!,
@@ -103,11 +105,12 @@ const TaskAddScreen: React.FC = () => {
selectedLocations,
hasDays,
selectedDays,
required,
],
);
return (
<Popup title={`Add ${type}`} onClose={goBack}>
<Popup title={type ? `Add ${type}` : `Update ${title}`} onClose={goBack}>
<Group title="Basic">
<TextInput label="Title" value={title} onChangeText={setTitle} />
<SideBySide>
@@ -150,10 +153,7 @@ const TaskAddScreen: React.FC = () => {
disabledText="Any day"
enabledText="Specific days"
/>
<SideBySide>
<Checkbok label="Required" flex={1} />
<TextInput label="Priority" flex={1} />
</SideBySide>
<Checkbok label="Required" onChange={setRequired} value={required} />
{type === TaskType.goal && (
<SideBySide>
<TextInput label="Start" flex={1} />

View File

@@ -0,0 +1,32 @@
import { useRemoveTask, useTasks } from "#/features/tasks";
import { List, Page } from "#/ui/components/base";
import { RootNavigationProp, TaskListScreenRouteProp } from "#/ui/router";
import { useNavigation, useRoute } from "@react-navigation/native";
const TaskListScreen: React.FC = () => {
const {
params: { type },
} = useRoute<TaskListScreenRouteProp>();
const { navigate } = useNavigation<RootNavigationProp>();
const tasks = useTasks(type);
const [removeTask] = useRemoveTask();
return (
<Page>
<List
items={tasks}
remove={removeTask}
getKey={l => l.id}
add={() => navigate('add-task', { type })}
render={(item) => ({
title: item.title,
onPress: () => {
navigate('add-task', { id: item.id })
},
})}
/>
</Page>
);
}
export { TaskListScreen };

View File

@@ -0,0 +1,171 @@
import { useAsyncCallback } from "#/features/async";
import { nanoid } from 'nanoid';
import { Task, TaskType, Time, timeUtils, UserLocation } from "#/features/data";
import { useLocations } from "#/features/location";
import { useSetTask, useTasks } from "#/features/tasks";
import { Button, Cell, Group, Popup, Row } from "#/ui/components/base"
import { Checkbok, TextInput, TimeInput, OptionalSelector } from "#/ui/components/form";
import { RootNavigationProp, TaskAddScreenRouteProp } from "#/ui/router";
import { Overline } from "#/ui/typography";
import { useNavigation, useRoute } from "@react-navigation/native";
import { useEffect, useMemo, useRef, useState } from "react";
import styled from "styled-components/native";
import { Override, useClearTaskOverride, useOverrides, useSetOverride, useSetTaskOverride } from "#/features/overrides";
const SideBySide = styled.View`
flex-direction: row;
justify-content: flex-end;
`;
const dayNames = [
'Monday',
'Tuesday',
'Wednsday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
]
const days = new Array(7).fill(undefined).map((_, i) => ({
id: i,
name: dayNames[i],
}))
const OverrideSetScreen: React.FC = () => {
const { params: { id }} = useRoute<TaskAddScreenRouteProp>();
const { navigate, goBack } = useNavigation<RootNavigationProp>();
const [setOverride] = useSetTaskOverride()
const [clearOverrides] = useClearTaskOverride();
const overrides = useOverrides();
const tasks = useTasks();
const task = useMemo(
() => tasks.find(t => t.id === id)!,
[tasks, id],
);
const locations = useLocations();
const [maxStart, setMaxStart] = useState<Time>();
const [minStart, setMinStart] = useState<Time>();
const [duration, setDuration] = useState('');
const [hasLocation, setHasLocation] = useState(false);
const [selectedLocations, setSelectedLocations] = useState<UserLocation[]>([]);
const [hasDays, setHasDays] = useState(false);
const [selectedDays, setSelectedDays] = useState<typeof days>([]);
useEffect(
() => {
if (!id) {
return;
}
const current = overrides.tasks[id];
if (!current) {
return;
}
setMinStart(current.startMin);
setMaxStart(current.startMax);
setDuration(current.duration?.toString() || '');
setHasLocation(!!current.locations);
setSelectedLocations(current.locations || []);
},
[id],
)
const [save] = useAsyncCallback(
async () => {
const override: Override = {
startMin: minStart,
startMax: maxStart,
duration: duration ? parseInt(duration) : undefined,
locations: hasLocation ? selectedLocations: undefined,
};
await setOverride(id, override);
navigate('main');
},
[
id,
maxStart,
minStart,
duration,
hasLocation,
selectedLocations,
hasDays,
selectedDays,
],
);
const [clear] = useAsyncCallback(
async () => {
await clearOverrides(id);
navigate('main');
},
[id, clearOverrides],
);
return (
<Popup title={`Overrides for ${task.title}`} onClose={goBack}>
<SideBySide>
<TimeInput
flex={1}
placeholder={task.startTime.min ? timeUtils.timeToString(task.startTime.min) : undefined}
label="Min start"
value={minStart}
onChange={setMinStart}
/>
<TimeInput
flex={1}
placeholder={task.startTime.max ? timeUtils.timeToString(task.startTime.max) : undefined}
label="Max start"
value={maxStart}
onChange={setMaxStart}
/>
</SideBySide>
<TextInput
label="Duration"
value={duration}
onChangeText={setDuration}
right={<Cell><Overline>min</Overline></Cell>}
/>
<OptionalSelector
label="Location"
enabled={hasLocation}
items={locations}
selected={selectedLocations}
onChange={setSelectedLocations}
render={location => ({
title: location.title,
})}
getKey={location => location.id}
setEnabled={setHasLocation}
disabledText="Anywhere"
enabledText="Specific location"
/>
<OptionalSelector
label="Days"
enabled={hasDays}
items={days}
selected={selectedDays}
onChange={setSelectedDays}
render={day=> ({
title: day.name
})}
getKey={day => day.id.toString()}
setEnabled={setHasDays}
disabledText="Any day"
enabledText="Specific days"
/>
<SideBySide>
<Checkbok label="Required" flex={1} />
<TextInput label="Priority" flex={1} />
</SideBySide>
<Row>
<SideBySide>
<Button onPress={clear} title="Clear" type="destructive" />
<Button onPress={save} title="Save" type="primary" />
</SideBySide>
</Row>
</Popup>
);
};
export { OverrideSetScreen };

View File

@@ -32,12 +32,8 @@ function createDataContext<T extends {[name: string]: any}>({
let next = typeof input === 'function'
? input(current!)
: input;
const result = {
...current!,
...next,
};
setCurrent(result);
await AsyncStorageLib.setItem(key, JSON.stringify(result));
setCurrent(next);
await AsyncStorageLib.setItem(key, JSON.stringify(next));
},
[key, current, setCurrent],
);

View File

@@ -2319,13 +2319,6 @@
dependencies:
"@types/react" "*"
"@types/react-native@^0.67.6":
version "0.67.6"
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.67.6.tgz#9a7de5feba6065aec9f44f9a1e8f6e55ee5d015c"
integrity sha512-NM6atxrefIXMLE/PyQ1bIQjQ/lWLdls3uVxItzKvNUUVZlGqgn/uGN4MarM9quSf90uSqJYPIAeAgTtBTUjhgg==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@~17.0.21":
version "17.0.44"
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.44.tgz#c3714bd34dd551ab20b8015d9d0dbec812a51ec7"
@@ -6194,6 +6187,11 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
fs@^0.0.1-security:
version "0.0.1-security"
resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4"
integrity sha1-invTcYa23d84E/I4WLV+yq9eQdQ=
fsevents@^1.2.7:
version "1.2.13"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38"
@@ -6596,7 +6594,7 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0:
hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -6882,6 +6880,11 @@ immer@8.0.1:
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA==
immutable@^4.0.0-rc.12:
version "4.0.0"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23"
integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==
import-fresh@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546"
@@ -8555,6 +8558,11 @@ media-typer@0.3.0:
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
memoize-one@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
memory-cache@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a"
@@ -9085,7 +9093,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment@>=2.0.0:
moment@>=2.0.0, moment@^2.24.0:
version "2.29.3"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3"
integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==
@@ -10521,7 +10529,7 @@ prop-types@15.5.8:
dependencies:
fbjs "^0.8.9"
prop-types@^15.6.0, prop-types@^15.7.2:
prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -10825,6 +10833,23 @@ react-native-calendar-strip@^2.2.5:
prop-types "^15.6.0"
recyclerlistview "^3.0.0"
react-native-calendars@^1.1284.0:
version "1.1284.0"
resolved "https://registry.yarnpkg.com/react-native-calendars/-/react-native-calendars-1.1284.0.tgz#afe82ae14568073b5873e37a2d035db420c65010"
integrity sha512-H4XvACg3zh6q6XDl1ELTkCpMrKY7JWnd4ioB4IeAWoRMOQW+n/IVQmxZ4045ghTXjP4zIke3Tn1i3OmashzIbw==
dependencies:
fs "^0.0.1-security"
hoist-non-react-statics "^3.3.1"
immutable "^4.0.0-rc.12"
lodash "^4.17.15"
memoize-one "^5.2.1"
prop-types "^15.5.10"
react-native-swipe-gestures "^1.0.5"
recyclerlistview "^3.0.5"
xdate "^0.8.0"
optionalDependencies:
moment "^2.24.0"
react-native-codegen@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.0.6.tgz#b3173faa879cf71bfade8d030f9c4698388f6909"
@@ -10870,6 +10895,11 @@ react-native-screens@~3.10.1:
react-freeze "^1.0.0"
warn-once "^0.1.0"
react-native-swipe-gestures@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/react-native-swipe-gestures/-/react-native-swipe-gestures-1.0.5.tgz#a172cb0f3e7478ccd681fd36b8bfbcdd098bde7c"
integrity sha512-Ns7Bn9H/Tyw278+5SQx9oAblDZ7JixyzeOczcBK8dipQk2pD7Djkcfnf1nB/8RErAmMLL9iXgW0QHqiII8AhKw==
react-native-web@0.17.1:
version "0.17.1"
resolved "https://registry.yarnpkg.com/react-native-web/-/react-native-web-0.17.1.tgz#90d473c89dd99b88bc9830b2a9fcdd2fc5f04902"
@@ -11063,7 +11093,7 @@ recursive-readdir@2.2.2:
dependencies:
minimatch "3.0.4"
recyclerlistview@^3.0.0:
recyclerlistview@^3.0.0, recyclerlistview@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-3.0.5.tgz#50bf5bcaa401d56bb6bb264354083f4d424408eb"
integrity sha512-JVHz13u520faEsbVqFrJOMuJjc4mJlOXODe5QdqAJHdl5/IpyYeo83uiHrpzxyLb8QtJ0889JMlDik+Z1Ed0QQ==
@@ -13500,6 +13530,11 @@ xcode@^3.0.0, xcode@^3.0.1:
simple-plist "^1.1.0"
uuid "^7.0.3"
xdate@^0.8.0:
version "0.8.2"
resolved "https://registry.yarnpkg.com/xdate/-/xdate-0.8.2.tgz#d7b033c00485d02695baf0044f4eacda3fc961a3"
integrity sha1-17AzwASF0CaVuvAET06s2j/JYaM=
xdl@59.2.36:
version "59.2.36"
resolved "https://registry.yarnpkg.com/xdl/-/xdl-59.2.36.tgz#26a987cbc9e8fd799cce3c7e32279f85737e0936"