mirror of
https://github.com/morten-olsen/bob-the-algorithm.git
synced 2026-02-08 00:46:25 +01:00
fix
This commit is contained in:
committed by
Morten Olsen
parent
8259232a83
commit
6181eeb0c8
61
.github/workflows/expo-main.yml
vendored
Normal file
61
.github/workflows/expo-main.yml
vendored
Normal 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 }}
|
||||
25
.github/workflows/publish-web.yml
vendored
25
.github/workflows/publish-web.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/*.log
|
||||
node_modules/
|
||||
.expo/
|
||||
dist/
|
||||
|
||||
51
app.config.js
Normal file
51
app.config.js
Normal 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;
|
||||
34
app.json
34
app.json
@@ -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
25
eas.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -11,7 +11,7 @@ const App: React.FC = () => {
|
||||
async (from: any, to: any) => ({
|
||||
to,
|
||||
from,
|
||||
time: 45 * 60 * 1000,
|
||||
time: 45,
|
||||
usableTime: 0,
|
||||
}),
|
||||
[],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './popup';
|
||||
export * from './row';
|
||||
export * from './button';
|
||||
export * from './group';
|
||||
export * from './list';
|
||||
|
||||
53
src/ui/components/base/list/index.tsx
Normal file
53
src/ui/components/base/list/index.tsx
Normal 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 };
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
64
src/ui/components/form/date/index.tsx
Normal file
64
src/ui/components/form/date/index.tsx
Normal 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;
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './input';
|
||||
export * from './checkbox';
|
||||
export * from './time';
|
||||
export * from './date';
|
||||
export * from './optional-selector';
|
||||
|
||||
@@ -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>
|
||||
|
||||
30
src/ui/components/plan/day/index.tsx
Normal file
30
src/ui/components/plan/day/index.tsx
Normal 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 };
|
||||
91
src/ui/components/plan/day/task.tsx
Normal file
91
src/ui/components/plan/day/task.tsx
Normal 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 };
|
||||
1
src/ui/components/plan/index.ts
Normal file
1
src/ui/components/plan/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './day';
|
||||
@@ -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 });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -12,8 +12,28 @@ 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>;
|
||||
export type RootNavigationProp = NativeStackNavigationProp<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'>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
57
src/ui/screens/plan/index.tsx
Normal file
57
src/ui/screens/plan/index.tsx
Normal 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 }
|
||||
@@ -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} />
|
||||
|
||||
32
src/ui/screens/task/list.tsx
Normal file
32
src/ui/screens/task/list.tsx
Normal 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 };
|
||||
171
src/ui/screens/task/overrides.tsx
Normal file
171
src/ui/screens/task/overrides.tsx
Normal 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 };
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
57
yarn.lock
57
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user