mirror of
https://github.com/morten-olsen/bob-the-algorithm.git
synced 2026-02-08 00:46:25 +01:00
init
This commit is contained in:
47
src/ui/screens/calendars/select.tsx
Normal file
47
src/ui/screens/calendars/select.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
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 };
|
||||
44
src/ui/screens/locations/list.tsx
Normal file
44
src/ui/screens/locations/list.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { useLocations, useRemoveLocation } from "#/features/location"
|
||||
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 Name = styled.Text`
|
||||
|
||||
`;
|
||||
|
||||
const LocationListScreen: React.FC = () => {
|
||||
const locations = useLocations();
|
||||
const removeLocation = useRemoveLocation();
|
||||
const { navigate } = useNavigation();
|
||||
return (
|
||||
<Wrapper>
|
||||
<Button icon="plus-circle" onPress={() => navigate('locationSet')} />
|
||||
<FlatList
|
||||
data={Object.values(locations)}
|
||||
keyExtractor={item => item.id}
|
||||
renderItem={({ item }) => (
|
||||
<Row
|
||||
title={item.title}
|
||||
onPress={() => {
|
||||
navigate('locationSet', { id: item.id });
|
||||
}}
|
||||
right={
|
||||
<Cell>
|
||||
<Button type="destructive" icon="trash" onPress={() => removeLocation(item.id)} />
|
||||
</Cell>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export { LocationListScreen };
|
||||
62
src/ui/screens/locations/set.tsx
Normal file
62
src/ui/screens/locations/set.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useLocations, useSetLocation } from "#/features/location";
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { Button, TextInput } from "#/ui/components";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { nanoid } from 'nanoid';
|
||||
import Popup from "#/ui/components/popup";
|
||||
|
||||
const LocationSetScreen: React.FC = () => {
|
||||
const { params = {} } = useRoute() as any;
|
||||
const id = useMemo(
|
||||
() => params.id || nanoid(),
|
||||
[params.id],
|
||||
)
|
||||
const locations = useLocations();
|
||||
const { navigate, goBack } = useNavigation();
|
||||
const [title, setTitle] = useState('');
|
||||
const [lng, setLng] = useState('');
|
||||
const [lat, setLat] = useState('');
|
||||
const set = useSetLocation();
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const current = locations[id];
|
||||
if (!current) {
|
||||
return;
|
||||
}
|
||||
setTitle(current.title);
|
||||
setLng(current.location?.longitute.toString() || '');
|
||||
setLat(current.location?.latitude.toString() || '');
|
||||
},
|
||||
[locations, id],
|
||||
)
|
||||
|
||||
const save = useCallback(
|
||||
() => {
|
||||
const lngParsed = parseFloat(lng);
|
||||
const latParsed = parseFloat(lat);
|
||||
set({
|
||||
id,
|
||||
title,
|
||||
location: {
|
||||
longitute: lngParsed,
|
||||
latitude: latParsed,
|
||||
},
|
||||
});
|
||||
navigate('main');
|
||||
},
|
||||
[title, lng, lat, id],
|
||||
)
|
||||
|
||||
return (
|
||||
<Popup onClose={goBack}>
|
||||
<TextInput value={title} onChangeText={setTitle} placeholder="Title" />
|
||||
<TextInput value={lng} onChangeText={setLng} placeholder="Longitute" />
|
||||
<TextInput value={lat} onChangeText={setLat} placeholder="Latitude" />
|
||||
<Button title="Save" onPress={save} />
|
||||
</Popup>
|
||||
)
|
||||
}
|
||||
|
||||
export { LocationSetScreen };
|
||||
|
||||
97
src/ui/screens/plan/day.tsx
Normal file
97
src/ui/screens/plan/day.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
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";
|
||||
|
||||
const Wrapper = styled.ScrollView`
|
||||
|
||||
`;
|
||||
|
||||
const getStats = (status: Status) => {
|
||||
console.log('status', status);
|
||||
if (status.current === 'running') {
|
||||
const runTime = formatDistanceToNow(status.start, { includeSeconds: true })
|
||||
return `calulated ${status.nodes} nodes in ${runTime}`;
|
||||
}
|
||||
const runTime = formatDistance(status.start, status.end, { includeSeconds: true })
|
||||
return `calulated ${status.nodes} nodes in ${runTime}`;
|
||||
};
|
||||
|
||||
const PlanDayScreen: React.FC = () => {
|
||||
const date = useDate();
|
||||
const [location] = useCurrentLocation();
|
||||
const [startTime, setStartTime] = useState('06:00');
|
||||
const [commit] = useCommit();
|
||||
const current = useMemo(
|
||||
() => location || {
|
||||
id: 'unknown',
|
||||
title: 'foo',
|
||||
},
|
||||
[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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{!!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 };
|
||||
82
src/ui/screens/plan/set.tsx
Normal file
82
src/ui/screens/plan/set.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useLocations, useSetLocation } from "#/features/location";
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { 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";
|
||||
import Popup from "#/ui/components/popup";
|
||||
|
||||
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 };
|
||||
|
||||
132
src/ui/screens/plan/tasks.tsx
Normal file
132
src/ui/screens/plan/tasks.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
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 };
|
||||
41
src/ui/screens/routines/list.tsx
Normal file
41
src/ui/screens/routines/list.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
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 };
|
||||
84
src/ui/screens/routines/set.tsx
Normal file
84
src/ui/screens/routines/set.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { 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';
|
||||
import Popup from '#/ui/components/popup';
|
||||
|
||||
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 };
|
||||
|
||||
Reference in New Issue
Block a user