mirror of
https://github.com/morten-olsen/bob-the-algorithm.git
synced 2026-02-08 00:46:25 +01:00
v3
This commit is contained in:
45
.github/workflows/expo-pr.yml
vendored
Normal file
45
.github/workflows/expo-pr.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Deploy Expo Preview
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy_branch_preview:
|
||||||
|
name: Deploy Branch Preview
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
- uses: expo/expo-github-action@v5
|
||||||
|
with:
|
||||||
|
expo-packager: yarn
|
||||||
|
expo-username: ${{ secrets.EXPO_CLI_USERNAME }}
|
||||||
|
expo-password: ${{ secrets.EXPO_CLI_PASSWORD }}
|
||||||
|
expo-cache: true
|
||||||
|
- name: Cache Node Modules
|
||||||
|
uses: actions/cache@v2
|
||||||
|
env:
|
||||||
|
cache-name: cache-node-modules
|
||||||
|
with:
|
||||||
|
path: ~/.cache/yarn
|
||||||
|
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||||
|
${{ runner.os }}-build-
|
||||||
|
${{ runner.os }}-
|
||||||
|
- name: Install Packages
|
||||||
|
run: npm i -g yarn && yarn install
|
||||||
|
- name: Expo Publish Channel
|
||||||
|
run: expo publish --non-interactive --release-channel pr${{ github.event.number }}
|
||||||
|
- name: Add Comment To PR
|
||||||
|
uses: mshick/add-pr-comment@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
EXPO_PROJECT: "@${{ secrets.EXPO_CLI_USERNAME }}/bob"
|
||||||
|
with:
|
||||||
|
message: |
|
||||||
|
## Application
|
||||||
|

|
||||||
|
Published to https://exp.host/${{ env.EXPO_PROJECT }}?release-channel=pr${{ github.event.number }}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = function(api) {
|
module.exports = function(api) {
|
||||||
api.cache(true);
|
api.cache.using(() => process.env.NODE_ENV);
|
||||||
return {
|
return {
|
||||||
presets: ['babel-preset-expo'],
|
presets: ['babel-preset-expo'],
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
15
package.json
15
package.json
@@ -14,12 +14,18 @@
|
|||||||
"jest": {
|
"jest": {
|
||||||
"preset": "jest-expo"
|
"preset": "jest-expo"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "~17.0.21",
|
||||||
|
"@types/react-dom": "~18.0.3",
|
||||||
|
"react-error-overlay": "6.0.9"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^12.0.0",
|
"@expo/vector-icons": "^12.0.0",
|
||||||
"@react-native-async-storage/async-storage": "~1.15.0",
|
"@react-native-async-storage/async-storage": "~1.15.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.0.5",
|
"@react-navigation/bottom-tabs": "^6.0.5",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-navigation/native": "^6.0.2",
|
||||||
"@react-navigation/native-stack": "^6.1.0",
|
"@react-navigation/native-stack": "^6.1.0",
|
||||||
|
"@react-navigation/stack": "^6.2.1",
|
||||||
"chroma-js": "^2.4.2",
|
"chroma-js": "^2.4.2",
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"expo": "~44.0.0",
|
"expo": "~44.0.0",
|
||||||
@@ -40,6 +46,8 @@
|
|||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"react-native": "0.64.3",
|
"react-native": "0.64.3",
|
||||||
"react-native-calendar-strip": "^2.2.5",
|
"react-native-calendar-strip": "^2.2.5",
|
||||||
|
"react-native-collapsible": "^1.6.0",
|
||||||
|
"react-native-gesture-handler": "^2.4.2",
|
||||||
"react-native-get-random-values": "^1.8.0",
|
"react-native-get-random-values": "^1.8.0",
|
||||||
"react-native-safe-area-context": "3.3.2",
|
"react-native-safe-area-context": "3.3.2",
|
||||||
"react-native-screens": "~3.10.1",
|
"react-native-screens": "~3.10.1",
|
||||||
@@ -49,17 +57,20 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.12.9",
|
"@babel/core": "^7.12.9",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
|
||||||
"@types/chroma-js": "^2.1.3",
|
"@types/chroma-js": "^2.1.3",
|
||||||
"@types/react": "~17.0.21",
|
"@types/react": "~17.0.21",
|
||||||
"@types/react-dom": "^18.0.3",
|
"@types/react-dom": "^18.0.3",
|
||||||
"@types/react-native": "~0.64.12",
|
"@types/react-native": "^0.67.6",
|
||||||
"@types/styled-components-react-native": "^5.1.3",
|
"@types/styled-components-react-native": "^5.1.3",
|
||||||
"babel-plugin-module-resolver": "^4.1.0",
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
"expo-cli": "^5.4.3",
|
"expo-cli": "^5.4.3",
|
||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jest-expo": "~44.0.1",
|
"jest-expo": "~44.0.1",
|
||||||
|
"react-refresh": "^0.13.0",
|
||||||
"react-test-renderer": "17.0.1",
|
"react-test-renderer": "17.0.1",
|
||||||
"typescript": "~4.3.5"
|
"typescript": "~4.3.5",
|
||||||
|
"webpack-hot-middleware": "^2.25.1"
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/app.tsx
11
src/app.tsx
@@ -1,19 +1,12 @@
|
|||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { Setup } from './features/setup';
|
import { Setup } from './features/setup';
|
||||||
import { Router } from './ui/router';
|
import { Router } from './ui/router';
|
||||||
import { ThemeProvider } from 'styled-components/native';
|
import { ThemeProvider } from 'styled-components/native';
|
||||||
import { light } from './ui';
|
import { light } from './ui';
|
||||||
import { set } from 'date-fns';
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const [day, setDate] = useState(() => set(new Date, {
|
|
||||||
hours: 0,
|
|
||||||
minutes: 0,
|
|
||||||
seconds: 0,
|
|
||||||
milliseconds: 0,
|
|
||||||
}));
|
|
||||||
const getTransit = useCallback(
|
const getTransit = useCallback(
|
||||||
async (from: any, to: any) => ({
|
async (from: any, to: any) => ({
|
||||||
to,
|
to,
|
||||||
@@ -27,7 +20,7 @@ const App: React.FC = () => {
|
|||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<ThemeProvider theme={light}>
|
<ThemeProvider theme={light}>
|
||||||
<Setup getTransit={getTransit} day={day} setDate={setDate}>
|
<Setup getTransit={getTransit}>
|
||||||
<Router />
|
<Router />
|
||||||
</Setup>
|
</Setup>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { UserLocation } from "#/types/location"
|
|
||||||
import { createContext } from "react"
|
|
||||||
|
|
||||||
type AgendaContext = {
|
|
||||||
enabled: boolean;
|
|
||||||
locations?: UserLocation[];
|
|
||||||
startMax?: Date;
|
|
||||||
startMin?: Date;
|
|
||||||
duration?: number;
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AgendaContextContextValue = {
|
|
||||||
contexts: {[id: string]: AgendaContext};
|
|
||||||
set: (id: string, context: AgendaContext) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AgendaContextContext = createContext<AgendaContextContextValue>(undefined as any);
|
|
||||||
|
|
||||||
export type { AgendaContext, AgendaContextContextValue };
|
|
||||||
export {AgendaContextContext };
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useAsyncCallback } from "#/hooks/async";
|
|
||||||
import { Task } from "#/types/task";
|
|
||||||
import { set } from "date-fns";
|
|
||||||
import { useContext, useMemo } from "react"
|
|
||||||
import { useDate } from "../calendar";
|
|
||||||
import { useTasks } from "../tasks";
|
|
||||||
import { AgendaContextContext } from "./context"
|
|
||||||
|
|
||||||
const toToday = (today: Date, target: Date) => set(target, {
|
|
||||||
year: today.getFullYear(),
|
|
||||||
month: today.getMonth(),
|
|
||||||
date: today.getDate(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useAgendaContext = () => {
|
|
||||||
const { contexts } = useContext(AgendaContextContext);
|
|
||||||
return contexts;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSetAgendaContext = () => {
|
|
||||||
const { set } = useContext(AgendaContextContext);
|
|
||||||
const result = useAsyncCallback(set, [set]);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTasksWithContext = () => {
|
|
||||||
const { all } = useTasks();
|
|
||||||
const date = useDate();
|
|
||||||
const contexts = useAgendaContext();
|
|
||||||
|
|
||||||
const withContext = useMemo<(Task & { enabled: boolean })[]>(
|
|
||||||
() => all.map((task) => {
|
|
||||||
const context = contexts[task.id];
|
|
||||||
if (!context) {
|
|
||||||
return { ...task, enabled: true };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...task,
|
|
||||||
locations: context.locations?.length || 0 > 0 ? context.locations : task.locations,
|
|
||||||
start: {
|
|
||||||
min: context.startMin ? toToday(date, context.startMin) : task.start.min,
|
|
||||||
max: context.startMax ? toToday(date, context.startMax) : task.start.max,
|
|
||||||
},
|
|
||||||
duration: {
|
|
||||||
...task.duration,
|
|
||||||
min: context.duration || task.duration.min,
|
|
||||||
},
|
|
||||||
count: context.count,
|
|
||||||
enabled: typeof context.enabled === 'undefined' ? true : context.enabled,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[all, contexts],
|
|
||||||
);
|
|
||||||
|
|
||||||
return withContext;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { AgendaContextProvider } from './provider';
|
|
||||||
export * from './hooks';
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { useAsync } from "#/hooks/async";
|
|
||||||
import AsyncStorageLib from "@react-native-async-storage/async-storage";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
import { ReactNode, useCallback, useMemo, useState } from "react";
|
|
||||||
import { AgendaContext, AgendaContextContext, AgendaContextContextValue } from "./context";
|
|
||||||
|
|
||||||
type AgendaContextProviderProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
day: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AGENDA_CONTEXT_STORAGE_KEY = 'agenda-contexts';
|
|
||||||
|
|
||||||
const AgendaContextProvider: React.FC<AgendaContextProviderProps> = ({
|
|
||||||
children,
|
|
||||||
day,
|
|
||||||
}) => {
|
|
||||||
const [contexts, setContexts] = useState<AgendaContextContextValue['contexts']>({});
|
|
||||||
const key = useMemo(
|
|
||||||
() => `${AGENDA_CONTEXT_STORAGE_KEY}-${format(day, 'yyyy-MM-dd')}`,
|
|
||||||
[day],
|
|
||||||
);
|
|
||||||
|
|
||||||
const set = useCallback(
|
|
||||||
async (id: string, context: AgendaContext) => {
|
|
||||||
const index = {
|
|
||||||
...contexts,
|
|
||||||
[id]: {...context},
|
|
||||||
};
|
|
||||||
setContexts(index);
|
|
||||||
await AsyncStorageLib.setItem(key, JSON.stringify(contexts));
|
|
||||||
},
|
|
||||||
[setContexts, contexts, key],
|
|
||||||
);
|
|
||||||
|
|
||||||
useAsync(
|
|
||||||
async () => {
|
|
||||||
const raw = await AsyncStorageLib.getItem(key);
|
|
||||||
if (!raw) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const items = JSON.parse(raw) as AgendaContextContextValue['contexts'];
|
|
||||||
Object.values(items).forEach((item) => {
|
|
||||||
if (item.startMax) {
|
|
||||||
item.startMax = new Date(item.startMax);
|
|
||||||
}
|
|
||||||
if (item.startMin) {
|
|
||||||
item.startMin = new Date(item.startMin);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
setContexts(items);
|
|
||||||
},
|
|
||||||
[key],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgendaContextContext.Provider value={{ contexts, set }}>
|
|
||||||
{children}
|
|
||||||
</AgendaContextContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { AgendaContextProviderProps };
|
|
||||||
export { AgendaContextProvider };
|
|
||||||
35
src/features/appointments/context.ts
Normal file
35
src/features/appointments/context.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { createContext } from "react"
|
||||||
|
import { Appointment } from "../data"
|
||||||
|
import { Day } from "../day"
|
||||||
|
|
||||||
|
enum AppointmentsStatus {
|
||||||
|
unavailable = 'unavailable',
|
||||||
|
unapproved = 'unapproved',
|
||||||
|
rejected = 'rejected',
|
||||||
|
approved = 'approved',
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppointmentsContextUnavailable = {
|
||||||
|
status: AppointmentsStatus.unavailable | AppointmentsStatus.rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppointmentsContextUnapprovedValue = {
|
||||||
|
status: AppointmentsStatus.unapproved;
|
||||||
|
request: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppointmentsContextApproved = {
|
||||||
|
status: AppointmentsStatus.approved;
|
||||||
|
getDay: (day: Day) => Promise<Appointment[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppointmentsContextValue = AppointmentsContextUnavailable
|
||||||
|
| AppointmentsContextUnapprovedValue
|
||||||
|
| AppointmentsContextApproved;
|
||||||
|
|
||||||
|
const AppointmentsContext = createContext<AppointmentsContextValue>(undefined as any);
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AppointmentsContextValue,
|
||||||
|
};
|
||||||
|
export { AppointmentsContext, AppointmentsStatus };
|
||||||
45
src/features/appointments/hooks.ts
Normal file
45
src/features/appointments/hooks.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useAsync, useAsyncCallback } from "#/features/async";
|
||||||
|
import { useContext } from "react"
|
||||||
|
import { Day, useDate } from "../day";
|
||||||
|
import { AppointmentsContext, AppointmentsStatus } from "./context"
|
||||||
|
|
||||||
|
export const useAppointmentStatus = () => {
|
||||||
|
const { status } = useContext(AppointmentsContext);
|
||||||
|
return status;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppointments = () => {
|
||||||
|
const date = useDate();
|
||||||
|
const context = useContext(AppointmentsContext);
|
||||||
|
const result = useAsync(
|
||||||
|
async () => {
|
||||||
|
if (context.status !== AppointmentsStatus.approved) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const appointments = await context.getDay(date);
|
||||||
|
return appointments;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
context.status === AppointmentsStatus.approved && context.getDay,
|
||||||
|
date,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useGetAppointments = () => {
|
||||||
|
const context = useContext(AppointmentsContext);
|
||||||
|
const result = useAsyncCallback(
|
||||||
|
async (date: Day) => {
|
||||||
|
if (context.status !== AppointmentsStatus.approved) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const appointments = await context.getDay(date);
|
||||||
|
return appointments;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
context.status === AppointmentsStatus.approved && context.getDay,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
2
src/features/appointments/index.ts
Normal file
2
src/features/appointments/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { AppointmentsProvider } from './provider';
|
||||||
|
export * from './hooks';
|
||||||
35
src/features/appointments/provider.tsx
Normal file
35
src/features/appointments/provider.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useAsync } from "#/features/async";
|
||||||
|
import { ReactNode } from "react"
|
||||||
|
import { Platform } from "react-native";
|
||||||
|
import { AppointmentsContext, AppointmentsContextValue, AppointmentsStatus } from './context';
|
||||||
|
|
||||||
|
type AppointmentsProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppointmentsProvider: React.FC<AppointmentsProviderProps> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [value] = useAsync<AppointmentsContextValue>(
|
||||||
|
async () => {
|
||||||
|
if (Platform.OS !== 'ios') {
|
||||||
|
return { status: AppointmentsStatus.unavailable };
|
||||||
|
}
|
||||||
|
return { status: AppointmentsStatus.unavailable };
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppointmentsContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AppointmentsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { AppointmentsProviderProps };
|
||||||
|
export { AppointmentsProvider };
|
||||||
1
src/features/async/index.ts
Normal file
1
src/features/async/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './hooks';
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Calendar } from "expo-calendar";
|
|
||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
type RejectedCalendarContextValue = {
|
|
||||||
status: 'rejected';
|
|
||||||
date: Date;
|
|
||||||
setDate: (date: Date) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnavailableCalendarContextValue = {
|
|
||||||
status: 'unavailable';
|
|
||||||
date: Date;
|
|
||||||
setDate: (date: Date) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AcceptedCalendarContextValue = {
|
|
||||||
status: 'ready';
|
|
||||||
date: Date;
|
|
||||||
setDate: (date: Date) => void;
|
|
||||||
calendars: Calendar[];
|
|
||||||
calendar: Calendar;
|
|
||||||
selected: Calendar[];
|
|
||||||
setSelected: (calendars: Calendar[]) => void;
|
|
||||||
error?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
type CalendarContextValue = RejectedCalendarContextValue
|
|
||||||
| UnavailableCalendarContextValue
|
|
||||||
| AcceptedCalendarContextValue
|
|
||||||
|
|
||||||
const CalendarContext = createContext<CalendarContextValue>(undefined as any);
|
|
||||||
|
|
||||||
export type { CalendarContextValue };
|
|
||||||
export { CalendarContext };
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { useContext } from "react"
|
|
||||||
import { CalendarContext } from "./context"
|
|
||||||
import { set } from 'date-fns'
|
|
||||||
import { useAsync, useAsyncCallback } from "#/hooks/async";
|
|
||||||
import { createEventAsync, deleteEventAsync, getEventsAsync } from "expo-calendar";
|
|
||||||
import { PlanItem } from "#/types/plans";
|
|
||||||
|
|
||||||
const emptyArray: never[] = [];
|
|
||||||
const emptyFn = () => undefined;
|
|
||||||
|
|
||||||
export const useCalendar = () => {
|
|
||||||
const context = useContext(CalendarContext);
|
|
||||||
if (context.status !== 'ready') {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return context.calendar;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCalendars = () => {
|
|
||||||
const context = useContext(CalendarContext);
|
|
||||||
if (context.status !== 'ready') {
|
|
||||||
return emptyArray;
|
|
||||||
}
|
|
||||||
return context.calendars;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSelectedCalendars = () => {
|
|
||||||
const context = useContext(CalendarContext);
|
|
||||||
if (context.status !== 'ready') {
|
|
||||||
return emptyArray;
|
|
||||||
}
|
|
||||||
return context.selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSetSelectedCalendars = () => {
|
|
||||||
const context = useContext(CalendarContext);
|
|
||||||
if (context.status !== 'ready') {
|
|
||||||
return emptyFn;
|
|
||||||
}
|
|
||||||
return context.setSelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDate = () => {
|
|
||||||
const { date } = useContext(CalendarContext);
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useSetDate = () => {
|
|
||||||
const { setDate } = useContext(CalendarContext);
|
|
||||||
return setDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCommit = () => {
|
|
||||||
const date = useDate();
|
|
||||||
const calendar = useCalendar();
|
|
||||||
const result = useAsyncCallback(
|
|
||||||
async (plan: PlanItem[]) => {
|
|
||||||
if (!calendar) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const end = set(date, {
|
|
||||||
hours: 24,
|
|
||||||
minutes: 0,
|
|
||||||
seconds: 0,
|
|
||||||
milliseconds: 0,
|
|
||||||
});
|
|
||||||
const current = await getEventsAsync([calendar.id], date, end);
|
|
||||||
await Promise.all(
|
|
||||||
current.map(async (item) => {
|
|
||||||
await deleteEventAsync(item.id)
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
for (let item of plan) {
|
|
||||||
if (item.type === 'task' && item.external) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const title = item.type === 'task' ? item.name : `${item.from.title} to ${item.to.title}`;
|
|
||||||
await createEventAsync(calendar.id, {
|
|
||||||
title: title,
|
|
||||||
startDate: item.start,
|
|
||||||
endDate: item.end,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[date, calendar],
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useToday = (start: Date, end?: Date) => {
|
|
||||||
const selectedCalendars = useSelectedCalendars();
|
|
||||||
if (!end) {
|
|
||||||
end = set(start, {
|
|
||||||
hours: 24,
|
|
||||||
minutes: 0,
|
|
||||||
seconds: 0,
|
|
||||||
milliseconds: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = useAsync(
|
|
||||||
async () => {
|
|
||||||
if (selectedCalendars.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return getEventsAsync(selectedCalendars.map(c => c.id), start, end!)
|
|
||||||
},
|
|
||||||
[selectedCalendars, start.getTime()],
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { CalendarProvider } from './provider';
|
|
||||||
export * from './hooks';
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import { Calendar, CalendarAccessLevel, createCalendarAsync, EntityTypes, getCalendarsAsync, getDefaultCalendarAsync, requestCalendarPermissionsAsync } from "expo-calendar";
|
|
||||||
import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
|
||||||
import { useAsync } from "#/hooks/async";
|
|
||||||
import { CalendarContext } from "./context";
|
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
||||||
import { Platform } from "react-native";
|
|
||||||
|
|
||||||
const SELECTED_STORAGE_KEY = 'selected_calendars';
|
|
||||||
|
|
||||||
type CalendarProviderProps = {
|
|
||||||
calendarName?: string,
|
|
||||||
date: Date;
|
|
||||||
children: ReactNode;
|
|
||||||
setDate: (date: Date) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type SetupResponse = {
|
|
||||||
status: 'rejected';
|
|
||||||
} | {
|
|
||||||
status: 'unavailable';
|
|
||||||
} | {
|
|
||||||
status: 'ready';
|
|
||||||
calendar: Calendar;
|
|
||||||
calendars: Calendar[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const CalendarProvider: React.FC<CalendarProviderProps> = ({
|
|
||||||
date,
|
|
||||||
children,
|
|
||||||
setDate,
|
|
||||||
calendarName = 'Bob the planner',
|
|
||||||
}) => {
|
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
||||||
const [value] = useAsync<SetupResponse>(
|
|
||||||
async () => {
|
|
||||||
const { status } = await requestCalendarPermissionsAsync();
|
|
||||||
if (Platform.OS !== 'ios') {
|
|
||||||
return { status: 'unavailable' };
|
|
||||||
}
|
|
||||||
if (status !== 'granted') {
|
|
||||||
return { status: 'rejected' };
|
|
||||||
}
|
|
||||||
let calendars = await getCalendarsAsync(EntityTypes.EVENT);
|
|
||||||
let calendar = calendars.find(c => c.title === calendarName);
|
|
||||||
if (!calendar) {
|
|
||||||
const defaultCalendar = await getDefaultCalendarAsync();
|
|
||||||
await createCalendarAsync({
|
|
||||||
title: calendarName,
|
|
||||||
source: defaultCalendar.source,
|
|
||||||
sourceId: defaultCalendar.source.id,
|
|
||||||
ownerAccount: 'personal',
|
|
||||||
accessLevel: CalendarAccessLevel.OWNER,
|
|
||||||
entityType: EntityTypes.EVENT,
|
|
||||||
name: calendarName,
|
|
||||||
});
|
|
||||||
calendars = await getCalendarsAsync(EntityTypes.EVENT);
|
|
||||||
calendar = calendars.find(c => c.name === calendarName)!;
|
|
||||||
}
|
|
||||||
const selectedRaw = await AsyncStorage.getItem(SELECTED_STORAGE_KEY)
|
|
||||||
if (selectedRaw) {
|
|
||||||
setSelectedIds(JSON.parse(selectedRaw));
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
status: 'ready',
|
|
||||||
calendars,
|
|
||||||
calendar,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const setSelected = useCallback(
|
|
||||||
(calendars: Calendar[]) => {
|
|
||||||
const ids = calendars.map(c => c.id);
|
|
||||||
setSelectedIds(ids);
|
|
||||||
AsyncStorage.setItem(SELECTED_STORAGE_KEY, JSON.stringify(ids));
|
|
||||||
},
|
|
||||||
[setSelectedIds]
|
|
||||||
)
|
|
||||||
const selected = useMemo(
|
|
||||||
() => {
|
|
||||||
if (value?.status !== 'ready') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return value.calendars.filter(c => selectedIds.includes(c.id));
|
|
||||||
},
|
|
||||||
[value, selectedIds],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.status !== 'ready') {
|
|
||||||
return (
|
|
||||||
<CalendarContext.Provider value={{ status: value.status, date, setDate }}>
|
|
||||||
{children}
|
|
||||||
</CalendarContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CalendarContext.Provider
|
|
||||||
value={{
|
|
||||||
status: 'ready',
|
|
||||||
setDate,
|
|
||||||
date,
|
|
||||||
selected,
|
|
||||||
setSelected,
|
|
||||||
calendar: value.calendar,
|
|
||||||
calendars: value.calendars,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</CalendarContext.Provider>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { CalendarProviderProps };
|
|
||||||
export { CalendarProvider };
|
|
||||||
2
src/features/data/index.ts
Normal file
2
src/features/data/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './types';
|
||||||
|
export { timeUtils } from './utils';
|
||||||
55
src/features/data/types.ts
Normal file
55
src/features/data/types.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Day } from "../day"
|
||||||
|
|
||||||
|
export enum TaskType {
|
||||||
|
appointment = 'appointment',
|
||||||
|
goal = 'goal',
|
||||||
|
routine = 'routine',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Time = {
|
||||||
|
hour: number;
|
||||||
|
minute: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserLocation = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
position: {
|
||||||
|
longitute: number;
|
||||||
|
latitude: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaskBase = {
|
||||||
|
type: TaskType;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
locations?: UserLocation[];
|
||||||
|
required: boolean;
|
||||||
|
priority?: number;
|
||||||
|
startTime: {
|
||||||
|
min: Time;
|
||||||
|
max: Time;
|
||||||
|
};
|
||||||
|
duration: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Appointment = TaskBase & {
|
||||||
|
type: TaskType.appointment;
|
||||||
|
calendarId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Goal = TaskBase & {
|
||||||
|
type: TaskType.goal;
|
||||||
|
completed: boolean;
|
||||||
|
deadline?: Day;
|
||||||
|
startDate?: Day;
|
||||||
|
days: boolean[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Routine = TaskBase & {
|
||||||
|
type: TaskType.routine;
|
||||||
|
days: boolean[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Task = Appointment | Goal | Routine;
|
||||||
37
src/features/data/utils.ts
Normal file
37
src/features/data/utils.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Time } from "./types";
|
||||||
|
|
||||||
|
const equal = (a: Time, b: Time) => {
|
||||||
|
return a.hour == b.hour && a.minute === b.minute;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringToTime = (input: string) => {
|
||||||
|
const [hourPart, minutePart] = input.split(':').map(a => a.trim()).filter(Boolean);
|
||||||
|
const hour = parseInt(hourPart);
|
||||||
|
const minute = parseInt(minutePart || '0');
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isInteger(hour)
|
||||||
|
|| !Number.isInteger(minute)
|
||||||
|
|| Number.isNaN(hour)
|
||||||
|
|| Number.isNaN(minute)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Time = {
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeToString = (input: Time) => `${input.hour}:${input.minute}`;
|
||||||
|
|
||||||
|
const timeUtils = {
|
||||||
|
timeToString,
|
||||||
|
stringToTime,
|
||||||
|
equal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { timeUtils };
|
||||||
12
src/features/day/context.ts
Normal file
12
src/features/day/context.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createContext } from "react";
|
||||||
|
import { Day } from ".";
|
||||||
|
|
||||||
|
type DateContextValue = {
|
||||||
|
date: Day;
|
||||||
|
setDate: (date: Day) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DateContext = createContext<DateContextValue>(undefined as any);
|
||||||
|
|
||||||
|
export type { DateContextValue };
|
||||||
|
export { DateContext }
|
||||||
7
src/features/day/day.ts
Normal file
7
src/features/day/day.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
type Day = {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
date: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { Day };
|
||||||
12
src/features/day/hooks.ts
Normal file
12
src/features/day/hooks.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useContext } from "react"
|
||||||
|
import { DateContext } from "./context"
|
||||||
|
|
||||||
|
export const useDate = () => {
|
||||||
|
const { date } = useContext(DateContext);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetDate = () => {
|
||||||
|
const { setDate } = useContext(DateContext);
|
||||||
|
return setDate;
|
||||||
|
}
|
||||||
4
src/features/day/index.ts
Normal file
4
src/features/day/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { DateProvider } from './provider';
|
||||||
|
export type { Day } from './day';
|
||||||
|
export * from './hooks';
|
||||||
|
export { dayUtils } from './utils';
|
||||||
20
src/features/day/provider.tsx
Normal file
20
src/features/day/provider.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { ReactNode, useState } from "react";
|
||||||
|
import { DateContext } from "./context";
|
||||||
|
import { dayUtils } from "./utils";
|
||||||
|
|
||||||
|
type DateProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DateProvider: React.FC<DateProviderProps> = ({ children }) => {
|
||||||
|
const [date, setDate] = useState(dayUtils.today());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DateContext.Provider value={{ date, setDate }}>
|
||||||
|
{children}
|
||||||
|
</DateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { DateProviderProps };
|
||||||
|
export { DateProvider };
|
||||||
30
src/features/day/utils.ts
Normal file
30
src/features/day/utils.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Day } from "./day";
|
||||||
|
|
||||||
|
const today = () => {
|
||||||
|
return dateToDay(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayToDate = (day: Day) => {
|
||||||
|
return new Date(day.year, day.month - 1, day.date, 0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateToDay = (input: Date) => {
|
||||||
|
const year = input.getFullYear();
|
||||||
|
const month = input.getMonth() + 1;
|
||||||
|
const date = input.getDate();
|
||||||
|
const day: Day = { year, month, date };
|
||||||
|
return day;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toId = (day: Day) => {
|
||||||
|
return `${day.year}-${day.month}-${day.date}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayUtils = {
|
||||||
|
today,
|
||||||
|
dateToDay,
|
||||||
|
dayToDate,
|
||||||
|
toId,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { dayUtils };
|
||||||
11
src/features/goals/context.ts
Normal file
11
src/features/goals/context.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createDataContext } from "#/utils/data-context";
|
||||||
|
import { Goal } from "../data";
|
||||||
|
|
||||||
|
const {
|
||||||
|
Context: GoalsContext,
|
||||||
|
Provider: GoalsProvider,
|
||||||
|
}= createDataContext<{[id: string]: Goal}>({
|
||||||
|
createDefault: () => ({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export { GoalsContext, GoalsProvider };
|
||||||
41
src/features/goals/hooks.ts
Normal file
41
src/features/goals/hooks.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useCallback, useContext, useMemo } from "react"
|
||||||
|
import { Goal } from "../data";
|
||||||
|
import { GoalsContext } from "./context"
|
||||||
|
|
||||||
|
export const useGoals = () => {
|
||||||
|
const { data } = useContext(GoalsContext);
|
||||||
|
const current = useMemo(
|
||||||
|
() => Object.values(data),
|
||||||
|
[data],
|
||||||
|
)
|
||||||
|
return current;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSetGoals = () => {
|
||||||
|
const { setData } = useContext(GoalsContext);
|
||||||
|
const set = useCallback(
|
||||||
|
(goal: Goal) => setData(current => ({
|
||||||
|
...current,
|
||||||
|
[goal.id]: goal,
|
||||||
|
})),
|
||||||
|
[setData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useRemoveGoal = () => {
|
||||||
|
const { setData } = useContext(GoalsContext);
|
||||||
|
const removeRoutine = useCallback(
|
||||||
|
(id: string) => {
|
||||||
|
setData(current => {
|
||||||
|
const next = {...current};
|
||||||
|
delete next[id];
|
||||||
|
return next;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setData],
|
||||||
|
);
|
||||||
|
|
||||||
|
return removeRoutine;
|
||||||
|
}
|
||||||
@@ -1,6 +1,19 @@
|
|||||||
import { GetTransition, UserLocation } from "#/types/location";
|
import { UserLocation } from "../data";
|
||||||
import { createContext } from "react"
|
import { createContext } from "react"
|
||||||
|
|
||||||
|
type Transition = {
|
||||||
|
time: number;
|
||||||
|
usableTime: number;
|
||||||
|
to: UserLocation;
|
||||||
|
from: UserLocation;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetTransition = (
|
||||||
|
from: UserLocation,
|
||||||
|
to: UserLocation,
|
||||||
|
time: Date,
|
||||||
|
) => Promise<Transition>;
|
||||||
|
|
||||||
type LocationContextValue = {
|
type LocationContextValue = {
|
||||||
locations: {
|
locations: {
|
||||||
[id: string]: UserLocation;
|
[id: string]: UserLocation;
|
||||||
@@ -13,5 +26,5 @@ type LocationContextValue = {
|
|||||||
|
|
||||||
const LocationContext = createContext<LocationContextValue>(undefined as any);
|
const LocationContext = createContext<LocationContextValue>(undefined as any);
|
||||||
|
|
||||||
export type { LocationContextValue };
|
export type { LocationContextValue, GetTransition, Transition };
|
||||||
export { LocationContext };
|
export { LocationContext };
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { useAsync } from "#/hooks/async";
|
import { useAsync } from "#/features/async";
|
||||||
import { useContext } from "react"
|
import { useContext, useMemo } from "react"
|
||||||
import { requestForegroundPermissionsAsync, getCurrentPositionAsync } from 'expo-location';
|
import { requestForegroundPermissionsAsync, getCurrentPositionAsync } from 'expo-location';
|
||||||
import { LocationContext } from "./context"
|
import { LocationContext } from "./context"
|
||||||
import { UserLocation } from "#/types/location";
|
import { UserLocation } from "../data";
|
||||||
import { getDistanceFromLatLonInKm } from "./utils";
|
import { getDistanceFromLatLonInKm } from "./utils";
|
||||||
|
|
||||||
export const useLocations = () => {
|
export const useLocations = () => {
|
||||||
const { locations } = useContext(LocationContext);
|
const { locations } = useContext(LocationContext);
|
||||||
return locations;
|
const result = useMemo(() => Object.values(locations), [locations]);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSetLocation = () => {
|
export const useSetLocation = () => {
|
||||||
@@ -31,7 +32,7 @@ export const useLookup = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useCurrentLocation = (proximity: number = 0.5) => {
|
export const useCurrentLocation = (proximity: number = 0.5) => {
|
||||||
const locations = useLocations();
|
const { locations } = useContext(LocationContext);
|
||||||
const result = useAsync<UserLocation | undefined>(
|
const result = useAsync<UserLocation | undefined>(
|
||||||
async () => {
|
async () => {
|
||||||
let { status } = await requestForegroundPermissionsAsync();
|
let { status } = await requestForegroundPermissionsAsync();
|
||||||
@@ -40,14 +41,14 @@ export const useCurrentLocation = (proximity: number = 0.5) => {
|
|||||||
}
|
}
|
||||||
let position = await getCurrentPositionAsync({});
|
let position = await getCurrentPositionAsync({});
|
||||||
const withDistance = Object.values(locations).map((location) => {
|
const withDistance = Object.values(locations).map((location) => {
|
||||||
if (!location.location) {
|
if (!location.position) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const distance = getDistanceFromLatLonInKm(
|
const distance = getDistanceFromLatLonInKm(
|
||||||
position.coords.latitude,
|
position.coords.latitude,
|
||||||
position.coords.longitude,
|
position.coords.longitude,
|
||||||
location.location.latitude,
|
location.position.latitude,
|
||||||
location.location.longitute,
|
location.position.longitute,
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
distance,
|
distance,
|
||||||
@@ -59,7 +60,7 @@ export const useCurrentLocation = (proximity: number = 0.5) => {
|
|||||||
return {
|
return {
|
||||||
id: `${position.coords.longitude} ${position.coords.latitude}`,
|
id: `${position.coords.longitude} ${position.coords.latitude}`,
|
||||||
title: 'Unknown',
|
title: 'Unknown',
|
||||||
location: {
|
position: {
|
||||||
latitude: position.coords.latitude,
|
latitude: position.coords.latitude,
|
||||||
longitute: position.coords.longitude,
|
longitute: position.coords.longitude,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
export type { Transition, GetTransition } from './context';
|
||||||
export { LocationProvider } from './provider';
|
export { LocationProvider } from './provider';
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useAsync, useAsyncCallback } from "#/hooks/async";
|
import { useAsync, useAsyncCallback } from "#/features/async";
|
||||||
import { GetTransition, UserLocation } from "#/types/location";
|
import { GetTransition } from "./context";
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import { ReactNode, useState } from "react";
|
import { ReactNode, useState } from "react";
|
||||||
import { LocationContext } from "./context";
|
import { LocationContext } from "./context";
|
||||||
|
import { UserLocation } from "../data";
|
||||||
|
|
||||||
type LocationProviderProps = {
|
type LocationProviderProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -10,7 +11,7 @@ type LocationProviderProps = {
|
|||||||
getTransition: GetTransition;
|
getTransition: GetTransition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LOCATION_STORAGE_KEY = 'location_storage';
|
const LOCATION_STORAGE_KEY = 'locations';
|
||||||
|
|
||||||
const LocationProvider: React.FC<LocationProviderProps> = ({
|
const LocationProvider: React.FC<LocationProviderProps> = ({
|
||||||
children,
|
children,
|
||||||
|
|||||||
30
src/features/overrides/context.ts
Normal file
30
src/features/overrides/context.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { createContext, SetStateAction } from "react";
|
||||||
|
import { Time, UserLocation } from "../data";
|
||||||
|
import { Day } from "../day";
|
||||||
|
|
||||||
|
type Override = {
|
||||||
|
locations?: UserLocation[] | null;
|
||||||
|
startMin?: Time;
|
||||||
|
startMax?: Time;
|
||||||
|
duration?: number;
|
||||||
|
required?: boolean;
|
||||||
|
priority?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OverrideIndex = {
|
||||||
|
startTime?: Time;
|
||||||
|
tasks: {
|
||||||
|
[id: string]: Override;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type OverrideContextValue = {
|
||||||
|
overrides: OverrideIndex;
|
||||||
|
get: (date: Day) => Promise<OverrideIndex>;
|
||||||
|
set: React.Dispatch<SetStateAction<OverrideIndex>>;
|
||||||
|
}
|
||||||
|
const OverrideContext = createContext<OverrideContextValue>(undefined as any);
|
||||||
|
|
||||||
|
export type { Override, OverrideIndex, OverrideContextValue };
|
||||||
|
export { OverrideContext };
|
||||||
33
src/features/overrides/hooks.ts
Normal file
33
src/features/overrides/hooks.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useContext } from "react"
|
||||||
|
import { useAsyncCallback } from "../async";
|
||||||
|
import { Time } from "../data";
|
||||||
|
import { OverrideContext } from "./context"
|
||||||
|
|
||||||
|
export const useOverrides = () => {
|
||||||
|
const { overrides } = useContext(OverrideContext);
|
||||||
|
return overrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSetOverride = () => {
|
||||||
|
const { set } = useContext(OverrideContext);
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStartTimeOverride = () => {
|
||||||
|
const { overrides } = useContext(OverrideContext);
|
||||||
|
return overrides.startTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSetStartTimeOverride = () => {
|
||||||
|
const { set } = useContext(OverrideContext);
|
||||||
|
const setStartTime = useAsyncCallback(
|
||||||
|
async (startTime?: Time) => {
|
||||||
|
set(current => ({
|
||||||
|
...current,
|
||||||
|
startTime,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[set],
|
||||||
|
);
|
||||||
|
return setStartTime;
|
||||||
|
};
|
||||||
3
src/features/overrides/index.ts
Normal file
3
src/features/overrides/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export type { Override, OverrideIndex } from './context';
|
||||||
|
export { OverrideProvider } from './provider';
|
||||||
|
export * from './hooks';
|
||||||
59
src/features/overrides/provider.tsx
Normal file
59
src/features/overrides/provider.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import AsyncStorageLib from "@react-native-async-storage/async-storage";
|
||||||
|
import React, { ReactNode, SetStateAction, useCallback, useState } from "react";
|
||||||
|
import { useAsync } from "../async";
|
||||||
|
import { Day, useDate, dayUtils } from "../day";
|
||||||
|
import { Override, OverrideContext, OverrideIndex } from "./context";
|
||||||
|
|
||||||
|
type OverrideProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StorageKey = 'overrides';
|
||||||
|
|
||||||
|
const OverrideProvider: React.FC<OverrideProviderProps> = ({ children }) => {
|
||||||
|
const currentDate = useDate();
|
||||||
|
const [overrides, setOverrides] = useState<OverrideIndex>();
|
||||||
|
|
||||||
|
const get = useCallback(
|
||||||
|
async (date: Day): Promise<OverrideIndex> => {
|
||||||
|
const raw = await AsyncStorageLib.getItem(`${StorageKey}_${dayUtils.toId(date)}`);
|
||||||
|
if (!raw) {
|
||||||
|
return { tasks: {} };
|
||||||
|
}
|
||||||
|
return JSON.parse(raw);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const set = useCallback(
|
||||||
|
async (override: SetStateAction<OverrideIndex>) => {
|
||||||
|
const next = typeof override === 'function' ? override(overrides!) : overrides;
|
||||||
|
setOverrides(next);
|
||||||
|
await AsyncStorageLib.setItem(
|
||||||
|
`${StorageKey}_${dayUtils.toId(currentDate)}`,
|
||||||
|
JSON.stringify(next),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[currentDate, overrides],
|
||||||
|
);
|
||||||
|
|
||||||
|
useAsync(
|
||||||
|
async () => {
|
||||||
|
setOverrides(await get(currentDate));
|
||||||
|
},
|
||||||
|
[currentDate, setOverrides],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!overrides) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverrideContext.Provider value={{ overrides, get, set }}>
|
||||||
|
{children}
|
||||||
|
</OverrideContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { OverrideProviderProps };
|
||||||
|
export { OverrideProvider };
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import { createContext } from 'react';
|
import { createDataContext } from '#/utils/data-context';
|
||||||
import { Strategies } from "./algorithm/build-graph";
|
import { Strategies } from "./algorithm/build-graph";
|
||||||
|
|
||||||
type PlannerOptions = {
|
type PlannerOptions = {
|
||||||
strategy: Strategies;
|
strategy: Strategies;
|
||||||
}
|
}
|
||||||
type PlannerContextValue = {
|
|
||||||
options: PlannerOptions;
|
|
||||||
setOptions: (options: Partial<PlannerOptions>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PlannerContext = createContext<PlannerContextValue>(undefined as any);
|
const {
|
||||||
|
Context: PlannerContext,
|
||||||
|
Provider: PlannerProvider,
|
||||||
|
} = createDataContext<PlannerOptions>({
|
||||||
|
createDefault: () => ({
|
||||||
|
strategy: Strategies.firstComplet,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
export type { PlannerContextValue, PlannerOptions };
|
export type { PlannerOptions };
|
||||||
export { PlannerContext };
|
export { PlannerContext, PlannerProvider };
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
import { useGetTransition } from "#/features/location";
|
|
||||||
import { buildGraph, Status, Strategies } from "./algorithm/build-graph";
|
import { buildGraph, Status, Strategies } from "./algorithm/build-graph";
|
||||||
import { constructDay } from "./algorithm/construct-day";
|
import { useContext } from "react";
|
||||||
import { useAsyncCallback } from "#/hooks/async";
|
|
||||||
import { UserLocation } from "#/types/location";
|
|
||||||
import { useDate } from "../calendar";
|
|
||||||
import { useTasksWithContext } from "../agenda-context";
|
|
||||||
import { useContext, useMemo, useState } from "react";
|
|
||||||
import { PlanItem } from "#/types/plans";
|
import { PlanItem } from "#/types/plans";
|
||||||
import { Task } from "#/types/task";
|
|
||||||
import { PlannerContext } from "./context";
|
import { PlannerContext } from "./context";
|
||||||
|
import { Task, UserLocation } from "../data";
|
||||||
|
|
||||||
export type UsePlanOptions = {
|
export type UsePlanOptions = {
|
||||||
location: UserLocation;
|
location: UserLocation;
|
||||||
@@ -25,53 +19,16 @@ export type UsePlan = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
export const usePlanOptions = () => {
|
export const usePlanOptions = () => {
|
||||||
const { options } = useContext(PlannerContext);
|
const { data } = useContext(PlannerContext);
|
||||||
return options;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSetPlanOptions = () => {
|
export const useSetPlanOptions = () => {
|
||||||
const { setOptions } = useContext(PlannerContext);
|
const { setData } = useContext(PlannerContext);
|
||||||
return setOptions;
|
return setData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePlan = ({
|
export const usePlan = () => {
|
||||||
location,
|
|
||||||
}: UsePlanOptions): UsePlan => {
|
|
||||||
const today = useDate();
|
|
||||||
const planOptions = usePlanOptions();
|
|
||||||
const [status, setStatus] = useState<Status>();
|
|
||||||
const all = useTasksWithContext();
|
|
||||||
const enabled = useMemo(() => all.filter(f => f.enabled), [all])
|
|
||||||
const getTransition = useGetTransition();
|
|
||||||
const [invoke, options] = useAsyncCallback(
|
|
||||||
async (start?: Date) => {
|
|
||||||
const graph = await buildGraph({
|
|
||||||
location,
|
|
||||||
time: start || today,
|
|
||||||
tasks: enabled,
|
|
||||||
strategy: planOptions.strategy,
|
|
||||||
context: {
|
|
||||||
getTransition,
|
|
||||||
},
|
|
||||||
callback: setStatus,
|
|
||||||
});
|
|
||||||
const valid = graph.filter(a => !a.status.dead && a.status.completed).sort((a, b) => b.score - a.score);
|
|
||||||
const day = constructDay(valid[0]);
|
|
||||||
return {
|
|
||||||
impossible: valid[0].impossibeTasks,
|
|
||||||
agenda: day,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[today, location, all, setStatus, planOptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
return [
|
|
||||||
invoke,
|
|
||||||
{
|
|
||||||
result: options.result,
|
|
||||||
loading: options.loading,
|
|
||||||
error: options.error,
|
|
||||||
status: status,
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export { PlannerProvider } from './provider';
|
export { PlannerProvider, PlannerOptions } from './context';
|
||||||
export type { PlannerOptions } from './context';
|
|
||||||
export { Strategies } from './algorithm/build-graph';
|
export { Strategies } from './algorithm/build-graph';
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { ReactNode, useCallback, useState } from 'react';
|
|
||||||
import { Strategies } from './algorithm/build-graph';
|
|
||||||
import { PlannerContext, PlannerOptions } from './context';
|
|
||||||
|
|
||||||
type PlannerProviderProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlannerProvider: React.FC<PlannerProviderProps> = ({ children }) => {
|
|
||||||
const [options, setOwnOptions] = useState<PlannerOptions>({
|
|
||||||
strategy: Strategies.firstComplet,
|
|
||||||
})
|
|
||||||
|
|
||||||
const setOptions = useCallback(
|
|
||||||
(next: Partial<PlannerOptions>) => {
|
|
||||||
setOwnOptions(current => ({
|
|
||||||
...current,
|
|
||||||
...next,
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
[setOwnOptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlannerContext.Provider value={{ options, setOptions }}>
|
|
||||||
{children}
|
|
||||||
</PlannerContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { PlannerProviderProps };
|
|
||||||
export { PlannerProvider };
|
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
import { GetTransition, Transition, UserLocation } from "./location";
|
|
||||||
import { Task } from "./task";
|
|
||||||
|
|
||||||
type Context = {
|
type Context = {
|
||||||
getTransition: GetTransition;
|
getTransition: GetTransition;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PlannedTask = {
|
||||||
|
type: 'task';
|
||||||
|
name: string;
|
||||||
|
start: Date;
|
||||||
|
external?: boolean;
|
||||||
|
end: Date;
|
||||||
|
score: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlannedTransition = {
|
||||||
|
type: 'transition';
|
||||||
|
start: Date;
|
||||||
|
end: Date;
|
||||||
|
from: UserLocation;
|
||||||
|
to: UserLocation;
|
||||||
|
};
|
||||||
|
|
||||||
type GraphNode = {
|
type GraphNode = {
|
||||||
location: UserLocation;
|
location: UserLocation;
|
||||||
@@ -1,26 +1,11 @@
|
|||||||
import { UserLocation } from "#/types/location";
|
import { createDataContext } from "#/utils/data-context";
|
||||||
import { createContext } from "react"
|
import { Routine } from "../data";
|
||||||
|
|
||||||
export type Routine = {
|
const {
|
||||||
id: string;
|
Context: RoutinesContext,
|
||||||
title: string;
|
Provider: RoutinesProvider,
|
||||||
required: boolean;
|
}= createDataContext<{[id: string]: Routine}>({
|
||||||
priority: number;
|
createDefault: () => ({}),
|
||||||
start: {
|
})
|
||||||
min: Date;
|
|
||||||
max: Date;
|
|
||||||
};
|
|
||||||
duration: number;
|
|
||||||
location?: UserLocation[];
|
|
||||||
days?: boolean[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RoutinesContextValue = {
|
export { RoutinesContext, RoutinesProvider };
|
||||||
routines: Routine[];
|
|
||||||
remove: (id: string) => any;
|
|
||||||
set: (routine: Routine) => any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RoutinesContext = createContext<RoutinesContextValue>(undefined as any);
|
|
||||||
|
|
||||||
export { RoutinesContext };
|
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
import { useCallback, useContext, useMemo } from "react"
|
import { useCallback, useContext, useMemo } from "react"
|
||||||
import { Routine, RoutinesContext } from "./context"
|
import { Routine } from "../data";
|
||||||
|
import { RoutinesContext } from "./context"
|
||||||
|
|
||||||
export const useRoutines = (day?: number) => {
|
export const useRoutines = () => {
|
||||||
const { routines } = useContext(RoutinesContext);
|
const { data } = useContext(RoutinesContext);
|
||||||
const current = useMemo(
|
const current = useMemo(
|
||||||
() => routines.filter(
|
() => Object.values(data),
|
||||||
r => typeof day === undefined
|
[data],
|
||||||
|| !r.days
|
)
|
||||||
|| r.days[day!],
|
|
||||||
),
|
|
||||||
[routines],
|
|
||||||
);
|
|
||||||
|
|
||||||
return current;
|
return current;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useSetRoutine = () => {
|
export const useSetRoutine = () => {
|
||||||
const { set } = useContext(RoutinesContext);
|
const { setData } = useContext(RoutinesContext);
|
||||||
const setRoutine = useCallback(
|
const set = useCallback(
|
||||||
(routine: Routine) => set(routine),
|
(routine: Routine) => setData(current => ({
|
||||||
[set],
|
...current,
|
||||||
|
[routine.id]: routine,
|
||||||
|
})),
|
||||||
|
[setData],
|
||||||
);
|
);
|
||||||
|
|
||||||
return setRoutine;
|
return set;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRemoveRoutine = () => {
|
export const useRemoveRoutine = () => {
|
||||||
const { remove } = useContext(RoutinesContext);
|
const { setData } = useContext(RoutinesContext);
|
||||||
const removeRoutine = useCallback(
|
const removeRoutine = useCallback(
|
||||||
(id: string) => remove(id),
|
(id: string) => {
|
||||||
[remove],
|
setData(current => {
|
||||||
|
const next = {...current};
|
||||||
|
delete next[id];
|
||||||
|
return next;
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setData],
|
||||||
);
|
);
|
||||||
|
|
||||||
return removeRoutine;
|
return removeRoutine;
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
export { RoutinesProvider } from './provider';
|
export { RoutinesProvider } from './context';
|
||||||
export { Routine } from './context';
|
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import { useAsync, useAsyncCallback } from "#/hooks/async";
|
|
||||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
||||||
import React, { ReactNode, useMemo, useState } from "react";
|
|
||||||
import { Routine, RoutinesContext } from "./context";
|
|
||||||
|
|
||||||
type RoutinesProviderProps = {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ROUTINES_STORAGE_KEY = 'routines-items';
|
|
||||||
|
|
||||||
const RoutinesProvider: React.FC<RoutinesProviderProps> = ({ children }) => {
|
|
||||||
const [routineIndex, setRoutineIndex] = useState<{[id: string]: Routine}>({});
|
|
||||||
const routines = useMemo(
|
|
||||||
() => Object.values(routineIndex),
|
|
||||||
[routineIndex]
|
|
||||||
);
|
|
||||||
|
|
||||||
useAsync(
|
|
||||||
async () => {
|
|
||||||
const raw = await AsyncStorage.getItem(ROUTINES_STORAGE_KEY);
|
|
||||||
if (!raw) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = JSON.parse(raw) as {[name: string]: Routine};
|
|
||||||
Object.values(result).forEach(item => {
|
|
||||||
item.start.max = new Date(item.start.max);
|
|
||||||
item.start.min = new Date(item.start.min);
|
|
||||||
});
|
|
||||||
|
|
||||||
setRoutineIndex(result);
|
|
||||||
},
|
|
||||||
[setRoutineIndex],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [set] = useAsyncCallback(
|
|
||||||
async (routine: Routine) => {
|
|
||||||
const index = {
|
|
||||||
...routineIndex,
|
|
||||||
[routine.id]: routine,
|
|
||||||
};
|
|
||||||
setRoutineIndex(index);
|
|
||||||
await AsyncStorage.setItem(ROUTINES_STORAGE_KEY, JSON.stringify(index));
|
|
||||||
},
|
|
||||||
[setRoutineIndex, routineIndex],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [remove] = useAsyncCallback(
|
|
||||||
async (id: string) => {
|
|
||||||
const index = {
|
|
||||||
...routineIndex,
|
|
||||||
};
|
|
||||||
delete index[id];
|
|
||||||
setRoutineIndex(index);
|
|
||||||
await AsyncStorage.setItem(ROUTINES_STORAGE_KEY, JSON.stringify(index));
|
|
||||||
},
|
|
||||||
[setRoutineIndex, routineIndex],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<RoutinesContext.Provider
|
|
||||||
value={{
|
|
||||||
routines,
|
|
||||||
set,
|
|
||||||
remove,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</RoutinesContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export type { RoutinesProviderProps };
|
|
||||||
export { RoutinesProvider };
|
|
||||||
@@ -1,35 +1,39 @@
|
|||||||
import { GetTransition } from "#/types/location"
|
|
||||||
import { ReactNode } from "react"
|
import { ReactNode } from "react"
|
||||||
import { AgendaContextProvider } from "./agenda-context"
|
import { AppointmentsProvider } from "./appointments"
|
||||||
import { CalendarProvider } from "./calendar"
|
import { DateProvider } from "./day"
|
||||||
import { LocationProvider } from "./location"
|
import { GoalsProvider } from "./goals/context"
|
||||||
|
import { GetTransition, LocationProvider } from "./location"
|
||||||
|
import { OverrideProvider } from "./overrides"
|
||||||
import { PlannerProvider } from "./planner"
|
import { PlannerProvider } from "./planner"
|
||||||
import { RoutinesProvider } from "./routines"
|
import { RoutinesProvider } from "./routines"
|
||||||
|
|
||||||
type SetupProps = {
|
type SetupProps = {
|
||||||
day: Date;
|
|
||||||
setDate: (date: Date) => void;
|
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
getTransit: GetTransition;
|
getTransit: GetTransition;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Setup: React.FC<SetupProps> = ({
|
const Setup: React.FC<SetupProps> = ({
|
||||||
children,
|
children,
|
||||||
day,
|
|
||||||
setDate,
|
|
||||||
getTransit,
|
getTransit,
|
||||||
}) => (
|
}) => {
|
||||||
<CalendarProvider date={day} setDate={setDate}>
|
return (
|
||||||
<RoutinesProvider>
|
<DateProvider>
|
||||||
|
<PlannerProvider storageKey="planner">
|
||||||
<LocationProvider getTransition={getTransit} lookup={() => []}>
|
<LocationProvider getTransition={getTransit} lookup={() => []}>
|
||||||
<AgendaContextProvider day={day}>
|
<AppointmentsProvider>
|
||||||
<PlannerProvider>
|
<GoalsProvider storageKey="goals">
|
||||||
|
<RoutinesProvider storageKey="routines">
|
||||||
|
<OverrideProvider>
|
||||||
{children}
|
{children}
|
||||||
</PlannerProvider>
|
</OverrideProvider>
|
||||||
</AgendaContextProvider>
|
|
||||||
</LocationProvider>
|
|
||||||
</RoutinesProvider>
|
</RoutinesProvider>
|
||||||
</CalendarProvider>
|
</GoalsProvider>
|
||||||
|
</AppointmentsProvider>
|
||||||
|
</LocationProvider>
|
||||||
|
</PlannerProvider>
|
||||||
|
</DateProvider>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export type { SetupProps };
|
export type { SetupProps };
|
||||||
export { Setup };
|
export { Setup };
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
import { useMemo } from "react";
|
|
||||||
import { useDate, useToday } from "#/features/calendar"
|
|
||||||
import { useRoutines } from "#/features/routines";
|
|
||||||
import { Task } from "#/types/task";
|
|
||||||
import { set } from "date-fns";
|
|
||||||
|
|
||||||
const toToday = (today: Date, target: Date) => set(target, {
|
|
||||||
year: today.getFullYear(),
|
|
||||||
month: today.getMonth(),
|
|
||||||
date: today.getDate(),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useTasks = () => {
|
|
||||||
const start = useDate();
|
|
||||||
const day = useMemo(
|
|
||||||
() => start.getDay(),
|
|
||||||
[start],
|
|
||||||
)
|
|
||||||
const [fromCalendar = []] = useToday(start);
|
|
||||||
const fromRoutines = useRoutines(day);
|
|
||||||
|
|
||||||
const tasksFromCalendar = useMemo<Task[]>(
|
|
||||||
() => fromCalendar.filter(e => !e.allDay).map(task => {
|
|
||||||
const start = new Date(task.startDate);
|
|
||||||
const end = new Date(task.endDate);
|
|
||||||
const duration = end.getTime() - start.getTime();
|
|
||||||
return {
|
|
||||||
id: task.id,
|
|
||||||
name: task.title,
|
|
||||||
external: true,
|
|
||||||
required: true,
|
|
||||||
start: {
|
|
||||||
min: start,
|
|
||||||
max: start,
|
|
||||||
},
|
|
||||||
priority: 100,
|
|
||||||
duration: {
|
|
||||||
min: duration,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
[fromCalendar],
|
|
||||||
);
|
|
||||||
|
|
||||||
const tasksFromRoutines = useMemo<Task[]>(
|
|
||||||
() => fromRoutines.map(task => ({
|
|
||||||
id: task.id,
|
|
||||||
name: task.title,
|
|
||||||
locations: task.location,
|
|
||||||
start: {
|
|
||||||
min: toToday(start, task.start.min),
|
|
||||||
max: toToday(start, task.start.max),
|
|
||||||
},
|
|
||||||
priority: task.priority,
|
|
||||||
required: task.required,
|
|
||||||
duration: {
|
|
||||||
min: task.duration,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
[fromRoutines, start],
|
|
||||||
);
|
|
||||||
|
|
||||||
const tasks = useMemo(
|
|
||||||
() => ({
|
|
||||||
calendar: tasksFromCalendar,
|
|
||||||
routines: tasksFromRoutines,
|
|
||||||
all: [
|
|
||||||
...tasksFromRoutines,
|
|
||||||
...tasksFromCalendar,
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
[tasksFromCalendar, tasksFromRoutines],
|
|
||||||
);
|
|
||||||
|
|
||||||
return tasks;
|
|
||||||
}
|
|
||||||
46
src/features/tasks/hooks.tsx
Normal file
46
src/features/tasks/hooks.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export const useTasks = (type?: TaskType) => {
|
||||||
|
const [appointments] = useAppointments();
|
||||||
|
const routines = useRoutines();
|
||||||
|
const goals = useGoals();
|
||||||
|
|
||||||
|
const tasks = useMemo<Task[]>(
|
||||||
|
() => {
|
||||||
|
if (!type) {
|
||||||
|
return [...(appointments || []), ...routines, ...goals];
|
||||||
|
}
|
||||||
|
const map = {
|
||||||
|
[TaskType.routine]: routines,
|
||||||
|
[TaskType.appointment]: appointments,
|
||||||
|
[TaskType.goal]: goals,
|
||||||
|
}
|
||||||
|
return map[type] || [];
|
||||||
|
},
|
||||||
|
[appointments, routines, goals, type],
|
||||||
|
);
|
||||||
|
|
||||||
|
return tasks;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSetTask = () => {
|
||||||
|
const setRoutine = useSetRoutine();
|
||||||
|
const setGoal = useSetGoals();
|
||||||
|
|
||||||
|
const result = useAsyncCallback(
|
||||||
|
async (task: Task) => {
|
||||||
|
if (task.type === TaskType.routine) {
|
||||||
|
await setRoutine(task);
|
||||||
|
} else if (task.type === TaskType.goal) {
|
||||||
|
await setGoal(task);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setRoutine, setGoal],
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
export type UserLocation = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
location?: {
|
|
||||||
longitute: number;
|
|
||||||
latitude: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Transition = {
|
|
||||||
time: number;
|
|
||||||
usableTime: number;
|
|
||||||
to: UserLocation;
|
|
||||||
from: UserLocation;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetTransition = (
|
|
||||||
from: UserLocation,
|
|
||||||
to: UserLocation,
|
|
||||||
time: Date,
|
|
||||||
) => Promise<Transition>;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { UserLocation } from "./location";
|
|
||||||
|
|
||||||
export type PlannedTask = {
|
|
||||||
type: 'task';
|
|
||||||
name: string;
|
|
||||||
start: Date;
|
|
||||||
external?: boolean;
|
|
||||||
end: Date;
|
|
||||||
score: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PlannedTransition = {
|
|
||||||
type: 'transition';
|
|
||||||
start: Date;
|
|
||||||
end: Date;
|
|
||||||
from: UserLocation;
|
|
||||||
to: UserLocation;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlanItem = PlannedTask | PlannedTransition;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { UserLocation } from "./location";
|
|
||||||
|
|
||||||
export type Task = {
|
|
||||||
id: string;
|
|
||||||
external?: boolean;
|
|
||||||
name: string;
|
|
||||||
locations?: UserLocation[];
|
|
||||||
count?: number;
|
|
||||||
required: boolean;
|
|
||||||
priority: number;
|
|
||||||
start: {
|
|
||||||
min: Date;
|
|
||||||
max: Date;
|
|
||||||
};
|
|
||||||
duration: {
|
|
||||||
min: number;
|
|
||||||
prefered?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
29
src/ui/components/base/group/header.tsx
Normal file
29
src/ui/components/base/group/header.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
import { Row, Cell } from '../row';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
add?: () => void;
|
||||||
|
onPress?: () => void;
|
||||||
|
left?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header({ title, add, onPress, left }: Props) {
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
onPress={onPress}
|
||||||
|
left={left}
|
||||||
|
title={title}
|
||||||
|
right={
|
||||||
|
add && (
|
||||||
|
<Cell onPress={add}>
|
||||||
|
<Icon name="plus-circle" size={18} />
|
||||||
|
</Cell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Header };
|
||||||
75
src/ui/components/base/group/index.tsx
Normal file
75
src/ui/components/base/group/index.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import React, { Fragment, ReactNode, useState } from 'react';
|
||||||
|
import styled from 'styled-components/native';
|
||||||
|
import Collapsible from 'react-native-collapsible';
|
||||||
|
import { Body1 } from '#/ui/typography';
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
import { Row, Cell } from '../row';
|
||||||
|
import { Header } from './header';
|
||||||
|
|
||||||
|
interface ListProps<T> {
|
||||||
|
title: string;
|
||||||
|
items: T[];
|
||||||
|
startHidden?: boolean;
|
||||||
|
getKey: (item: T) => any;
|
||||||
|
render: (item: T) => ReactNode;
|
||||||
|
add?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChildProps {
|
||||||
|
title: string;
|
||||||
|
startHidden?: boolean;
|
||||||
|
add?: () => void;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Wrapper = styled.View`
|
||||||
|
border-radius: 7px;
|
||||||
|
background: ${({ theme }) => theme.colors.background};
|
||||||
|
shadow-offset: 0 0;
|
||||||
|
shadow-opacity: 0.1;
|
||||||
|
shadow-color: ${({ theme }) => theme.colors.shadow};
|
||||||
|
shadow-radius: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function Group<T = any>(props: ListProps<T> | ChildProps) {
|
||||||
|
const [visible, setVisible] = useState(!props.startHidden);
|
||||||
|
const { title, items, getKey, render, add, children } =
|
||||||
|
props as ListProps<T> & ChildProps;
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Wrapper>
|
||||||
|
<>
|
||||||
|
<Header
|
||||||
|
left={
|
||||||
|
<Cell><Icon name={visible ? 'chevron-down' : 'chevron-up'} size={18} /></Cell>
|
||||||
|
}
|
||||||
|
title={title}
|
||||||
|
add={add}
|
||||||
|
onPress={() => setVisible(!visible)}
|
||||||
|
/>
|
||||||
|
<Collapsible collapsed={!visible}>
|
||||||
|
{items && items.map((item, i) => (
|
||||||
|
<Fragment key={getKey(item) || i}>{render(item)}</Fragment>
|
||||||
|
))}
|
||||||
|
{children}
|
||||||
|
{!children && (!items || items.length === 0) && (
|
||||||
|
<Row
|
||||||
|
left={
|
||||||
|
<Cell>
|
||||||
|
<Icon color="textShade" name="maximize" />
|
||||||
|
</Cell>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Body1 style={{ marginLeft: 10 }} color="textShade">
|
||||||
|
Empty
|
||||||
|
</Body1>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
|
</Collapsible>
|
||||||
|
</>
|
||||||
|
</Wrapper>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Group };
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
export * from './icon';
|
export * from './icon';
|
||||||
export * from './modal';
|
export * from './modal';
|
||||||
export * from './icon';
|
|
||||||
export * from './form';
|
|
||||||
export * from './page';
|
export * from './page';
|
||||||
export * from './popup';
|
export * from './popup';
|
||||||
export * from './row';
|
export * from './row';
|
||||||
export * from './form';
|
|
||||||
export * from './button';
|
export * from './button';
|
||||||
|
export * from './group';
|
||||||
@@ -2,10 +2,10 @@ import React, { ReactNode } from 'react';
|
|||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import styled from 'styled-components/native';
|
import styled from 'styled-components/native';
|
||||||
import { Icon } from '../icon';
|
import { Icon } from '../icon';
|
||||||
import { Row, Cell } from '../row';
|
import { Row, Cell, RowProps } from '../row';
|
||||||
import { Page } from '../page';
|
import { Page } from '../page';
|
||||||
|
|
||||||
interface Props {
|
type Props = RowProps & {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
@@ -17,19 +17,25 @@ const Top = styled.Pressable`
|
|||||||
const Wrapper = styled.View`
|
const Wrapper = styled.View`
|
||||||
background: ${({ theme }) => theme.colors.background};
|
background: ${({ theme }) => theme.colors.background};
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
shadow-color: ${({ theme }) => theme.colors.shadow};
|
shadow-color: ${({ theme }) => theme.colors.shadow};
|
||||||
shadow-offset: 0 0;
|
shadow-offset: 0 0;
|
||||||
shadow-opacity: 1;
|
shadow-opacity: 1;
|
||||||
shadow-radius: 200px;
|
shadow-radius: 200px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
margin-bottom: -12px;
|
margin-bottom: -12px;
|
||||||
|
max-height: 80%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Outer = styled.View`
|
const Outer = styled.View`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Popup: React.FC<Props> = ({ visible, children, onClose }) => {
|
const Content = styled.ScrollView`
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Popup: React.FC<Props> = ({ children, onClose, right, ...rowProps }) => {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -38,13 +44,19 @@ const Popup: React.FC<Props> = ({ visible, children, onClose }) => {
|
|||||||
<Top onPress={onClose} />
|
<Top onPress={onClose} />
|
||||||
<Wrapper style={{ paddingBottom: insets.bottom + 12 }}>
|
<Wrapper style={{ paddingBottom: insets.bottom + 12 }}>
|
||||||
<Row
|
<Row
|
||||||
|
{...rowProps}
|
||||||
right={
|
right={
|
||||||
|
<>
|
||||||
|
{right}
|
||||||
<Cell onPress={onClose}>
|
<Cell onPress={onClose}>
|
||||||
<Icon name="x-circle" />
|
<Icon name="x-circle" />
|
||||||
</Cell>
|
</Cell>
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Content>
|
||||||
{children}
|
{children}
|
||||||
|
</Content>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</Outer>
|
</Outer>
|
||||||
</Page>
|
</Page>
|
||||||
@@ -3,7 +3,7 @@ import { TouchableOpacity } from 'react-native';
|
|||||||
import styled from 'styled-components/native';
|
import styled from 'styled-components/native';
|
||||||
import { Theme } from '#/ui/theme';
|
import { Theme } from '#/ui/theme';
|
||||||
|
|
||||||
interface Props {
|
type CellProps = {
|
||||||
accessibilityRole?: TouchableOpacity['props']['accessibilityRole'];
|
accessibilityRole?: TouchableOpacity['props']['accessibilityRole'];
|
||||||
accessibilityLabel?: string;
|
accessibilityLabel?: string;
|
||||||
accessibilityHint?: string;
|
accessibilityHint?: string;
|
||||||
@@ -34,7 +34,7 @@ const Wrapper = styled.View<{
|
|||||||
|
|
||||||
const Touch = styled.TouchableOpacity``;
|
const Touch = styled.TouchableOpacity``;
|
||||||
|
|
||||||
const Cell: React.FC<Props> = ({ children, onPress, ...props}) => {
|
const Cell: React.FC<CellProps> = ({ children, onPress, ...props}) => {
|
||||||
const {
|
const {
|
||||||
accessibilityLabel,
|
accessibilityLabel,
|
||||||
accessibilityRole,
|
accessibilityRole,
|
||||||
@@ -62,4 +62,5 @@ const Cell: React.FC<Props> = ({ children, onPress, ...props}) => {
|
|||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { CellProps };
|
||||||
export { Cell };
|
export { Cell };
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import styled from 'styled-components/native';
|
import styled from 'styled-components/native';
|
||||||
import { Title1, Body1, Overline } from '#/ui/typography';
|
import { Title1, Body1, Overline } from '#/ui/typography';
|
||||||
import { Cell } from './cell';
|
import { Cell, CellProps } from './cell';
|
||||||
|
|
||||||
type RowProps = {
|
type RowProps = CellProps & {
|
||||||
background?: string;
|
background?: string;
|
||||||
top?: ReactNode;
|
top?: ReactNode;
|
||||||
left?: ReactNode;
|
left?: ReactNode;
|
||||||
@@ -42,8 +42,9 @@ const Row: React.FC<RowProps> = ({
|
|||||||
description,
|
description,
|
||||||
children,
|
children,
|
||||||
onPress,
|
onPress,
|
||||||
|
...cellProps
|
||||||
}) => (
|
}) => (
|
||||||
<Cell background={background} opacity={opacity} onPress={onPress}>
|
<Cell {...cellProps} background={background} opacity={opacity} onPress={onPress}>
|
||||||
{left}
|
{left}
|
||||||
<Cell flex={1} direction="column" align="stretch">
|
<Cell flex={1} direction="column" align="stretch">
|
||||||
{!!top}
|
{!!top}
|
||||||
64
src/ui/components/date/bar/index.tsx
Normal file
64
src/ui/components/date/bar/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import CalendarStrip from 'react-native-calendar-strip';
|
||||||
|
import { dayUtils, useDate, useSetDate } from "#/features/day";
|
||||||
|
import { useTheme } from "styled-components/native";
|
||||||
|
|
||||||
|
const DateBar: React.FC = () => {
|
||||||
|
const date = useDate();
|
||||||
|
const theme = useTheme();
|
||||||
|
const setDate = useSetDate();
|
||||||
|
const selected = useMemo(
|
||||||
|
() => [{
|
||||||
|
date: dayUtils.dayToDate(date),
|
||||||
|
lines: [{ color: theme.colors.icon }],
|
||||||
|
}],
|
||||||
|
[date],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<CalendarStrip
|
||||||
|
markedDates={selected}
|
||||||
|
style={{
|
||||||
|
height: 150,
|
||||||
|
paddingTop: 60,
|
||||||
|
paddingBottom: 10,
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
}}
|
||||||
|
calendarColor={'#fff'}
|
||||||
|
selectedDate={dayUtils.dayToDate(date)}
|
||||||
|
startingDate={dayUtils.dayToDate(date)}
|
||||||
|
onDateSelected={(date) => {
|
||||||
|
setDate(dayUtils.dateToDay(date.utc().toDate()));
|
||||||
|
}}
|
||||||
|
shouldAllowFontScaling={false}
|
||||||
|
iconContainer={{flex: 0.1}}
|
||||||
|
calendarHeaderStyle={{
|
||||||
|
color: theme.colors.text,
|
||||||
|
fontSize: theme.font.baseSize * 1.2,
|
||||||
|
}}
|
||||||
|
highlightDateNameStyle={{
|
||||||
|
color: theme.colors.icon,
|
||||||
|
fontSize: theme.font.baseSize * 0.6,
|
||||||
|
}}
|
||||||
|
iconLeftStyle={{
|
||||||
|
tintColor: theme.colors.text,
|
||||||
|
}}
|
||||||
|
iconRightStyle={{
|
||||||
|
tintColor: theme.colors.text,
|
||||||
|
}}
|
||||||
|
highlightDateNumberStyle={{
|
||||||
|
color: theme.colors.icon,
|
||||||
|
fontSize: theme.font.baseSize * 1.2,
|
||||||
|
}}
|
||||||
|
dateNumberStyle={{
|
||||||
|
color: theme.colors.text,
|
||||||
|
fontSize: theme.font.baseSize * 1.2,
|
||||||
|
}}
|
||||||
|
dateNameStyle={{
|
||||||
|
color: theme.colors.text,
|
||||||
|
fontSize: theme.font.baseSize * 0.6,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DateBar };
|
||||||
1
src/ui/components/date/index.ts
Normal file
1
src/ui/components/date/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './bar';
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Row } from "../../row"
|
import { Row, RowProps } from "#/ui/components/base"
|
||||||
|
|
||||||
type CheckboxProps = {
|
type CheckboxProps = RowProps & {
|
||||||
value?: boolean;
|
value?: boolean;
|
||||||
label: string;
|
label: string;
|
||||||
onChange: (value: boolean) => void;
|
onChange: (value: boolean) => void;
|
||||||
@@ -10,8 +10,10 @@ const Checkbok: React.FC<CheckboxProps> = ({
|
|||||||
value,
|
value,
|
||||||
label,
|
label,
|
||||||
onChange,
|
onChange,
|
||||||
|
...rowProps
|
||||||
}) => (
|
}) => (
|
||||||
<Row
|
<Row
|
||||||
|
{...rowProps}
|
||||||
overline={label}
|
overline={label}
|
||||||
title={value? 'Yes' : 'No'}
|
title={value? 'Yes' : 'No'}
|
||||||
onPress={() => onChange(!value)}
|
onPress={() => onChange(!value)}
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './checkbox';
|
export * from './checkbox';
|
||||||
|
export * from './time';
|
||||||
|
export * from './optional-selector';
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styled, { useTheme } from 'styled-components/native';
|
import styled from 'styled-components/native';
|
||||||
import { Row, RowProps } from '../../row';
|
import { Row, RowProps } from '#/ui/components/base';
|
||||||
|
|
||||||
type Props = RowProps & {
|
type Props = RowProps & {
|
||||||
|
label: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value: string;
|
value: string;
|
||||||
onChangeText: (text: string) => any;
|
onChangeText: (text: string) => any;
|
||||||
@@ -17,11 +18,11 @@ const InputField = styled.TextInput`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const TextInput: React.FC<Props> = ({ placeholder, value, onChangeText, children, ...row }) => {
|
const TextInput: React.FC<Props> = ({ label, placeholder, value, onChangeText, children, ...row }) => {
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
return (
|
||||||
<Row overline={placeholder} {...row}>
|
<Row overline={label} {...row}>
|
||||||
<InputField
|
<InputField
|
||||||
|
placeholder={placeholder}
|
||||||
value={value}
|
value={value}
|
||||||
onChangeText={onChangeText}
|
onChangeText={onChangeText}
|
||||||
/>
|
/>
|
||||||
|
|||||||
116
src/ui/components/form/optional-selector/index.tsx
Normal file
116
src/ui/components/form/optional-selector/index.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { Body1 } from "#/ui/typography";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import styled from "styled-components/native";
|
||||||
|
import { Row, RowProps, Cell, Icon } from "../../base";
|
||||||
|
|
||||||
|
type Props<T> = {
|
||||||
|
label: string;
|
||||||
|
setEnabled: (enabled: boolean) => void;
|
||||||
|
enabled: boolean;
|
||||||
|
onChange: (items: T[]) => void;
|
||||||
|
items: T[];
|
||||||
|
enabledText: string;
|
||||||
|
disabledText: string;
|
||||||
|
selected?: T[];
|
||||||
|
render: (item: T) => RowProps;
|
||||||
|
getKey: (item: T) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = styled.View`
|
||||||
|
border-radius: 5px;
|
||||||
|
background: ${({ theme }) => theme.colors.shade};
|
||||||
|
border-radius: 7px;
|
||||||
|
shadow-offset: 0 0;
|
||||||
|
shadow-opacity: 0.1;
|
||||||
|
shadow-color: ${({ theme }) => theme.colors.shadow};
|
||||||
|
shadow-radius: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Top = styled.View`
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Touch = styled.TouchableOpacity`
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Content = styled.View`
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TopButton = styled.View<{ selected: boolean }>`
|
||||||
|
background: ${({ selected, theme }) => selected ? theme.colors.shade : theme.colors.background};
|
||||||
|
padding: ${({ theme }) => theme.margins.small}px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
function OptionalSelector<T>({
|
||||||
|
label,
|
||||||
|
enabled,
|
||||||
|
setEnabled,
|
||||||
|
onChange,
|
||||||
|
items,
|
||||||
|
enabledText,
|
||||||
|
disabledText,
|
||||||
|
selected,
|
||||||
|
render,
|
||||||
|
getKey,
|
||||||
|
}: Props<T>) {
|
||||||
|
const toggle = useCallback(
|
||||||
|
(item: T) => {
|
||||||
|
if (!selected) {
|
||||||
|
return onChange([item]);
|
||||||
|
}
|
||||||
|
const nextId = getKey(item);
|
||||||
|
const current = selected.find(i => getKey(i) === nextId);
|
||||||
|
if (current) {
|
||||||
|
onChange(selected.filter(i => i !== current));
|
||||||
|
} else {
|
||||||
|
onChange([...selected, item]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[selected, getKey]
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<Row overline={label}>
|
||||||
|
<Wrapper>
|
||||||
|
<Top>
|
||||||
|
<Touch onPress={() => setEnabled(false)}>
|
||||||
|
<TopButton selected={!enabled}>
|
||||||
|
<Body1>{disabledText}</Body1>
|
||||||
|
</TopButton>
|
||||||
|
</Touch>
|
||||||
|
<Touch onPress={() => setEnabled(true)}>
|
||||||
|
<TopButton selected={enabled}>
|
||||||
|
<Body1>{enabledText}</Body1>
|
||||||
|
</TopButton>
|
||||||
|
</Touch>
|
||||||
|
</Top>
|
||||||
|
{enabled && (
|
||||||
|
<Content>
|
||||||
|
{items.map((item) => {
|
||||||
|
const { left, ...props } = render(item);
|
||||||
|
const isSelected = !!selected && selected.includes(item);
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
key={getKey(item)}
|
||||||
|
{...props}
|
||||||
|
left={(
|
||||||
|
<>
|
||||||
|
<Cell onPress={() => toggle(item)}>
|
||||||
|
<Icon name={isSelected ? 'check-circle' : 'circle'} />
|
||||||
|
</Cell>
|
||||||
|
{left}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Content>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { OptionalSelector }
|
||||||
55
src/ui/components/form/time/index.tsx
Normal file
55
src/ui/components/form/time/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import styled from 'styled-components/native';
|
||||||
|
import { Row, RowProps } from '#/ui/components/base';
|
||||||
|
import { Time, timeUtils } from '#/features/data';
|
||||||
|
|
||||||
|
type Props = RowProps & {
|
||||||
|
label: string;
|
||||||
|
placeholder?: string;
|
||||||
|
value?: Time;
|
||||||
|
onChange: (time?: Time) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TimeField = styled.TextInput`
|
||||||
|
background: ${({ theme }) => theme.colors.input};
|
||||||
|
color: ${({ theme }) => theme.colors.text};
|
||||||
|
padding: ${({ theme }) => theme.margins.small}px;
|
||||||
|
font-size: ${({ theme }) => theme.font.baseSize}px;
|
||||||
|
border-radius: ${({ theme }) => theme.sizes.corners}px;
|
||||||
|
width: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TimeInput };
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
|
|
||||||
import React, { ReactNode, useMemo } from "react";
|
|
||||||
import styled from "styled-components/native";
|
|
||||||
import stringToColor from 'string-to-color';
|
|
||||||
import parseCSSColor from "parse-css-color";
|
|
||||||
import chroma from 'chroma-js';
|
|
||||||
import { PlanItem } from "#/types/plans";
|
|
||||||
|
|
||||||
type AgendaItemProps = {
|
|
||||||
item: LayoutItem;
|
|
||||||
onPress?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
type LayoutItem = {
|
|
||||||
height: number;
|
|
||||||
color: string;
|
|
||||||
body?: ReactNode;
|
|
||||||
start: Date;
|
|
||||||
end: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Time = styled.Text<{background : string}>`
|
|
||||||
font-size: 10px;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: bold;
|
|
||||||
color: ${({ background }) => background === 'transparent' ? '#222' : '#fff'};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TimeBox = styled.View<{
|
|
||||||
background: string;
|
|
||||||
}>`
|
|
||||||
margin-right: 10px;
|
|
||||||
width: 50px;
|
|
||||||
height: 100%;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: ${({ background }) => background === 'transparent' ? background : chroma(background).darken(1.5).hex()};
|
|
||||||
`;
|
|
||||||
|
|
||||||
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: 3px;
|
|
||||||
border: solid 1px ${({ background }) => background === 'transparent' ? background : chroma(background).darken(0.3).hex()};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Main = styled.View`
|
|
||||||
flex: 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
const isDark = (color: string) => {
|
|
||||||
const parsed = parseCSSColor(color);
|
|
||||||
const [r, g, b] = parsed!.values;
|
|
||||||
|
|
||||||
var luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; // per ITU-R BT.709
|
|
||||||
|
|
||||||
return luma < 150;
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatTime = (time: Date) => {
|
|
||||||
const hours = time.getHours().toString().padStart(2, '0')
|
|
||||||
const minutes = time.getMinutes().toString().padStart(2, '0')
|
|
||||||
|
|
||||||
return `${hours}:${minutes}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Touch = styled.TouchableOpacity`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AgendaItemView: React.FC<AgendaItemProps> = ({ item, onPress }) => {
|
|
||||||
const view = (
|
|
||||||
<Block height={Math.max(70, item.height / 15000)} background={item.color}>
|
|
||||||
<TimeBox background={item.color}>
|
|
||||||
<Time background={item.color}>{formatTime(item.start)}</Time>
|
|
||||||
<Time background={item.color}>{formatTime(item.end)}</Time>
|
|
||||||
</TimeBox>
|
|
||||||
<Main>
|
|
||||||
{item.body}
|
|
||||||
</Main>
|
|
||||||
<Filler />
|
|
||||||
</Block>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (onPress) {
|
|
||||||
return (
|
|
||||||
<Touch onPress={onPress}>
|
|
||||||
{view}
|
|
||||||
</Touch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return view;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { AgendaItemProps };
|
|
||||||
export { AgendaItemView };
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import React, { ReactNode, useMemo } from "react";
|
|
||||||
import styled from "styled-components/native";
|
|
||||||
import stringToColor from 'string-to-color';
|
|
||||||
import chroma from 'chroma-js';
|
|
||||||
import { PlanItem } from "#/types/plans";
|
|
||||||
import { AgendaItemView } from "./agenda-item";
|
|
||||||
|
|
||||||
type DayViewProps = {
|
|
||||||
plan: PlanItem[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type LayoutItem = {
|
|
||||||
height: number;
|
|
||||||
color: string;
|
|
||||||
body?: ReactNode;
|
|
||||||
start: Date;
|
|
||||||
end: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Wrapper = styled.View`
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Title = styled.Text`
|
|
||||||
`;
|
|
||||||
|
|
||||||
const getBody = (item: PlanItem) => {
|
|
||||||
if (item.type === 'transition') {
|
|
||||||
return <Title>{item.from.title} ➜ {item.to.title}</Title>
|
|
||||||
} else {
|
|
||||||
return <Title>{item.name}</Title>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const DayView: React.FC<DayViewProps> = ({ plan }) => {
|
|
||||||
const layout = useMemo(
|
|
||||||
() => {
|
|
||||||
const [...planItems] = [...plan];
|
|
||||||
const items: LayoutItem[] = [];
|
|
||||||
var lastPlanItem: PlanItem | undefined;
|
|
||||||
for (let planItem of planItems) {
|
|
||||||
if (lastPlanItem && planItem.start.getTime() - lastPlanItem.end.getTime() > 0) {
|
|
||||||
items.push({
|
|
||||||
height: planItem.start.getTime() - lastPlanItem.end.getTime(),
|
|
||||||
color: 'transparent',
|
|
||||||
start: lastPlanItem.end,
|
|
||||||
end: planItem.start,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
let color = planItem.type === 'transition' ? '#34495e' : stringToColor(planItem.name);
|
|
||||||
color = chroma(color).luminance(0.7).saturate(1).brighten(0.6).hex();
|
|
||||||
items.push({
|
|
||||||
height: planItem.end.getTime() - planItem.start.getTime(),
|
|
||||||
color,
|
|
||||||
start: planItem.start,
|
|
||||||
end: planItem.end,
|
|
||||||
body: getBody(planItem),
|
|
||||||
});
|
|
||||||
lastPlanItem = planItem;
|
|
||||||
}
|
|
||||||
return items;
|
|
||||||
},
|
|
||||||
[plan],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
{layout.map((item, i) => (
|
|
||||||
<AgendaItemView key={i} item={item} />
|
|
||||||
))}
|
|
||||||
</Wrapper>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
export type { DayViewProps };
|
|
||||||
export { DayView };
|
|
||||||
44
src/ui/components/tasks/group/index.tsx
Normal file
44
src/ui/components/tasks/group/index.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { TaskType } from "#/features/data";
|
||||||
|
import { useTasks } from "#/features/tasks";
|
||||||
|
import { Group } from "#/ui/components/base"
|
||||||
|
import { RootNavigationProp } from "#/ui/router";
|
||||||
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { TaskListItem } from "../list-item";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
type: TaskType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskGroup: React.FC<Props> = ({ type }) => {
|
||||||
|
const { navigate } = useNavigation<RootNavigationProp>();
|
||||||
|
const tasks = useTasks(type);
|
||||||
|
|
||||||
|
const add = useCallback(
|
||||||
|
(type: TaskType) => {
|
||||||
|
navigate('add-task', {
|
||||||
|
type,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
title={type}
|
||||||
|
add={() => add(type)}
|
||||||
|
items={tasks || []}
|
||||||
|
getKey={(task) => task.id}
|
||||||
|
render={(task) => (
|
||||||
|
<TaskListItem
|
||||||
|
item={task}
|
||||||
|
onPress={() => {
|
||||||
|
navigate('add-task', { id: task.id });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TaskGroup };
|
||||||
1
src/ui/components/tasks/index.ts
Normal file
1
src/ui/components/tasks/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './list-item';
|
||||||
17
src/ui/components/tasks/list-item/index.tsx
Normal file
17
src/ui/components/tasks/list-item/index.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Task } from "#/features/data";
|
||||||
|
import { Row, RowProps } from "../../base";
|
||||||
|
|
||||||
|
type Props = RowProps & {
|
||||||
|
item: Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TaskListItem: React.FC<Props> = ({ item, ...rowProps }) => {
|
||||||
|
return (
|
||||||
|
<Row
|
||||||
|
{...rowProps}
|
||||||
|
title={item.title}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TaskListItem };
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './components';
|
export * from './components/base';
|
||||||
export * from './theme';
|
export * from './theme';
|
||||||
|
|||||||
@@ -1,113 +1,2 @@
|
|||||||
import { useMemo } from 'react';
|
export { Router } from './router';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
export * from './types';
|
||||||
import { useTheme } from 'styled-components/native';
|
|
||||||
import { LocationListScreen } from '#/ui/screens/locations/list';
|
|
||||||
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
|
|
||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
||||||
import { RoutinesListScreen } from '../screens/routines/list';
|
|
||||||
import { LocationSetScreen } from '../screens/locations/set';
|
|
||||||
import { PlanDayScreen } from '../screens/plan/day';
|
|
||||||
import { CalendarSelectScreen } from '../screens/calendars/select';
|
|
||||||
import { RoutineSetScreen } from '../screens/routines/set';
|
|
||||||
import { TaskListScreen } from '../screens/plan/tasks';
|
|
||||||
import { AgendaContextSetScreen } from '../screens/plan/set';
|
|
||||||
import { Icon } from '../components';
|
|
||||||
import { PlanSettingsScreen } from '../screens/plan/settings';
|
|
||||||
|
|
||||||
const MainTabsNvaigator = createBottomTabNavigator();
|
|
||||||
|
|
||||||
const MainTabs: React.FC = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
return (
|
|
||||||
<MainTabsNvaigator.Navigator
|
|
||||||
screenOptions={{
|
|
||||||
tabBarActiveTintColor: theme.colors.primary,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MainTabsNvaigator.Screen
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
tabBarLabel: 'Prepare',
|
|
||||||
tabBarIcon: ({ focused }) => <Icon color={focused ? 'primary' : 'text'} name="check-square" />,
|
|
||||||
}}
|
|
||||||
name="tasks"
|
|
||||||
component={TaskListScreen}
|
|
||||||
/>
|
|
||||||
<MainTabsNvaigator.Screen
|
|
||||||
name="plan"
|
|
||||||
component={PlanDayScreen}
|
|
||||||
options={{
|
|
||||||
tabBarLabel: 'Plan',
|
|
||||||
tabBarIcon: ({ focused }) => <Icon color={focused ? 'primary' : 'text'} name="calendar" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MainTabsNvaigator.Screen
|
|
||||||
name="locations"
|
|
||||||
component={LocationListScreen}
|
|
||||||
options={{
|
|
||||||
tabBarLabel: 'Locations',
|
|
||||||
tabBarIcon: ({ focused }) => <Icon color={focused ? 'primary' : 'text'} name="map-pin" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MainTabsNvaigator.Screen
|
|
||||||
name="routines"
|
|
||||||
component={RoutinesListScreen}
|
|
||||||
options={{
|
|
||||||
tabBarLabel: 'Routines',
|
|
||||||
tabBarIcon: ({ focused }) => <Icon color={focused ? 'primary' : 'text'} name="activity" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<MainTabsNvaigator.Screen
|
|
||||||
name="calendars"
|
|
||||||
component={CalendarSelectScreen}
|
|
||||||
options={{
|
|
||||||
tabBarLabel: 'Calendars',
|
|
||||||
tabBarIcon: ({ focused }) => <Icon color={focused ? 'primary' : 'text'} name="more-vertical" />,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</MainTabsNvaigator.Navigator>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RootNavigator = createNativeStackNavigator();
|
|
||||||
|
|
||||||
const Root: React.FC = () => (
|
|
||||||
<RootNavigator.Navigator screenOptions={{ headerShown: false }}>
|
|
||||||
<RootNavigator.Group>
|
|
||||||
<RootNavigator.Screen name="main" component={MainTabs} />
|
|
||||||
</RootNavigator.Group>
|
|
||||||
<RootNavigator.Group screenOptions={{ presentation: 'transparentModal' }}>
|
|
||||||
<RootNavigator.Screen name="locationSet" component={LocationSetScreen} />
|
|
||||||
<RootNavigator.Screen name="routineSet" component={RoutineSetScreen} />
|
|
||||||
<RootNavigator.Screen name="agendaContextSet" component={AgendaContextSetScreen} />
|
|
||||||
<RootNavigator.Screen name="planSettings" component={PlanSettingsScreen} />
|
|
||||||
</RootNavigator.Group>
|
|
||||||
</RootNavigator.Navigator>
|
|
||||||
);
|
|
||||||
|
|
||||||
const Router: React.FC = () => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const baseTheme = useMemo(
|
|
||||||
() => DefaultTheme,
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
const navigationTheme = useMemo(
|
|
||||||
() => ({
|
|
||||||
...baseTheme,
|
|
||||||
colors: {
|
|
||||||
...baseTheme.colors,
|
|
||||||
background: theme.colors.shade,
|
|
||||||
card: theme.colors.background,
|
|
||||||
text: theme.colors.text,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[baseTheme, theme],
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<NavigationContainer theme={navigationTheme}>
|
|
||||||
<Root />
|
|
||||||
</NavigationContainer>
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Router };
|
|
||||||
|
|||||||
98
src/ui/router/router.tsx
Normal file
98
src/ui/router/router.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
|
import { useTheme } from 'styled-components/native';
|
||||||
|
import { NavigationContainer, DefaultTheme } from '@react-navigation/native';
|
||||||
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
|
import { Icon } from '../components/base';
|
||||||
|
import { DayScreen } from '../screens/day';
|
||||||
|
import { TaskAddScreen } from '../screens/task/add';
|
||||||
|
import { MainTabParamList, RootStackParamList } from './types';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
import { MoreScreen } from '../screens/more';
|
||||||
|
import { LocationListScreen } from '../screens/locations/list';
|
||||||
|
import { LocationSetScreen } from '../screens/locations/set';
|
||||||
|
|
||||||
|
const MoreStackNavigator = createNativeStackNavigator();
|
||||||
|
|
||||||
|
const MoreStack: React.FC = () => (
|
||||||
|
<MoreStackNavigator.Navigator>
|
||||||
|
<MoreStackNavigator.Screen name="more-main" component={MoreScreen} />
|
||||||
|
<MoreStackNavigator.Screen name="locations" component={LocationListScreen} />
|
||||||
|
</MoreStackNavigator.Navigator>
|
||||||
|
);
|
||||||
|
|
||||||
|
const MainTabsNvaigator = createBottomTabNavigator<MainTabParamList>();
|
||||||
|
|
||||||
|
const MainTabs: React.FC = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<MainTabsNvaigator.Navigator
|
||||||
|
screenOptions={{
|
||||||
|
tabBarActiveTintColor: theme.colors.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MainTabsNvaigator.Screen
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
tabBarLabel: 'Days',
|
||||||
|
tabBarIcon: ({ focused }) => <Icon color={focused ? 'primary' : 'text'} name="check-square" />,
|
||||||
|
}}
|
||||||
|
name="day"
|
||||||
|
component={DayScreen}
|
||||||
|
/>
|
||||||
|
<MainTabsNvaigator.Screen
|
||||||
|
options={{
|
||||||
|
headerShown: false,
|
||||||
|
tabBarLabel: 'More',
|
||||||
|
tabBarIcon: ({ focused }) => <Icon color={focused ? 'primary' : 'text'} name="more-vertical" />,
|
||||||
|
}}
|
||||||
|
name="more"
|
||||||
|
component={MoreStack}
|
||||||
|
/>
|
||||||
|
</MainTabsNvaigator.Navigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RootNavigator = Platform.OS === 'web'
|
||||||
|
? createStackNavigator<RootStackParamList>()
|
||||||
|
: createNativeStackNavigator<RootStackParamList>();
|
||||||
|
|
||||||
|
const Root: React.FC = () => (
|
||||||
|
<RootNavigator.Navigator screenOptions={{ headerShown: false, animationEnabled: true }}>
|
||||||
|
<RootNavigator.Group>
|
||||||
|
<RootNavigator.Screen name="main" component={MainTabs} />
|
||||||
|
</RootNavigator.Group>
|
||||||
|
<RootNavigator.Group screenOptions={{ presentation: 'transparentModal' }}>
|
||||||
|
<RootNavigator.Screen name="add-task" component={TaskAddScreen} />
|
||||||
|
<RootNavigator.Screen name="set-location" component={LocationSetScreen} />
|
||||||
|
</RootNavigator.Group>
|
||||||
|
</RootNavigator.Navigator>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Router: React.FC = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const baseTheme = useMemo(
|
||||||
|
() => DefaultTheme,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const navigationTheme = useMemo(
|
||||||
|
() => ({
|
||||||
|
...baseTheme,
|
||||||
|
colors: {
|
||||||
|
...baseTheme.colors,
|
||||||
|
background: theme.colors.shade,
|
||||||
|
card: theme.colors.background,
|
||||||
|
text: theme.colors.text,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[baseTheme, theme],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<NavigationContainer theme={navigationTheme}>
|
||||||
|
<Root />
|
||||||
|
</NavigationContainer>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Router };
|
||||||
34
src/ui/router/types.ts
Normal file
34
src/ui/router/types.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { TaskType } from "#/features/data";
|
||||||
|
import { NavigatorScreenParams, RouteProp } from "@react-navigation/native";
|
||||||
|
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
|
||||||
|
|
||||||
|
export type RootStackParamList = {
|
||||||
|
main: undefined;
|
||||||
|
'add-task': {
|
||||||
|
type: TaskType;
|
||||||
|
} | {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
'set-location': {
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type RootRouteProp = RouteProp<RootStackParamList>;
|
||||||
|
export type RootNavigationProp = NativeStackNavigationProp<RootStackParamList>;
|
||||||
|
|
||||||
|
export type LocationSetScreenRouteProp = RouteProp<RootStackParamList, 'set-location'>;
|
||||||
|
|
||||||
|
export type TaskAddScreenRouteProp = RouteProp<RootStackParamList, 'add-task'>;
|
||||||
|
export type TaskAddScreenNavigationProp = NativeStackNavigationProp<
|
||||||
|
RootStackParamList,
|
||||||
|
'add-task'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type MainTabParamList = {
|
||||||
|
day: NavigatorScreenParams<RootStackParamList>;
|
||||||
|
more: NavigatorScreenParams<RootStackParamList>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DayScreenRouteProp = RouteProp<MainTabParamList, 'day'>;
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import { useCalendars, useSelectedCalendars, useSetSelectedCalendars } from "#/features/calendar"
|
|
||||||
import { Calendar } from "expo-calendar";
|
|
||||||
import { useCallback } from "react";
|
|
||||||
import styled from "styled-components/native";
|
|
||||||
|
|
||||||
const Wrapper = styled.View`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Button = styled.Button`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const CalendarSelectScreen: React.FC = () => {
|
|
||||||
const calendars = useCalendars();
|
|
||||||
const selected = useSelectedCalendars();
|
|
||||||
const setSelected = useSetSelectedCalendars();
|
|
||||||
const toggle = useCallback(
|
|
||||||
(calendar: Calendar) => {
|
|
||||||
const isSelected = !!selected.find(c => c.id === calendar.id);
|
|
||||||
if (isSelected) {
|
|
||||||
setSelected(selected.filter(c => c.id !== calendar.id));
|
|
||||||
} else {
|
|
||||||
setSelected([
|
|
||||||
...selected,
|
|
||||||
calendar,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[selected]
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
{calendars.map((calendar) => (
|
|
||||||
<Button
|
|
||||||
key={calendar.id}
|
|
||||||
title={calendar.title + (selected.includes(calendar) ? ' -y' : '-n')}
|
|
||||||
onPress={() => toggle(calendar)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
))}
|
|
||||||
</Wrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { CalendarSelectScreen };
|
|
||||||
34
src/ui/screens/day/index.tsx
Normal file
34
src/ui/screens/day/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useAppointmentStatus } from "#/features/appointments";
|
||||||
|
import { AppointmentsStatus } from "#/features/appointments/context";
|
||||||
|
import { TaskType } from "#/features/data";
|
||||||
|
import { dayUtils, useDate } from "#/features/day";
|
||||||
|
import { useSetStartTimeOverride, useStartTimeOverride } from "#/features/overrides";
|
||||||
|
import { DateBar } from "#/ui/components/date"
|
||||||
|
import { TimeInput } from "#/ui/components/form";
|
||||||
|
import { TaskGroup } from "#/ui/components/tasks/group";
|
||||||
|
|
||||||
|
const DayScreen: React.FC = () => {
|
||||||
|
const date = useDate();
|
||||||
|
const appointmentStatus = useAppointmentStatus();
|
||||||
|
const startTimeOverride = useStartTimeOverride();
|
||||||
|
const [setStartTimeOverride] = useSetStartTimeOverride();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DateBar />
|
||||||
|
<TimeInput
|
||||||
|
key={dayUtils.toId(date)}
|
||||||
|
label="Start time"
|
||||||
|
value={startTimeOverride}
|
||||||
|
onChange={setStartTimeOverride}
|
||||||
|
/>
|
||||||
|
{appointmentStatus === AppointmentsStatus.rejected && (
|
||||||
|
<TaskGroup type={TaskType.appointment} />
|
||||||
|
)}
|
||||||
|
<TaskGroup type={TaskType.routine} />
|
||||||
|
<TaskGroup type={TaskType.goal} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DayScreen };
|
||||||
@@ -1,43 +1,34 @@
|
|||||||
import { useLocations, useRemoveLocation } from "#/features/location"
|
import { useLocations, useRemoveLocation } from "#/features/location"
|
||||||
import { Button, Cell } from "#/ui/components";
|
import { Button, Cell, Icon, Page, Row } from "#/ui/components/base";
|
||||||
import { Row } from "#/ui/components/row/row";
|
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import { FlatList } from "react-native";
|
import { FlatList } from "react-native";
|
||||||
import styled from "styled-components/native";
|
|
||||||
|
|
||||||
const Wrapper = styled.View`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Name = styled.Text`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LocationListScreen: React.FC = () => {
|
const LocationListScreen: React.FC = () => {
|
||||||
|
const { navigate } = useNavigation();
|
||||||
const locations = useLocations();
|
const locations = useLocations();
|
||||||
const removeLocation = useRemoveLocation();
|
const removeLocation = useRemoveLocation();
|
||||||
const { navigate } = useNavigation();
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Page>
|
||||||
<Button icon="plus-circle" onPress={() => navigate('locationSet')} />
|
<Button title="Add" onPress={() => navigate('set-location', {})}/>
|
||||||
<FlatList
|
<FlatList
|
||||||
data={Object.values(locations)}
|
data={locations}
|
||||||
keyExtractor={item => item.id}
|
keyExtractor={item => item.id}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<Row
|
<Row
|
||||||
title={item.title}
|
title={item.title}
|
||||||
onPress={() => {
|
right={(
|
||||||
navigate('locationSet', { id: item.id });
|
<Cell onPress={() => removeLocation(item.id)}>
|
||||||
}}
|
<Icon
|
||||||
right={
|
name="trash"
|
||||||
<Cell>
|
color="destructive"
|
||||||
<Button type="destructive" icon="trash" onPress={() => removeLocation(item.id)} />
|
/>
|
||||||
</Cell>
|
</Cell>
|
||||||
}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Wrapper>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,56 @@
|
|||||||
import { useLocations, useSetLocation } from "#/features/location";
|
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
|
||||||
import { Popup, Button, TextInput } from "#/ui/components";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { useAsyncCallback } from "#/features/async";
|
||||||
|
import { useLocations, useSetLocation } from "#/features/location"
|
||||||
|
import { Button, Popup, Row } from "#/ui/components/base";
|
||||||
|
import { TextInput } from "#/ui/components/form";
|
||||||
|
import { LocationSetScreenRouteProp, RootNavigationProp } from "#/ui/router";
|
||||||
|
import { useNavigation, useRoute } from "@react-navigation/native";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const LocationSetScreen: React.FC = () => {
|
const LocationSetScreen: React.FC = () => {
|
||||||
const { params = {} } = useRoute() as any;
|
const {
|
||||||
const id = useMemo(
|
params: { id = nanoid() },
|
||||||
() => params.id || nanoid(),
|
} = useRoute<LocationSetScreenRouteProp>();
|
||||||
[params.id],
|
const { navigate } = useNavigation<RootNavigationProp>();
|
||||||
)
|
|
||||||
const locations = useLocations();
|
const locations = useLocations();
|
||||||
const { navigate, goBack } = useNavigation();
|
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
const [lng, setLng] = useState('');
|
const setLocation = useSetLocation();
|
||||||
const [lat, setLat] = useState('');
|
|
||||||
const set = useSetLocation();
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
const current = locations[id];
|
const current = locations.find(l => l.id === id);
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setTitle(current.title);
|
setTitle(current.title);
|
||||||
setLng(current.location?.longitute.toString() || '');
|
|
||||||
setLat(current.location?.latitude.toString() || '');
|
|
||||||
},
|
},
|
||||||
[locations, id],
|
[id, locations],
|
||||||
)
|
)
|
||||||
|
|
||||||
const save = useCallback(
|
const [save] = useAsyncCallback(
|
||||||
() => {
|
async () => {
|
||||||
const lngParsed = parseFloat(lng);
|
await setLocation({
|
||||||
const latParsed = parseFloat(lat);
|
|
||||||
set({
|
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
location: {
|
position: { longitute: 0, latitude: 0 },
|
||||||
longitute: lngParsed,
|
|
||||||
latitude: latParsed,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
navigate('main');
|
navigate('main');
|
||||||
},
|
},
|
||||||
[title, lng, lat, id],
|
[id, title],
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup onClose={goBack}>
|
<Popup title="Edit location">
|
||||||
<TextInput value={title} onChangeText={setTitle} placeholder="Title" />
|
<TextInput
|
||||||
<TextInput value={lng} onChangeText={setLng} placeholder="Longitute" />
|
label="What should it call the location?"
|
||||||
<TextInput value={lat} onChangeText={setLat} placeholder="Latitude" />
|
value={title}
|
||||||
|
onChangeText={setTitle}
|
||||||
|
/>
|
||||||
|
<Row>
|
||||||
<Button title="Save" onPress={save} />
|
<Button title="Save" onPress={save} />
|
||||||
|
</Row>
|
||||||
</Popup>
|
</Popup>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export { LocationSetScreen };
|
export { LocationSetScreen };
|
||||||
|
|
||||||
|
|||||||
28
src/ui/screens/more/index.tsx
Normal file
28
src/ui/screens/more/index.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Page, Row } from "#/ui/components/base";
|
||||||
|
import { useNavigation } from "@react-navigation/native";
|
||||||
|
|
||||||
|
const MoreScreen: React.FC = () => {
|
||||||
|
const { navigate } = useNavigation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page>
|
||||||
|
<Row
|
||||||
|
title="Calendars"
|
||||||
|
/>
|
||||||
|
<Row
|
||||||
|
title="Locations"
|
||||||
|
onPress={() => navigate('locations')}
|
||||||
|
/>
|
||||||
|
<Row
|
||||||
|
title="Routines"
|
||||||
|
onPress={() => navigate('routines')}
|
||||||
|
/>
|
||||||
|
<Row
|
||||||
|
title="Goals"
|
||||||
|
onPress={() => navigate('goals')}
|
||||||
|
/>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MoreScreen };
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { useCurrentLocation } from "#/features/location"
|
|
||||||
import { usePlan } from "#/features/planner"
|
|
||||||
import { Button, Cell, Page, Row, TextInput } from "#/ui/components";
|
|
||||||
import { DayView } from "#/ui/components/specialized/plan/day";
|
|
||||||
import { Body1 } from "#/ui/typography";
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
|
||||||
import { useCommit, useDate } from "#/features/calendar";
|
|
||||||
import { format, formatDistance, formatDistanceToNow, set } from "date-fns";
|
|
||||||
import styled from "styled-components/native";
|
|
||||||
import { Status } from "#/features/planner/algorithm/build-graph";
|
|
||||||
import { useNavigation } from "@react-navigation/native";
|
|
||||||
|
|
||||||
const Wrapper = styled.ScrollView`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const getStats = (status: Status) => {
|
|
||||||
if (status.current === 'running') {
|
|
||||||
const runTime = formatDistanceToNow(status.start, { includeSeconds: true })
|
|
||||||
return `calulated ${status.nodes} nodes in ${runTime} using ${status.strategy}`;
|
|
||||||
}
|
|
||||||
const runTime = formatDistance(status.start, status.end, { includeSeconds: true })
|
|
||||||
return `calulated ${status.nodes} nodes in ${runTime} using ${status.strategy}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PlanDayScreen: React.FC = () => {
|
|
||||||
const date = useDate();
|
|
||||||
const [location] = useCurrentLocation();
|
|
||||||
const [startTime, setStartTime] = useState('06:00');
|
|
||||||
const [commit] = useCommit();
|
|
||||||
const { navigate } = useNavigation();
|
|
||||||
const current = useMemo(
|
|
||||||
() => location || {
|
|
||||||
id: 'unknown',
|
|
||||||
title: 'Unknown',
|
|
||||||
},
|
|
||||||
[location]
|
|
||||||
)
|
|
||||||
const [plan, options] = usePlan({
|
|
||||||
location: current,
|
|
||||||
})
|
|
||||||
const update = useCallback(
|
|
||||||
() => {
|
|
||||||
const target = new Date(`2000-01-01T${startTime}:00`)
|
|
||||||
const corrected = set(date, {
|
|
||||||
hours: target.getHours(),
|
|
||||||
minutes: target.getMinutes(),
|
|
||||||
})
|
|
||||||
plan(corrected);
|
|
||||||
},
|
|
||||||
[date, plan, startTime],
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Page>
|
|
||||||
<TextInput
|
|
||||||
overline="Start time"
|
|
||||||
value={startTime}
|
|
||||||
onChangeText={setStartTime}
|
|
||||||
right={(
|
|
||||||
<>
|
|
||||||
<Cell>
|
|
||||||
{!options.error && options.status && options.status.current === 'running' ? (
|
|
||||||
<Button type="destructive" onPress={options.status.cancel} icon="x" />
|
|
||||||
) : (
|
|
||||||
<Button icon="play" onPress={update} />
|
|
||||||
)}
|
|
||||||
</Cell>
|
|
||||||
{!!options.result?.agenda && (
|
|
||||||
<Cell>
|
|
||||||
<Button onPress={() => commit(options.result?.agenda || [])} icon="download" />
|
|
||||||
</Cell>
|
|
||||||
)}
|
|
||||||
<Cell>
|
|
||||||
<Button onPress={() => navigate('planSettings')} icon="settings" />
|
|
||||||
</Cell>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{!!options.error && (
|
|
||||||
<Row title={JSON.stringify(options.error)} />
|
|
||||||
)}
|
|
||||||
{options.status?.current === 'running' && (
|
|
||||||
<Row
|
|
||||||
title={getStats(options.status)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!!options.result && options.status?.current === 'completed' && (
|
|
||||||
<Row title={format(date, 'EEEE - do MMMM')} overline={getStats(options.status)}>
|
|
||||||
|
|
||||||
{options.result.impossible && options.result.impossible.length > 0 && <Body1>Impossible: {options.result.impossible.map(i => i.name).join(', ')}</Body1>}
|
|
||||||
<DayView plan={options.result.agenda} />
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
</Page>
|
|
||||||
</Wrapper>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { PlanDayScreen };
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import { useLocations, useSetLocation } from "#/features/location";
|
|
||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
|
||||||
import { Popup, Button, Checkbok, TextInput } from "#/ui/components";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useAgendaContext, useSetAgendaContext } from "#/features/agenda-context";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
const AgendaContextSetScreen: React.FC = () => {
|
|
||||||
const { params = {} } = useRoute() as any;
|
|
||||||
const id = useMemo(
|
|
||||||
() => params.id || nanoid(),
|
|
||||||
[params.id],
|
|
||||||
)
|
|
||||||
const contexts = useAgendaContext();
|
|
||||||
const { navigate, goBack } = useNavigation();
|
|
||||||
const locations = useLocations();
|
|
||||||
const [location, setLocation] = useState('');
|
|
||||||
const [enabled, setEnabled] = useState(true);
|
|
||||||
const [startMin, setStartMin] = useState('');
|
|
||||||
const [startMax, setStartMax] = useState('');
|
|
||||||
const [duration, setDuration] = useState('');
|
|
||||||
const [count, setCount] = useState('1');
|
|
||||||
const [set] = useSetAgendaContext();
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
const current = contexts[id];
|
|
||||||
if (!current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const name = current.locations?.map(l => l.title).join(',') || '';
|
|
||||||
if (current.startMin) {
|
|
||||||
setStartMin(format(current.startMin, 'HH:mm'));
|
|
||||||
}
|
|
||||||
if (current.startMax) {
|
|
||||||
setStartMax(format(current.startMax, 'HH:mm'));
|
|
||||||
}
|
|
||||||
if (current.duration) {
|
|
||||||
setDuration((current.duration / 1000 / 60).toString());
|
|
||||||
}
|
|
||||||
if (current.count) {
|
|
||||||
setCount(current.count.toString());
|
|
||||||
}
|
|
||||||
setLocation(name);
|
|
||||||
setEnabled(current.enabled);
|
|
||||||
},
|
|
||||||
[contexts, id],
|
|
||||||
)
|
|
||||||
|
|
||||||
const save = useCallback(
|
|
||||||
() => {
|
|
||||||
const name = location.split(',').map(a => Object.values(locations).find(i => i.title.toLowerCase() === a.trim().toLowerCase())).filter(Boolean);
|
|
||||||
set(id, {
|
|
||||||
enabled,
|
|
||||||
locations: name as any,
|
|
||||||
count: parseInt(count),
|
|
||||||
startMin: startMin ? new Date(`2020-01-01T${startMin}:00`) : undefined,
|
|
||||||
startMax: startMax ? new Date(`2020-01-01T${startMax}:00`) : undefined,
|
|
||||||
duration: duration ? parseInt(duration) * 1000 * 60 : undefined,
|
|
||||||
});
|
|
||||||
navigate('main');
|
|
||||||
},
|
|
||||||
[set, id, enabled, location, count, locations, startMin, startMax, duration],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popup onClose={goBack}>
|
|
||||||
<TextInput value={location} onChangeText={setLocation} placeholder="Locations" />
|
|
||||||
<TextInput value={startMin} onChangeText={setStartMin} placeholder="Start min" />
|
|
||||||
<TextInput value={startMax} onChangeText={setStartMax} placeholder="Start max" />
|
|
||||||
<TextInput value={duration} onChangeText={setDuration} placeholder="Duration" />
|
|
||||||
<TextInput value={count} onChangeText={setCount} placeholder="Count" />
|
|
||||||
<Checkbok label="Enabled" value={enabled} onChange={setEnabled} />
|
|
||||||
<Button title="Save" onPress={save} />
|
|
||||||
</Popup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { AgendaContextSetScreen };
|
|
||||||
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { Strategies, usePlanOptions, useSetPlanOptions } from "#/features/planner"
|
|
||||||
import { Selector } from "#/ui/components/form/selector";
|
|
||||||
import { Popup } from "#/ui/components";
|
|
||||||
import { useNavigation } from "@react-navigation/native";
|
|
||||||
|
|
||||||
const items = [{
|
|
||||||
display: 'First valid',
|
|
||||||
value: Strategies.firstValid,
|
|
||||||
}, {
|
|
||||||
display: 'First complete',
|
|
||||||
value: Strategies.firstComplet,
|
|
||||||
}, {
|
|
||||||
display: 'All valid',
|
|
||||||
value: Strategies.allValid,
|
|
||||||
}, {
|
|
||||||
display: 'All',
|
|
||||||
value: Strategies.all,
|
|
||||||
}];
|
|
||||||
|
|
||||||
const PlanSettingsScreen: React.FC = () => {
|
|
||||||
const options = usePlanOptions();
|
|
||||||
const setOptions = useSetPlanOptions();
|
|
||||||
const { goBack } = useNavigation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popup onClose={goBack}>
|
|
||||||
<Selector
|
|
||||||
label="Strategy"
|
|
||||||
items={items}
|
|
||||||
getId={i => i}
|
|
||||||
selected={options.strategy}
|
|
||||||
setSelected={(strategy) => {
|
|
||||||
setOptions({ strategy: strategy || Strategies.firstComplet });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Popup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { PlanSettingsScreen };
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import { useAgendaContext, useSetAgendaContext, useTasksWithContext } from "#/features/agenda-context";
|
|
||||||
import { set } from 'date-fns';
|
|
||||||
import chroma from 'chroma-js';
|
|
||||||
import stringToColor from 'string-to-color';
|
|
||||||
import { Button, Cell, Icon } from "#/ui/components";
|
|
||||||
import { Row } from "#/ui/components";
|
|
||||||
import { AgendaItemView } from "#/ui/components/specialized/plan/agenda-item";
|
|
||||||
import { Body1 } from "#/ui/typography";
|
|
||||||
import { useNavigation } from "@react-navigation/native";
|
|
||||||
import { useCallback, useMemo } from "react";
|
|
||||||
import { FlatList } from "react-native";
|
|
||||||
import CalendarStrip from 'react-native-calendar-strip';
|
|
||||||
import styled, { useTheme } from "styled-components/native";
|
|
||||||
import { useDate, useSetDate } from "#/features/calendar";
|
|
||||||
|
|
||||||
const Wrapper = styled.View`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Strip = () => {
|
|
||||||
const date = useDate();
|
|
||||||
const theme = useTheme();
|
|
||||||
const setDate = useSetDate();
|
|
||||||
const selected = useMemo(
|
|
||||||
() => [{
|
|
||||||
date,
|
|
||||||
lines: [{ color: theme.colors.icon }],
|
|
||||||
}],
|
|
||||||
[date],
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<CalendarStrip
|
|
||||||
markedDates={selected}
|
|
||||||
style={{
|
|
||||||
height: 150,
|
|
||||||
paddingTop: 60,
|
|
||||||
paddingBottom: 10,
|
|
||||||
backgroundColor: theme.colors.background,
|
|
||||||
}}
|
|
||||||
calendarColor={'#fff'}
|
|
||||||
selectedDate={date}
|
|
||||||
startingDate={date}
|
|
||||||
onDateSelected={(date) => {
|
|
||||||
setDate(set(date.toDate(), { hours: 0, minutes: 0, seconds: 0, milliseconds: 0 }));
|
|
||||||
}}
|
|
||||||
shouldAllowFontScaling={false}
|
|
||||||
iconContainer={{flex: 0.1}}
|
|
||||||
calendarHeaderStyle={{
|
|
||||||
color: theme.colors.text,
|
|
||||||
fontSize: theme.font.baseSize * 1.2,
|
|
||||||
}}
|
|
||||||
highlightDateNameStyle={{
|
|
||||||
color: theme.colors.icon,
|
|
||||||
fontSize: theme.font.baseSize * 0.6,
|
|
||||||
}}
|
|
||||||
iconLeftStyle={{
|
|
||||||
tintColor: theme.colors.text,
|
|
||||||
}}
|
|
||||||
iconRightStyle={{
|
|
||||||
tintColor: theme.colors.text,
|
|
||||||
}}
|
|
||||||
highlightDateNumberStyle={{
|
|
||||||
color: theme.colors.icon,
|
|
||||||
fontSize: theme.font.baseSize * 1.2,
|
|
||||||
}}
|
|
||||||
dateNumberStyle={{
|
|
||||||
color: theme.colors.text,
|
|
||||||
fontSize: theme.font.baseSize * 1.2,
|
|
||||||
}}
|
|
||||||
dateNameStyle={{
|
|
||||||
color: theme.colors.text,
|
|
||||||
fontSize: theme.font.baseSize * 0.6,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TaskListScreen: React.FC = () => {
|
|
||||||
const tasks = useTasksWithContext();
|
|
||||||
const { navigate } = useNavigation();
|
|
||||||
const contexts = useAgendaContext();
|
|
||||||
const [set] = useSetAgendaContext();
|
|
||||||
|
|
||||||
const toggle = useCallback(
|
|
||||||
(task: any) => {
|
|
||||||
const context = contexts[task.id] || {};
|
|
||||||
set(task.id, {
|
|
||||||
...context,
|
|
||||||
enabled: !task.enabled,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[set],
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<FlatList
|
|
||||||
ListHeaderComponent={Strip}
|
|
||||||
data={Object.values(tasks)}
|
|
||||||
keyExtractor={item => item.id}
|
|
||||||
renderItem={({ item }) => (
|
|
||||||
<Row
|
|
||||||
onPress={() => {
|
|
||||||
toggle(item);
|
|
||||||
//navigate('agendaContextSet', { id: item.id });
|
|
||||||
}}
|
|
||||||
opacity={item.enabled ? undefined : 0.3}
|
|
||||||
right={(
|
|
||||||
<Button
|
|
||||||
icon="edit"
|
|
||||||
onPress={() => {
|
|
||||||
navigate('agendaContextSet', { id: item.id });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<AgendaItemView
|
|
||||||
item={{
|
|
||||||
height: 1000 * 60 * 30,
|
|
||||||
body: <Body1>{item.name}</Body1>,
|
|
||||||
start: item.start.min,
|
|
||||||
color: chroma(stringToColor(item.name)).luminance(0.7).saturate(1).brighten(0.6).hex(),
|
|
||||||
end: new Date(item.start.max.getTime() + item.duration.min),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { TaskListScreen };
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { useRemoveRoutine, useRoutines } from "#/features/routines";
|
|
||||||
import { Button, Cell } from "#/ui/components";
|
|
||||||
import { Row } from "#/ui/components/row/row";
|
|
||||||
import { useNavigation } from "@react-navigation/native";
|
|
||||||
import { FlatList } from "react-native";
|
|
||||||
import styled from "styled-components/native";
|
|
||||||
|
|
||||||
const Wrapper = styled.View`
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
const RoutinesListScreen: React.FC = () => {
|
|
||||||
const routines = useRoutines();
|
|
||||||
const removeRoutine = useRemoveRoutine();
|
|
||||||
const { navigate } = useNavigation();
|
|
||||||
return (
|
|
||||||
<Wrapper>
|
|
||||||
<Button icon="plus-circle" onPress={() => navigate('routineSet')} />
|
|
||||||
<FlatList
|
|
||||||
data={Object.values(routines)}
|
|
||||||
keyExtractor={item => item.id}
|
|
||||||
renderItem={({ item }) => (
|
|
||||||
<Row
|
|
||||||
title={item.title}
|
|
||||||
subtitle={item.location?.map(l => l.title).join(', ')}
|
|
||||||
onPress={() => {
|
|
||||||
navigate('routineSet', { id: item.id });
|
|
||||||
}}
|
|
||||||
right={
|
|
||||||
<Cell>
|
|
||||||
<Button icon="trash" type="destructive" onPress={() => removeRoutine(item.id)} />
|
|
||||||
</Cell>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { RoutinesListScreen };
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
|
||||||
import { Popup, Button, Checkbok, TextInput } from "#/ui/components";
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { useRoutines, useSetRoutine } from '#/features/routines';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { useLocations } from '#/features/location';
|
|
||||||
|
|
||||||
const RoutineSetScreen: React.FC = () => {
|
|
||||||
const { params = {} } = useRoute() as any;
|
|
||||||
const id = useMemo(
|
|
||||||
() => params.id || nanoid(),
|
|
||||||
[params.id],
|
|
||||||
)
|
|
||||||
const routines = useRoutines();
|
|
||||||
const { navigate, goBack } = useNavigation();
|
|
||||||
const [title, setTitle] = useState('');
|
|
||||||
const [startMin, setStartMin] = useState('');
|
|
||||||
const [startMax, setStartMax] = useState('');
|
|
||||||
const [duration, setDuration] = useState('');
|
|
||||||
const locations = useLocations();
|
|
||||||
const [required, setRequired] = useState(false);
|
|
||||||
const [location, setLocation] = useState('');
|
|
||||||
const set = useSetRoutine();
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => {
|
|
||||||
const current = routines.find(r => r.id === id);
|
|
||||||
if (!current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTitle(current.title);
|
|
||||||
if (current.start.min) {
|
|
||||||
setStartMin(format(current.start.min, 'HH:mm'));
|
|
||||||
}
|
|
||||||
if (current.start.max) {
|
|
||||||
setStartMax(format(current.start.max, 'HH:mm'));
|
|
||||||
}
|
|
||||||
if (current.duration) {
|
|
||||||
setDuration((current.duration / 1000 / 60).toString());
|
|
||||||
}
|
|
||||||
setRequired(!!current.required);
|
|
||||||
const name = current.location?.map(l => l.title).join(',') || '';
|
|
||||||
setLocation(name);
|
|
||||||
},
|
|
||||||
[routines, id],
|
|
||||||
)
|
|
||||||
|
|
||||||
const save = useCallback(
|
|
||||||
() => {
|
|
||||||
const name = location.split(',').map(a => Object.values(locations).find(i => i.title.toLowerCase() === a.trim().toLowerCase())).filter(Boolean);
|
|
||||||
set({
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
priority: 50,
|
|
||||||
required: required,
|
|
||||||
location: name.length > 0 ? name as any : undefined,
|
|
||||||
start: {
|
|
||||||
min: new Date(`2020-01-01T${startMin}:00`),
|
|
||||||
max: new Date(`2020-01-01T${startMax}:00`),
|
|
||||||
},
|
|
||||||
duration: parseInt(duration) * 1000 * 60
|
|
||||||
});
|
|
||||||
navigate('main');
|
|
||||||
},
|
|
||||||
[title, startMin, startMax, duration, location, required],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popup onClose={goBack}>
|
|
||||||
<TextInput value={title} onChangeText={setTitle} placeholder="Title" />
|
|
||||||
<TextInput value={startMin} onChangeText={setStartMin} placeholder="Start min" />
|
|
||||||
<TextInput value={startMax} onChangeText={setStartMax} placeholder="Start max" />
|
|
||||||
<TextInput value={duration} onChangeText={setDuration} placeholder="Duration" />
|
|
||||||
<TextInput value={location} onChangeText={setLocation} placeholder="Location" />
|
|
||||||
<Checkbok label="Required" value={required} onChange={setRequired} />
|
|
||||||
<Button title="Save" onPress={save} />
|
|
||||||
</Popup>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export { RoutineSetScreen };
|
|
||||||
|
|
||||||
171
src/ui/screens/task/add.tsx
Normal file
171
src/ui/screens/task/add.tsx
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { useAsyncCallback } from "#/features/async";
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import { Task, TaskType, Time, 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, useRef, useState } from "react";
|
||||||
|
import styled from "styled-components/native";
|
||||||
|
|
||||||
|
const SideBySide = styled.View`
|
||||||
|
flex-direction: row;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const dayNames = [
|
||||||
|
'Monday',
|
||||||
|
'Tuesday',
|
||||||
|
'Wednsday',
|
||||||
|
'Thursday',
|
||||||
|
'Friday',
|
||||||
|
'Saturday',
|
||||||
|
'Sunday',
|
||||||
|
]
|
||||||
|
|
||||||
|
const days = new Array(7).fill(undefined).map((_, i) => ({
|
||||||
|
id: i,
|
||||||
|
name: dayNames[i],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const TaskAddScreen: React.FC = () => {
|
||||||
|
const { params: { type, id }} = useRoute<TaskAddScreenRouteProp>();
|
||||||
|
const { navigate, goBack } = useNavigation<RootNavigationProp>();
|
||||||
|
const [currentId, setCurrentId] = useState(id || nanoid());
|
||||||
|
const [setTask] = useSetTask();
|
||||||
|
const tasks = useTasks();
|
||||||
|
const [currentType, setCurrentType] = useState<TaskType>(type);
|
||||||
|
|
||||||
|
const locations = useLocations();
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
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 = tasks.find(t => t.id);
|
||||||
|
if (!current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTitle(current.title);
|
||||||
|
setMaxStart(current.startTime.max);
|
||||||
|
setMinStart(current.startTime.min);
|
||||||
|
setDuration(current.duration?.toString() || '');
|
||||||
|
setHasLocation(!!current.locations);
|
||||||
|
setSelectedLocations(current.locations || []);
|
||||||
|
setCurrentType(current.type || TaskType.goal);
|
||||||
|
if (current.type === TaskType.goal || current.type === TaskType.routine) {
|
||||||
|
setHasDays(!!current.days);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[id],
|
||||||
|
)
|
||||||
|
|
||||||
|
const [save] = useAsyncCallback(
|
||||||
|
async () => {
|
||||||
|
const task: Partial<Task> = {
|
||||||
|
id: currentId,
|
||||||
|
title,
|
||||||
|
type: currentType,
|
||||||
|
required: true,
|
||||||
|
startTime: {
|
||||||
|
max: maxStart!,
|
||||||
|
min: minStart!,
|
||||||
|
},
|
||||||
|
duration: parseInt(duration),
|
||||||
|
locations: hasLocation ? selectedLocations: undefined,
|
||||||
|
};
|
||||||
|
if (task.type === TaskType.goal || task.type === TaskType.routine) {
|
||||||
|
task.days = hasDays
|
||||||
|
? new Array(7).fill(undefined).map((_, i) => !!selectedDays.find(d => d.id === i))
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
await setTask(task as Task);
|
||||||
|
navigate('main');
|
||||||
|
},
|
||||||
|
[
|
||||||
|
title,
|
||||||
|
currentId,
|
||||||
|
maxStart,
|
||||||
|
minStart,
|
||||||
|
duration,
|
||||||
|
hasLocation,
|
||||||
|
selectedLocations,
|
||||||
|
hasDays,
|
||||||
|
selectedDays,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popup title={`Add ${type}`} onClose={goBack}>
|
||||||
|
<Group title="Basic">
|
||||||
|
<TextInput label="Title" value={title} onChangeText={setTitle} />
|
||||||
|
<SideBySide>
|
||||||
|
<TimeInput flex={1} label="Min start" value={minStart} onChange={setMinStart} />
|
||||||
|
<TimeInput flex={1} label="Max start" value={maxStart} onChange={setMaxStart} />
|
||||||
|
</SideBySide>
|
||||||
|
<TextInput
|
||||||
|
label="Duration"
|
||||||
|
value={duration}
|
||||||
|
onChangeText={setDuration}
|
||||||
|
right={<Cell><Overline>min</Overline></Cell>}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<Group title="Optional" startHidden>
|
||||||
|
<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>
|
||||||
|
{type === TaskType.goal && (
|
||||||
|
<SideBySide>
|
||||||
|
<TextInput label="Start" flex={1} />
|
||||||
|
<TextInput label="Deadline" flex={1} />
|
||||||
|
</SideBySide>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Row>
|
||||||
|
<Button onPress={save} title="Save" type="primary" />
|
||||||
|
</Row>
|
||||||
|
</Popup>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { TaskAddScreen };
|
||||||
@@ -2,8 +2,8 @@ import { Theme } from './theme';
|
|||||||
|
|
||||||
const light: Theme = {
|
const light: Theme = {
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#1abc9c',
|
primary: '#6c5ce7',
|
||||||
icon: '#1abc9c',
|
icon: '#6c5ce7',
|
||||||
destructive: '#e74c3c',
|
destructive: '#e74c3c',
|
||||||
shade: '#ededed',
|
shade: '#ededed',
|
||||||
input: '#ddd',
|
input: '#ddd',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {} from 'styled-components';
|
import {} from 'styled-components';
|
||||||
import Theme from './Theme'; // Import type from above file
|
import { Theme } from './theme'; // Import type from above file
|
||||||
declare module 'styled-components' {
|
declare module 'styled-components' {
|
||||||
export interface DefaultTheme extends Theme {} // extends the global DefaultTheme with our ThemeType.
|
export interface DefaultTheme extends Theme {} // extends the global DefaultTheme with our ThemeType.
|
||||||
}
|
}
|
||||||
68
src/utils/data-context.tsx
Normal file
68
src/utils/data-context.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { useAsync, useAsyncCallback } from "#/features/async";
|
||||||
|
import AsyncStorageLib from "@react-native-async-storage/async-storage";
|
||||||
|
import { createContext, ReactNode, useState } from "react"
|
||||||
|
|
||||||
|
type DataContextOptions<T> = {
|
||||||
|
createDefault: () => T;
|
||||||
|
deserialize?: (item: T) => T;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DataContextProviderProps = {
|
||||||
|
storageKey: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createDataContext<T extends {[name: string]: any}>({
|
||||||
|
createDefault,
|
||||||
|
deserialize = a => a,
|
||||||
|
}: DataContextOptions<T>) {
|
||||||
|
const Context = createContext<{
|
||||||
|
data: T;
|
||||||
|
setData: (data: T | ((current: T) => T)) => Promise<void>;
|
||||||
|
}>(undefined as any);
|
||||||
|
|
||||||
|
const Provider: React.FC<DataContextProviderProps> = ({
|
||||||
|
storageKey: key,
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [current, setCurrent] = useState<T>();
|
||||||
|
|
||||||
|
const [setData] = useAsyncCallback(
|
||||||
|
async (input: T | ((current: T) => T)) => {
|
||||||
|
let next = typeof input === 'function'
|
||||||
|
? input(current!)
|
||||||
|
: input;
|
||||||
|
const result = {
|
||||||
|
...current!,
|
||||||
|
...next,
|
||||||
|
};
|
||||||
|
setCurrent(result);
|
||||||
|
await AsyncStorageLib.setItem(key, JSON.stringify(result));
|
||||||
|
},
|
||||||
|
[key, current, setCurrent],
|
||||||
|
);
|
||||||
|
|
||||||
|
useAsync(
|
||||||
|
async () => {
|
||||||
|
const raw = await AsyncStorageLib.getItem(key);
|
||||||
|
const next = raw ? deserialize(JSON.parse(raw)) : createDefault();
|
||||||
|
setCurrent(next);
|
||||||
|
},
|
||||||
|
[key, setCurrent],
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!current) {
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ data: current, setData }}>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
return { Context, Provider };
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createDataContext };
|
||||||
17
webpack.config.js
Normal file
17
webpack.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const createExpoWebpackConfigAsync = require("@expo/webpack-config");
|
||||||
|
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
|
||||||
|
|
||||||
|
module.exports = async function (env, argv) {
|
||||||
|
const config = await createExpoWebpackConfigAsync(env, argv);
|
||||||
|
|
||||||
|
// Use the React refresh plugin in development mode
|
||||||
|
if (env.mode === "development") {
|
||||||
|
config.plugins.push(
|
||||||
|
new ReactRefreshWebpackPlugin({
|
||||||
|
forceEnable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user