diff --git a/.github/workflows/expo-pr.yml b/.github/workflows/expo-pr.yml new file mode 100644 index 0000000..e69c610 --- /dev/null +++ b/.github/workflows/expo-pr.yml @@ -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 + ![Expo QR](https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=exp://exp.host/${{ env.EXPO_PROJECT }}?release-channel=pr${{ github.event.number }}) + Published to https://exp.host/${{ env.EXPO_PROJECT }}?release-channel=pr${{ github.event.number }} diff --git a/babel.config.js b/babel.config.js index b8c0a62..d5e760e 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,5 +1,5 @@ module.exports = function(api) { - api.cache(true); + api.cache.using(() => process.env.NODE_ENV); return { presets: ['babel-preset-expo'], plugins: [ diff --git a/package.json b/package.json index bb6a497..a31e701 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,18 @@ "jest": { "preset": "jest-expo" }, + "resolutions": { + "@types/react": "~17.0.21", + "@types/react-dom": "~18.0.3", + "react-error-overlay": "6.0.9" + }, "dependencies": { "@expo/vector-icons": "^12.0.0", "@react-native-async-storage/async-storage": "~1.15.0", "@react-navigation/bottom-tabs": "^6.0.5", "@react-navigation/native": "^6.0.2", "@react-navigation/native-stack": "^6.1.0", + "@react-navigation/stack": "^6.2.1", "chroma-js": "^2.4.2", "date-fns": "^2.28.0", "expo": "~44.0.0", @@ -40,6 +46,8 @@ "react-dom": "17.0.1", "react-native": "0.64.3", "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-safe-area-context": "3.3.2", "react-native-screens": "~3.10.1", @@ -49,17 +57,20 @@ }, "devDependencies": { "@babel/core": "^7.12.9", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.5", "@types/chroma-js": "^2.1.3", "@types/react": "~17.0.21", "@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", "babel-plugin-module-resolver": "^4.1.0", "expo-cli": "^5.4.3", "jest": "^26.6.3", "jest-expo": "~44.0.1", + "react-refresh": "^0.13.0", "react-test-renderer": "17.0.1", - "typescript": "~4.3.5" + "typescript": "~4.3.5", + "webpack-hot-middleware": "^2.25.1" }, "private": true } diff --git a/src/app.tsx b/src/app.tsx index a65d015..be7aa74 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,19 +1,12 @@ import { StatusBar } from 'expo-status-bar'; import { SafeAreaProvider } from 'react-native-safe-area-context'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback } from 'react'; import { Setup } from './features/setup'; import { Router } from './ui/router'; import { ThemeProvider } from 'styled-components/native'; import { light } from './ui'; -import { set } from 'date-fns'; const App: React.FC = () => { - const [day, setDate] = useState(() => set(new Date, { - hours: 0, - minutes: 0, - seconds: 0, - milliseconds: 0, - })); const getTransit = useCallback( async (from: any, to: any) => ({ to, @@ -27,7 +20,7 @@ const App: React.FC = () => { - + diff --git a/src/features/agenda-context/context.ts b/src/features/agenda-context/context.ts deleted file mode 100644 index 5757956..0000000 --- a/src/features/agenda-context/context.ts +++ /dev/null @@ -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; -} - -const AgendaContextContext = createContext(undefined as any); - -export type { AgendaContext, AgendaContextContextValue }; -export {AgendaContextContext }; diff --git a/src/features/agenda-context/hooks.ts b/src/features/agenda-context/hooks.ts deleted file mode 100644 index c4883fa..0000000 --- a/src/features/agenda-context/hooks.ts +++ /dev/null @@ -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; -} diff --git a/src/features/agenda-context/index.ts b/src/features/agenda-context/index.ts deleted file mode 100644 index b4160a3..0000000 --- a/src/features/agenda-context/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { AgendaContextProvider } from './provider'; -export * from './hooks'; diff --git a/src/features/agenda-context/provider.tsx b/src/features/agenda-context/provider.tsx deleted file mode 100644 index 3b5caba..0000000 --- a/src/features/agenda-context/provider.tsx +++ /dev/null @@ -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 = ({ - children, - day, -}) => { - const [contexts, setContexts] = useState({}); - 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 ( - - {children} - - ); -}; - -export type { AgendaContextProviderProps }; -export { AgendaContextProvider }; diff --git a/src/features/appointments/context.ts b/src/features/appointments/context.ts new file mode 100644 index 0000000..bc95fa7 --- /dev/null +++ b/src/features/appointments/context.ts @@ -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; +} + +type AppointmentsContextApproved = { + status: AppointmentsStatus.approved; + getDay: (day: Day) => Promise +} + +type AppointmentsContextValue = AppointmentsContextUnavailable + | AppointmentsContextUnapprovedValue + | AppointmentsContextApproved; + +const AppointmentsContext = createContext(undefined as any); + +export type { + AppointmentsContextValue, +}; +export { AppointmentsContext, AppointmentsStatus }; diff --git a/src/features/appointments/hooks.ts b/src/features/appointments/hooks.ts new file mode 100644 index 0000000..19bb7eb --- /dev/null +++ b/src/features/appointments/hooks.ts @@ -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; +} diff --git a/src/features/appointments/index.ts b/src/features/appointments/index.ts new file mode 100644 index 0000000..bceae21 --- /dev/null +++ b/src/features/appointments/index.ts @@ -0,0 +1,2 @@ +export { AppointmentsProvider } from './provider'; +export * from './hooks'; diff --git a/src/features/appointments/provider.tsx b/src/features/appointments/provider.tsx new file mode 100644 index 0000000..c8188c2 --- /dev/null +++ b/src/features/appointments/provider.tsx @@ -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 = ({ + children, +}) => { + const [value] = useAsync( + async () => { + if (Platform.OS !== 'ios') { + return { status: AppointmentsStatus.unavailable }; + } + return { status: AppointmentsStatus.unavailable }; + }, + [], + ); + + if (!value) { + return <> + } + + return ( + + {children} + + ); +} + +export type { AppointmentsProviderProps }; +export { AppointmentsProvider }; diff --git a/src/hooks/async.ts b/src/features/async/hooks.ts similarity index 100% rename from src/hooks/async.ts rename to src/features/async/hooks.ts diff --git a/src/features/async/index.ts b/src/features/async/index.ts new file mode 100644 index 0000000..4cc90d0 --- /dev/null +++ b/src/features/async/index.ts @@ -0,0 +1 @@ +export * from './hooks'; diff --git a/src/features/calendar/context.ts b/src/features/calendar/context.ts deleted file mode 100644 index 41010f5..0000000 --- a/src/features/calendar/context.ts +++ /dev/null @@ -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(undefined as any); - -export type { CalendarContextValue }; -export { CalendarContext }; diff --git a/src/features/calendar/hooks.ts b/src/features/calendar/hooks.ts deleted file mode 100644 index 5dcefd3..0000000 --- a/src/features/calendar/hooks.ts +++ /dev/null @@ -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; -} diff --git a/src/features/calendar/index.ts b/src/features/calendar/index.ts deleted file mode 100644 index 4fc35c9..0000000 --- a/src/features/calendar/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { CalendarProvider } from './provider'; -export * from './hooks'; diff --git a/src/features/calendar/provider.tsx b/src/features/calendar/provider.tsx deleted file mode 100644 index 8450f70..0000000 --- a/src/features/calendar/provider.tsx +++ /dev/null @@ -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 = ({ - date, - children, - setDate, - calendarName = 'Bob the planner', -}) => { - const [selectedIds, setSelectedIds] = useState([]); - const [value] = useAsync( - 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 ( - - {children} - - ); - } - - return ( - - {children} - - ) -}; - -export type { CalendarProviderProps }; -export { CalendarProvider }; diff --git a/src/features/calendar/utils.ts b/src/features/calendar/utils.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/features/data/index.ts b/src/features/data/index.ts new file mode 100644 index 0000000..afaab1f --- /dev/null +++ b/src/features/data/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export { timeUtils } from './utils'; diff --git a/src/features/data/types.ts b/src/features/data/types.ts new file mode 100644 index 0000000..96d3e84 --- /dev/null +++ b/src/features/data/types.ts @@ -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; diff --git a/src/features/data/utils.ts b/src/features/data/utils.ts new file mode 100644 index 0000000..dcd7b84 --- /dev/null +++ b/src/features/data/utils.ts @@ -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 }; diff --git a/src/features/day/context.ts b/src/features/day/context.ts new file mode 100644 index 0000000..4e7ccd8 --- /dev/null +++ b/src/features/day/context.ts @@ -0,0 +1,12 @@ +import { createContext } from "react"; +import { Day } from "."; + +type DateContextValue = { + date: Day; + setDate: (date: Day) => void; +} + +const DateContext = createContext(undefined as any); + +export type { DateContextValue }; +export { DateContext } diff --git a/src/features/day/day.ts b/src/features/day/day.ts new file mode 100644 index 0000000..91fcad0 --- /dev/null +++ b/src/features/day/day.ts @@ -0,0 +1,7 @@ +type Day = { + year: number; + month: number; + date: number; +} + +export type { Day }; diff --git a/src/features/day/hooks.ts b/src/features/day/hooks.ts new file mode 100644 index 0000000..d2d1b2d --- /dev/null +++ b/src/features/day/hooks.ts @@ -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; +} diff --git a/src/features/day/index.ts b/src/features/day/index.ts new file mode 100644 index 0000000..00ab3a9 --- /dev/null +++ b/src/features/day/index.ts @@ -0,0 +1,4 @@ +export { DateProvider } from './provider'; +export type { Day } from './day'; +export * from './hooks'; +export { dayUtils } from './utils'; diff --git a/src/features/day/provider.tsx b/src/features/day/provider.tsx new file mode 100644 index 0000000..fe4b691 --- /dev/null +++ b/src/features/day/provider.tsx @@ -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 = ({ children }) => { + const [date, setDate] = useState(dayUtils.today()); + + return ( + + {children} + + ); +} + +export type { DateProviderProps }; +export { DateProvider }; diff --git a/src/features/day/utils.ts b/src/features/day/utils.ts new file mode 100644 index 0000000..35d016d --- /dev/null +++ b/src/features/day/utils.ts @@ -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 }; diff --git a/src/features/goals/context.ts b/src/features/goals/context.ts new file mode 100644 index 0000000..aa11944 --- /dev/null +++ b/src/features/goals/context.ts @@ -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 }; diff --git a/src/features/goals/hooks.ts b/src/features/goals/hooks.ts new file mode 100644 index 0000000..ba0b33c --- /dev/null +++ b/src/features/goals/hooks.ts @@ -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; +} diff --git a/src/features/location/context.ts b/src/features/location/context.ts index 1c9f0b5..281d289 100644 --- a/src/features/location/context.ts +++ b/src/features/location/context.ts @@ -1,6 +1,19 @@ -import { GetTransition, UserLocation } from "#/types/location"; +import { UserLocation } from "../data"; import { createContext } from "react" +type Transition = { + time: number; + usableTime: number; + to: UserLocation; + from: UserLocation; +}; + +type GetTransition = ( + from: UserLocation, + to: UserLocation, + time: Date, +) => Promise; + type LocationContextValue = { locations: { [id: string]: UserLocation; @@ -13,5 +26,5 @@ type LocationContextValue = { const LocationContext = createContext(undefined as any); -export type { LocationContextValue }; +export type { LocationContextValue, GetTransition, Transition }; export { LocationContext }; diff --git a/src/features/location/hooks.ts b/src/features/location/hooks.ts index 0ccbd8c..97624b9 100644 --- a/src/features/location/hooks.ts +++ b/src/features/location/hooks.ts @@ -1,13 +1,14 @@ -import { useAsync } from "#/hooks/async"; -import { useContext } from "react" +import { useAsync } from "#/features/async"; +import { useContext, useMemo } from "react" import { requestForegroundPermissionsAsync, getCurrentPositionAsync } from 'expo-location'; import { LocationContext } from "./context" -import { UserLocation } from "#/types/location"; +import { UserLocation } from "../data"; import { getDistanceFromLatLonInKm } from "./utils"; export const useLocations = () => { const { locations } = useContext(LocationContext); - return locations; + const result = useMemo(() => Object.values(locations), [locations]); + return result; } export const useSetLocation = () => { @@ -31,7 +32,7 @@ export const useLookup = () => { } export const useCurrentLocation = (proximity: number = 0.5) => { - const locations = useLocations(); + const { locations } = useContext(LocationContext); const result = useAsync( async () => { let { status } = await requestForegroundPermissionsAsync(); @@ -40,14 +41,14 @@ export const useCurrentLocation = (proximity: number = 0.5) => { } let position = await getCurrentPositionAsync({}); const withDistance = Object.values(locations).map((location) => { - if (!location.location) { + if (!location.position) { return; } const distance = getDistanceFromLatLonInKm( position.coords.latitude, position.coords.longitude, - location.location.latitude, - location.location.longitute, + location.position.latitude, + location.position.longitute, ) return { distance, @@ -59,7 +60,7 @@ export const useCurrentLocation = (proximity: number = 0.5) => { return { id: `${position.coords.longitude} ${position.coords.latitude}`, title: 'Unknown', - location: { + position: { latitude: position.coords.latitude, longitute: position.coords.longitude, }, diff --git a/src/features/location/index.ts b/src/features/location/index.ts index 09edba4..5f884ee 100644 --- a/src/features/location/index.ts +++ b/src/features/location/index.ts @@ -1,2 +1,3 @@ +export type { Transition, GetTransition } from './context'; export { LocationProvider } from './provider'; export * from './hooks'; diff --git a/src/features/location/provider.tsx b/src/features/location/provider.tsx index 057e7bb..d8384c4 100644 --- a/src/features/location/provider.tsx +++ b/src/features/location/provider.tsx @@ -1,8 +1,9 @@ -import { useAsync, useAsyncCallback } from "#/hooks/async"; -import { GetTransition, UserLocation } from "#/types/location"; +import { useAsync, useAsyncCallback } from "#/features/async"; +import { GetTransition } from "./context"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { ReactNode, useState } from "react"; import { LocationContext } from "./context"; +import { UserLocation } from "../data"; type LocationProviderProps = { children: ReactNode; @@ -10,7 +11,7 @@ type LocationProviderProps = { getTransition: GetTransition; } -const LOCATION_STORAGE_KEY = 'location_storage'; +const LOCATION_STORAGE_KEY = 'locations'; const LocationProvider: React.FC = ({ children, diff --git a/src/features/overrides/context.ts b/src/features/overrides/context.ts new file mode 100644 index 0000000..ceb04f0 --- /dev/null +++ b/src/features/overrides/context.ts @@ -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; + set: React.Dispatch>; +} +const OverrideContext = createContext(undefined as any); + +export type { Override, OverrideIndex, OverrideContextValue }; +export { OverrideContext }; diff --git a/src/features/overrides/hooks.ts b/src/features/overrides/hooks.ts new file mode 100644 index 0000000..c0a97e9 --- /dev/null +++ b/src/features/overrides/hooks.ts @@ -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; +}; diff --git a/src/features/overrides/index.ts b/src/features/overrides/index.ts new file mode 100644 index 0000000..3dd4aa5 --- /dev/null +++ b/src/features/overrides/index.ts @@ -0,0 +1,3 @@ +export type { Override, OverrideIndex } from './context'; +export { OverrideProvider } from './provider'; +export * from './hooks'; diff --git a/src/features/overrides/provider.tsx b/src/features/overrides/provider.tsx new file mode 100644 index 0000000..44db3d1 --- /dev/null +++ b/src/features/overrides/provider.tsx @@ -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 = ({ children }) => { + const currentDate = useDate(); + const [overrides, setOverrides] = useState(); + + const get = useCallback( + async (date: Day): Promise => { + const raw = await AsyncStorageLib.getItem(`${StorageKey}_${dayUtils.toId(date)}`); + if (!raw) { + return { tasks: {} }; + } + return JSON.parse(raw); + }, + [], + ); + + const set = useCallback( + async (override: SetStateAction) => { + 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 ( + + {children} + + ); +} + +export type { OverrideProviderProps }; +export { OverrideProvider }; diff --git a/src/features/planner/context.ts b/src/features/planner/context.ts index d035e42..3f2b31e 100644 --- a/src/features/planner/context.ts +++ b/src/features/planner/context.ts @@ -1,15 +1,18 @@ -import { createContext } from 'react'; +import { createDataContext } from '#/utils/data-context'; import { Strategies } from "./algorithm/build-graph"; type PlannerOptions = { strategy: Strategies; } -type PlannerContextValue = { - options: PlannerOptions; - setOptions: (options: Partial) => void; -} -const PlannerContext = createContext(undefined as any); +const { + Context: PlannerContext, + Provider: PlannerProvider, +} = createDataContext({ + createDefault: () => ({ + strategy: Strategies.firstComplet, + }), +}); -export type { PlannerContextValue, PlannerOptions }; -export { PlannerContext }; +export type { PlannerOptions }; +export { PlannerContext, PlannerProvider }; diff --git a/src/features/planner/hooks.ts b/src/features/planner/hooks.ts index 2f978da..83c5511 100644 --- a/src/features/planner/hooks.ts +++ b/src/features/planner/hooks.ts @@ -1,14 +1,8 @@ -import { useGetTransition } from "#/features/location"; import { buildGraph, Status, Strategies } from "./algorithm/build-graph"; -import { constructDay } from "./algorithm/construct-day"; -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 { useContext } from "react"; import { PlanItem } from "#/types/plans"; -import { Task } from "#/types/task"; import { PlannerContext } from "./context"; +import { Task, UserLocation } from "../data"; export type UsePlanOptions = { location: UserLocation; @@ -25,53 +19,16 @@ export type UsePlan = [ ] export const usePlanOptions = () => { - const { options } = useContext(PlannerContext); - return options; + const { data } = useContext(PlannerContext); + return data; } export const useSetPlanOptions = () => { - const { setOptions } = useContext(PlannerContext); - return setOptions; + const { setData } = useContext(PlannerContext); + return setData; } -export const usePlan = ({ - location, -}: UsePlanOptions): UsePlan => { - const today = useDate(); - const planOptions = usePlanOptions(); - const [status, setStatus] = useState(); - 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, - } - ]; +export const usePlan = () => { + } + diff --git a/src/features/planner/index.ts b/src/features/planner/index.ts index 3507be0..18aa4fb 100644 --- a/src/features/planner/index.ts +++ b/src/features/planner/index.ts @@ -1,4 +1,3 @@ -export { PlannerProvider } from './provider'; -export type { PlannerOptions } from './context'; +export { PlannerProvider, PlannerOptions } from './context'; export { Strategies } from './algorithm/build-graph'; export * from './hooks'; diff --git a/src/features/planner/provider.tsx b/src/features/planner/provider.tsx deleted file mode 100644 index 4e5efda..0000000 --- a/src/features/planner/provider.tsx +++ /dev/null @@ -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 = ({ children }) => { - const [options, setOwnOptions] = useState({ - strategy: Strategies.firstComplet, - }) - - const setOptions = useCallback( - (next: Partial) => { - setOwnOptions(current => ({ - ...current, - ...next, - })) - }, - [setOwnOptions], - ); - - return ( - - {children} - - ); -} - -export type { PlannerProviderProps }; -export { PlannerProvider }; diff --git a/src/types/graph.ts b/src/features/planner/types.ts similarity index 59% rename from src/types/graph.ts rename to src/features/planner/types.ts index 696a6a4..5e35872 100644 --- a/src/types/graph.ts +++ b/src/features/planner/types.ts @@ -1,10 +1,24 @@ -import { GetTransition, Transition, UserLocation } from "./location"; -import { Task } from "./task"; type Context = { 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 = { location: UserLocation; diff --git a/src/features/routines/context.ts b/src/features/routines/context.ts index c7c6544..5ed82fc 100644 --- a/src/features/routines/context.ts +++ b/src/features/routines/context.ts @@ -1,26 +1,11 @@ -import { UserLocation } from "#/types/location"; -import { createContext } from "react" +import { createDataContext } from "#/utils/data-context"; +import { Routine } from "../data"; -export type Routine = { - id: string; - title: string; - required: boolean; - priority: number; - start: { - min: Date; - max: Date; - }; - duration: number; - location?: UserLocation[]; - days?: boolean[]; -} +const { + Context: RoutinesContext, + Provider: RoutinesProvider, +}= createDataContext<{[id: string]: Routine}>({ + createDefault: () => ({}), +}) -export type RoutinesContextValue = { - routines: Routine[]; - remove: (id: string) => any; - set: (routine: Routine) => any; -} - -const RoutinesContext = createContext(undefined as any); - -export { RoutinesContext }; +export { RoutinesContext, RoutinesProvider }; diff --git a/src/features/routines/hooks.ts b/src/features/routines/hooks.ts index 365eb07..dda2251 100644 --- a/src/features/routines/hooks.ts +++ b/src/features/routines/hooks.ts @@ -1,35 +1,40 @@ import { useCallback, useContext, useMemo } from "react" -import { Routine, RoutinesContext } from "./context" +import { Routine } from "../data"; +import { RoutinesContext } from "./context" -export const useRoutines = (day?: number) => { - const { routines } = useContext(RoutinesContext); +export const useRoutines = () => { + const { data } = useContext(RoutinesContext); const current = useMemo( - () => routines.filter( - r => typeof day === undefined - || !r.days - || r.days[day!], - ), - [routines], - ); - + () => Object.values(data), + [data], + ) return current; }; export const useSetRoutine = () => { - const { set } = useContext(RoutinesContext); - const setRoutine = useCallback( - (routine: Routine) => set(routine), - [set], + const { setData } = useContext(RoutinesContext); + const set = useCallback( + (routine: Routine) => setData(current => ({ + ...current, + [routine.id]: routine, + })), + [setData], ); - return setRoutine; + return set; } export const useRemoveRoutine = () => { - const { remove } = useContext(RoutinesContext); + const { setData } = useContext(RoutinesContext); const removeRoutine = useCallback( - (id: string) => remove(id), - [remove], + (id: string) => { + setData(current => { + const next = {...current}; + delete next[id]; + return next; + }) + }, + [setData], ); return removeRoutine; diff --git a/src/features/routines/index.ts b/src/features/routines/index.ts index 60dc3b8..180886f 100644 --- a/src/features/routines/index.ts +++ b/src/features/routines/index.ts @@ -1,3 +1,2 @@ -export { RoutinesProvider } from './provider'; -export { Routine } from './context'; +export { RoutinesProvider } from './context'; export * from './hooks'; diff --git a/src/features/routines/provider.tsx b/src/features/routines/provider.tsx deleted file mode 100644 index 4f73869..0000000 --- a/src/features/routines/provider.tsx +++ /dev/null @@ -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 = ({ 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 ( - - {children} - - ) -} - -export type { RoutinesProviderProps }; -export { RoutinesProvider }; diff --git a/src/features/setup.tsx b/src/features/setup.tsx index b8bd948..056b6af 100644 --- a/src/features/setup.tsx +++ b/src/features/setup.tsx @@ -1,35 +1,39 @@ -import { GetTransition } from "#/types/location" import { ReactNode } from "react" -import { AgendaContextProvider } from "./agenda-context" -import { CalendarProvider } from "./calendar" -import { LocationProvider } from "./location" +import { AppointmentsProvider } from "./appointments" +import { DateProvider } from "./day" +import { GoalsProvider } from "./goals/context" +import { GetTransition, LocationProvider } from "./location" +import { OverrideProvider } from "./overrides" import { PlannerProvider } from "./planner" import { RoutinesProvider } from "./routines" type SetupProps = { - day: Date; - setDate: (date: Date) => void; children: ReactNode; getTransit: GetTransition; } + const Setup: React.FC = ({ children, - day, - setDate, getTransit, -}) => ( - - - []}> - - - {children} - - - - - -); +}) => { + return ( + + + []}> + + + + + {children} + + + + + + + + ); +}; export type { SetupProps }; export { Setup }; diff --git a/src/features/tasks/hooks.ts b/src/features/tasks/hooks.ts deleted file mode 100644 index 5a01f85..0000000 --- a/src/features/tasks/hooks.ts +++ /dev/null @@ -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( - () => 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( - () => 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; -} diff --git a/src/features/tasks/hooks.tsx b/src/features/tasks/hooks.tsx new file mode 100644 index 0000000..5d9b30e --- /dev/null +++ b/src/features/tasks/hooks.tsx @@ -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( + () => { + 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; +}; diff --git a/src/types/location.ts b/src/types/location.ts deleted file mode 100644 index d82e1e2..0000000 --- a/src/types/location.ts +++ /dev/null @@ -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; diff --git a/src/types/plans.ts b/src/types/plans.ts deleted file mode 100644 index f548650..0000000 --- a/src/types/plans.ts +++ /dev/null @@ -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; diff --git a/src/types/task.ts b/src/types/task.ts deleted file mode 100644 index e7e0472..0000000 --- a/src/types/task.ts +++ /dev/null @@ -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; - }; -} diff --git a/src/ui/components/button/index.tsx b/src/ui/components/base/button/index.tsx similarity index 100% rename from src/ui/components/button/index.tsx rename to src/ui/components/base/button/index.tsx diff --git a/src/ui/components/base/group/header.tsx b/src/ui/components/base/group/header.tsx new file mode 100644 index 0000000..2df81d6 --- /dev/null +++ b/src/ui/components/base/group/header.tsx @@ -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 ( + + + + ) + } + /> + ); +} + +export { Header }; diff --git a/src/ui/components/base/group/index.tsx b/src/ui/components/base/group/index.tsx new file mode 100644 index 0000000..a01ec55 --- /dev/null +++ b/src/ui/components/base/group/index.tsx @@ -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 { + 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(props: ListProps | ChildProps) { + const [visible, setVisible] = useState(!props.startHidden); + const { title, items, getKey, render, add, children } = + props as ListProps & ChildProps; + return ( + + + <> +
+ } + title={title} + add={add} + onPress={() => setVisible(!visible)} + /> + + {items && items.map((item, i) => ( + {render(item)} + ))} + {children} + {!children && (!items || items.length === 0) && ( + + + + } + > + + Empty + + + )} + + + + + ); +} + +export { Group }; diff --git a/src/ui/components/icon/index.tsx b/src/ui/components/base/icon/index.tsx similarity index 100% rename from src/ui/components/icon/index.tsx rename to src/ui/components/base/icon/index.tsx diff --git a/src/ui/components/index.ts b/src/ui/components/base/index.ts similarity index 67% rename from src/ui/components/index.ts rename to src/ui/components/base/index.ts index c1ecf15..be016f0 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/base/index.ts @@ -1,9 +1,7 @@ export * from './icon'; export * from './modal'; -export * from './icon'; -export * from './form'; export * from './page'; export * from './popup'; export * from './row'; -export * from './form'; export * from './button'; +export * from './group'; diff --git a/src/ui/components/modal/index.tsx b/src/ui/components/base/modal/index.tsx similarity index 100% rename from src/ui/components/modal/index.tsx rename to src/ui/components/base/modal/index.tsx diff --git a/src/ui/components/modal/react-modal.native.tsx b/src/ui/components/base/modal/react-modal.native.tsx similarity index 100% rename from src/ui/components/modal/react-modal.native.tsx rename to src/ui/components/base/modal/react-modal.native.tsx diff --git a/src/ui/components/modal/react-modal.tsx b/src/ui/components/base/modal/react-modal.tsx similarity index 100% rename from src/ui/components/modal/react-modal.tsx rename to src/ui/components/base/modal/react-modal.tsx diff --git a/src/ui/components/page/index.tsx b/src/ui/components/base/page/index.tsx similarity index 100% rename from src/ui/components/page/index.tsx rename to src/ui/components/base/page/index.tsx diff --git a/src/ui/components/popup/index.tsx b/src/ui/components/base/popup/index.tsx similarity index 64% rename from src/ui/components/popup/index.tsx rename to src/ui/components/base/popup/index.tsx index 72f3159..829cc1c 100644 --- a/src/ui/components/popup/index.tsx +++ b/src/ui/components/base/popup/index.tsx @@ -2,10 +2,10 @@ import React, { ReactNode } from 'react'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import styled from 'styled-components/native'; import { Icon } from '../icon'; -import { Row, Cell } from '../row'; +import { Row, Cell, RowProps } from '../row'; import { Page } from '../page'; -interface Props { +type Props = RowProps & { onClose?: () => void; children: ReactNode; } @@ -17,19 +17,25 @@ const Top = styled.Pressable` const Wrapper = styled.View` background: ${({ theme }) => theme.colors.background}; width: 100%; + max-width: 500px; shadow-color: ${({ theme }) => theme.colors.shadow}; shadow-offset: 0 0; shadow-opacity: 1; shadow-radius: 200px; border-radius: 12px; margin-bottom: -12px; + max-height: 80%; `; const Outer = styled.View` flex: 1; + align-items: center; `; -const Popup: React.FC = ({ visible, children, onClose }) => { +const Content = styled.ScrollView` +`; + +const Popup: React.FC = ({ children, onClose, right, ...rowProps }) => { const insets = useSafeAreaInsets(); return ( @@ -38,13 +44,19 @@ const Popup: React.FC = ({ visible, children, onClose }) => { - - + <> + {right} + + + + } /> - {children} + + {children} + diff --git a/src/ui/components/row/cell.tsx b/src/ui/components/base/row/cell.tsx similarity index 93% rename from src/ui/components/row/cell.tsx rename to src/ui/components/base/row/cell.tsx index 0406a01..a4fa634 100644 --- a/src/ui/components/row/cell.tsx +++ b/src/ui/components/base/row/cell.tsx @@ -3,7 +3,7 @@ import { TouchableOpacity } from 'react-native'; import styled from 'styled-components/native'; import { Theme } from '#/ui/theme'; -interface Props { +type CellProps = { accessibilityRole?: TouchableOpacity['props']['accessibilityRole']; accessibilityLabel?: string; accessibilityHint?: string; @@ -34,7 +34,7 @@ const Wrapper = styled.View<{ const Touch = styled.TouchableOpacity``; -const Cell: React.FC = ({ children, onPress, ...props}) => { +const Cell: React.FC = ({ children, onPress, ...props}) => { const { accessibilityLabel, accessibilityRole, @@ -62,4 +62,5 @@ const Cell: React.FC = ({ children, onPress, ...props}) => { return node; }; +export type { CellProps }; export { Cell }; diff --git a/src/ui/components/row/index.ts b/src/ui/components/base/row/index.ts similarity index 100% rename from src/ui/components/row/index.ts rename to src/ui/components/base/row/index.ts diff --git a/src/ui/components/row/placeholder-icon.tsx b/src/ui/components/base/row/placeholder-icon.tsx similarity index 100% rename from src/ui/components/row/placeholder-icon.tsx rename to src/ui/components/base/row/placeholder-icon.tsx diff --git a/src/ui/components/row/row.tsx b/src/ui/components/base/row/row.tsx similarity index 87% rename from src/ui/components/row/row.tsx rename to src/ui/components/base/row/row.tsx index 61035fe..f569a3e 100644 --- a/src/ui/components/row/row.tsx +++ b/src/ui/components/base/row/row.tsx @@ -1,9 +1,9 @@ import React, { ReactNode } from 'react'; import styled from 'styled-components/native'; import { Title1, Body1, Overline } from '#/ui/typography'; -import { Cell } from './cell'; +import { Cell, CellProps } from './cell'; -type RowProps = { +type RowProps = CellProps & { background?: string; top?: ReactNode; left?: ReactNode; @@ -42,8 +42,9 @@ const Row: React.FC = ({ description, children, onPress, + ...cellProps }) => ( - + {left} {!!top} diff --git a/src/ui/components/date/bar/index.tsx b/src/ui/components/date/bar/index.tsx new file mode 100644 index 0000000..4e13ea8 --- /dev/null +++ b/src/ui/components/date/bar/index.tsx @@ -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 ( + { + 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 }; diff --git a/src/ui/components/date/index.ts b/src/ui/components/date/index.ts new file mode 100644 index 0000000..1ab9701 --- /dev/null +++ b/src/ui/components/date/index.ts @@ -0,0 +1 @@ +export * from './bar'; diff --git a/src/ui/components/form/checkbox/index.tsx b/src/ui/components/form/checkbox/index.tsx index b981342..daf52bc 100644 --- a/src/ui/components/form/checkbox/index.tsx +++ b/src/ui/components/form/checkbox/index.tsx @@ -1,6 +1,6 @@ -import { Row } from "../../row" +import { Row, RowProps } from "#/ui/components/base" -type CheckboxProps = { +type CheckboxProps = RowProps & { value?: boolean; label: string; onChange: (value: boolean) => void; @@ -10,8 +10,10 @@ const Checkbok: React.FC = ({ value, label, onChange, + ...rowProps }) => ( onChange(!value)} diff --git a/src/ui/components/form/index.ts b/src/ui/components/form/index.ts index a27526b..17085ec 100644 --- a/src/ui/components/form/index.ts +++ b/src/ui/components/form/index.ts @@ -1,2 +1,4 @@ export * from './input'; export * from './checkbox'; +export * from './time'; +export * from './optional-selector'; diff --git a/src/ui/components/form/input/index.tsx b/src/ui/components/form/input/index.tsx index 3c70145..2c01629 100644 --- a/src/ui/components/form/input/index.tsx +++ b/src/ui/components/form/input/index.tsx @@ -1,8 +1,9 @@ import React from 'react'; -import styled, { useTheme } from 'styled-components/native'; -import { Row, RowProps } from '../../row'; +import styled from 'styled-components/native'; +import { Row, RowProps } from '#/ui/components/base'; type Props = RowProps & { + label: string; placeholder?: string; value: string; onChangeText: (text: string) => any; @@ -17,11 +18,11 @@ const InputField = styled.TextInput` width: 100%; `; -const TextInput: React.FC = ({ placeholder, value, onChangeText, children, ...row }) => { - const theme = useTheme(); +const TextInput: React.FC = ({ label, placeholder, value, onChangeText, children, ...row }) => { return ( - + diff --git a/src/ui/components/form/optional-selector/index.tsx b/src/ui/components/form/optional-selector/index.tsx new file mode 100644 index 0000000..05025a3 --- /dev/null +++ b/src/ui/components/form/optional-selector/index.tsx @@ -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 = { + 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({ + label, + enabled, + setEnabled, + onChange, + items, + enabledText, + disabledText, + selected, + render, + getKey, +}: Props) { + 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 ( + + + + setEnabled(false)}> + + {disabledText} + + + setEnabled(true)}> + + {enabledText} + + + + {enabled && ( + + {items.map((item) => { + const { left, ...props } = render(item); + const isSelected = !!selected && selected.includes(item); + return ( + + toggle(item)}> + + + {left} + + )} + /> + ); + })} + + )} + + + ) +} + +export { OptionalSelector } diff --git a/src/ui/components/form/time/index.tsx b/src/ui/components/form/time/index.tsx new file mode 100644 index 0000000..7d3502b --- /dev/null +++ b/src/ui/components/form/time/index.tsx @@ -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 = ({ 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 ( + + + {children} + + ); +}; + +export { TimeInput }; diff --git a/src/ui/components/specialized/plan/agenda-item.tsx b/src/ui/components/specialized/plan/agenda-item.tsx deleted file mode 100644 index 7137b71..0000000 --- a/src/ui/components/specialized/plan/agenda-item.tsx +++ /dev/null @@ -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 = ({ item, onPress }) => { - const view = ( - - - - - -
- {item.body} -
- -
- ); - - if (onPress) { - return ( - - {view} - - ); - } - return view; -}; - -export type { AgendaItemProps }; -export { AgendaItemView }; diff --git a/src/ui/components/specialized/plan/day.tsx b/src/ui/components/specialized/plan/day.tsx deleted file mode 100644 index da8e81a..0000000 --- a/src/ui/components/specialized/plan/day.tsx +++ /dev/null @@ -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 {item.from.title} ➜ {item.to.title} - } else { - return {item.name} - } -} - - -const DayView: React.FC = ({ 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 ( - - {layout.map((item, i) => ( - - ))} - - ) -}; - -export type { DayViewProps }; -export { DayView }; diff --git a/src/ui/components/tasks/group/index.tsx b/src/ui/components/tasks/group/index.tsx new file mode 100644 index 0000000..76672a9 --- /dev/null +++ b/src/ui/components/tasks/group/index.tsx @@ -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 = ({ type }) => { + const { navigate } = useNavigation(); + const tasks = useTasks(type); + + const add = useCallback( + (type: TaskType) => { + navigate('add-task', { + type, + }) + }, + [navigate], + ); + + return ( + add(type)} + items={tasks || []} + getKey={(task) => task.id} + render={(task) => ( + { + navigate('add-task', { id: task.id }); + }} + /> + )} + /> + ); +}; + +export { TaskGroup }; diff --git a/src/ui/components/tasks/index.ts b/src/ui/components/tasks/index.ts new file mode 100644 index 0000000..273db2c --- /dev/null +++ b/src/ui/components/tasks/index.ts @@ -0,0 +1 @@ +export * from './list-item'; diff --git a/src/ui/components/tasks/list-item/index.tsx b/src/ui/components/tasks/list-item/index.tsx new file mode 100644 index 0000000..858b61b --- /dev/null +++ b/src/ui/components/tasks/list-item/index.tsx @@ -0,0 +1,17 @@ +import { Task } from "#/features/data"; +import { Row, RowProps } from "../../base"; + +type Props = RowProps & { + item: Task; +} + +const TaskListItem: React.FC = ({ item, ...rowProps }) => { + return ( + + ); +}; + +export { TaskListItem }; diff --git a/src/ui/helpers/react.tsx b/src/ui/helpers/react.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/ui/index.ts b/src/ui/index.ts index 7f64104..f5cf858 100644 --- a/src/ui/index.ts +++ b/src/ui/index.ts @@ -1,2 +1,2 @@ -export * from './components'; +export * from './components/base'; export * from './theme'; diff --git a/src/ui/router/index.tsx b/src/ui/router/index.tsx index 006f4a5..b5430f8 100644 --- a/src/ui/router/index.tsx +++ b/src/ui/router/index.tsx @@ -1,113 +1,2 @@ -import { useMemo } from 'react'; -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -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 ( - - , - }} - name="tasks" - component={TaskListScreen} - /> - , - }} - /> - , - }} - /> - , - }} - /> - , - }} - /> - - ); -}; - -const RootNavigator = createNativeStackNavigator(); - -const Root: React.FC = () => ( - - - - - - - - - - - -); - -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 ( - - - - ) -}; - -export { Router }; +export { Router } from './router'; +export * from './types'; diff --git a/src/ui/router/router.tsx b/src/ui/router/router.tsx new file mode 100644 index 0000000..7d26496 --- /dev/null +++ b/src/ui/router/router.tsx @@ -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 = () => ( + + + + +); + +const MainTabsNvaigator = createBottomTabNavigator(); + +const MainTabs: React.FC = () => { + const theme = useTheme(); + return ( + + , + }} + name="day" + component={DayScreen} + /> + , + }} + name="more" + component={MoreStack} + /> + + ); +}; + +const RootNavigator = Platform.OS === 'web' + ? createStackNavigator() + : createNativeStackNavigator(); + +const Root: React.FC = () => ( + + + + + + + + + +); + +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 ( + + + + ) +}; + +export { Router }; diff --git a/src/ui/router/types.ts b/src/ui/router/types.ts new file mode 100644 index 0000000..b268753 --- /dev/null +++ b/src/ui/router/types.ts @@ -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; +export type RootNavigationProp = NativeStackNavigationProp; + +export type LocationSetScreenRouteProp = RouteProp; + +export type TaskAddScreenRouteProp = RouteProp; +export type TaskAddScreenNavigationProp = NativeStackNavigationProp< + RootStackParamList, + 'add-task' +>; + +export type MainTabParamList = { + day: NavigatorScreenParams; + more: NavigatorScreenParams; +} + +export type DayScreenRouteProp = RouteProp; diff --git a/src/ui/screens/calendars/select.tsx b/src/ui/screens/calendars/select.tsx deleted file mode 100644 index d78924e..0000000 --- a/src/ui/screens/calendars/select.tsx +++ /dev/null @@ -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 ( - - {calendars.map((calendar) => ( -