web version

This commit is contained in:
Morten Olsen
2022-05-07 12:35:14 +02:00
parent 7057e11e96
commit 0b2f23ecb2
10 changed files with 144 additions and 15 deletions

25
.github/workflows/publish-web.yml vendored Normal file
View File

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

View File

@@ -2,6 +2,7 @@
"name": "bob", "name": "bob",
"version": "1.0.0", "version": "1.0.0",
"main": "node_modules/expo/AppEntry.js", "main": "node_modules/expo/AppEntry.js",
"homepage": "/bob-the-algorithm",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo start --android",
@@ -50,6 +51,7 @@
"@babel/core": "^7.12.9", "@babel/core": "^7.12.9",
"@types/chroma-js": "^2.1.3", "@types/chroma-js": "^2.1.3",
"@types/react": "~17.0.21", "@types/react": "~17.0.21",
"@types/react-dom": "^18.0.3",
"@types/react-native": "~0.64.12", "@types/react-native": "~0.64.12",
"@types/styled-components-react-native": "^5.1.3", "@types/styled-components-react-native": "^5.1.3",
"babel-plugin-module-resolver": "^4.1.0", "babel-plugin-module-resolver": "^4.1.0",

View File

@@ -1,7 +1,20 @@
import { Calendar } from "expo-calendar"; import { Calendar } from "expo-calendar";
import { createContext } from "react"; import { createContext } from "react";
type CalendarContextValue = { 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; date: Date;
setDate: (date: Date) => void; setDate: (date: Date) => void;
calendars: Calendar[]; calendars: Calendar[];
@@ -11,6 +24,10 @@ type CalendarContextValue = {
error?: any; error?: any;
} }
type CalendarContextValue = RejectedCalendarContextValue
| UnavailableCalendarContextValue
| AcceptedCalendarContextValue
const CalendarContext = createContext<CalendarContextValue>(undefined as any); const CalendarContext = createContext<CalendarContextValue>(undefined as any);
export type { CalendarContextValue }; export type { CalendarContextValue };

View File

@@ -5,24 +5,39 @@ import { useAsync, useAsyncCallback } from "#/hooks/async";
import { createEventAsync, deleteEventAsync, getEventsAsync } from "expo-calendar"; import { createEventAsync, deleteEventAsync, getEventsAsync } from "expo-calendar";
import { PlanItem } from "#/types/plans"; import { PlanItem } from "#/types/plans";
const emptyArray: never[] = [];
const emptyFn = () => undefined;
export const useCalendar = () => { export const useCalendar = () => {
const { calendar } = useContext(CalendarContext); const context = useContext(CalendarContext);
return calendar; if (context.status !== 'ready') {
return undefined;
}
return context.calendar;
} }
export const useCalendars = () => { export const useCalendars = () => {
const { calendars } = useContext(CalendarContext); const context = useContext(CalendarContext);
return calendars; if (context.status !== 'ready') {
return emptyArray;
}
return context.calendars;
} }
export const useSelectedCalendars = () => { export const useSelectedCalendars = () => {
const { selected } = useContext(CalendarContext); const context = useContext(CalendarContext);
return selected; if (context.status !== 'ready') {
return emptyArray;
}
return context.selected;
} }
export const useSetSelectedCalendars = () => { export const useSetSelectedCalendars = () => {
const { setSelected } = useContext(CalendarContext); const context = useContext(CalendarContext);
return setSelected; if (context.status !== 'ready') {
return emptyFn;
}
return context.setSelected;
} }
export const useDate = () => { export const useDate = () => {
@@ -40,6 +55,9 @@ export const useCommit = () => {
const calendar = useCalendar(); const calendar = useCalendar();
const result = useAsyncCallback( const result = useAsyncCallback(
async (plan: PlanItem[]) => { async (plan: PlanItem[]) => {
if (!calendar) {
return;
}
const end = set(date, { const end = set(date, {
hours: 24, hours: 24,
minutes: 0, minutes: 0,
@@ -64,7 +82,7 @@ export const useCommit = () => {
}) })
} }
}, },
[date], [date, calendar],
); );
return result; return result;

View File

@@ -3,6 +3,7 @@ import React, { ReactNode, useCallback, useMemo, useState } from "react";
import { useAsync } from "#/hooks/async"; import { useAsync } from "#/hooks/async";
import { CalendarContext } from "./context"; import { CalendarContext } from "./context";
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { Platform } from "react-native";
const SELECTED_STORAGE_KEY = 'selected_calendars'; const SELECTED_STORAGE_KEY = 'selected_calendars';
@@ -15,6 +16,8 @@ type CalendarProviderProps = {
type SetupResponse = { type SetupResponse = {
status: 'rejected'; status: 'rejected';
} | {
status: 'unavailable';
} | { } | {
status: 'ready'; status: 'ready';
calendar: Calendar; calendar: Calendar;
@@ -31,6 +34,9 @@ const CalendarProvider: React.FC<CalendarProviderProps> = ({
const [value] = useAsync<SetupResponse>( const [value] = useAsync<SetupResponse>(
async () => { async () => {
const { status } = await requestCalendarPermissionsAsync(); const { status } = await requestCalendarPermissionsAsync();
if (Platform.OS !== 'ios') {
return { status: 'unavailable' };
}
if (status !== 'granted') { if (status !== 'granted') {
return { status: 'rejected' }; return { status: 'rejected' };
} }
@@ -81,13 +87,22 @@ const CalendarProvider: React.FC<CalendarProviderProps> = ({
[value, selectedIds], [value, selectedIds],
); );
if (!value || value.status !== 'ready') { if (!value) {
return <></> return <></>
} }
if (value.status !== 'ready') {
return (
<CalendarContext.Provider value={{ status: value.status, date, setDate }}>
{children}
</CalendarContext.Provider>
);
}
return ( return (
<CalendarContext.Provider <CalendarContext.Provider
value={{ value={{
status: 'ready',
setDate, setDate,
date, date,
selected, selected,

View File

@@ -1,7 +1,7 @@
import { UserLocation } from "#/types/location"; import { UserLocation } from "#/types/location";
import { createContext } from "react" import { createContext } from "react"
type Routine = { export type Routine = {
id: string; id: string;
title: string; title: string;
required: boolean; required: boolean;
@@ -15,7 +15,7 @@ type Routine = {
days?: boolean[]; days?: boolean[];
} }
type RoutinesContextValue = { export type RoutinesContextValue = {
routines: Routine[]; routines: Routine[];
remove: (id: string) => any; remove: (id: string) => any;
set: (routine: Routine) => any; set: (routine: Routine) => any;
@@ -23,5 +23,4 @@ type RoutinesContextValue = {
const RoutinesContext = createContext<RoutinesContextValue>(undefined as any); const RoutinesContext = createContext<RoutinesContextValue>(undefined as any);
export type { Routine, RoutinesContextValue };
export { RoutinesContext }; export { RoutinesContext };

View File

@@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Modal as Wrapper } from 'react-native'; import Wrapper from './react-modal';
import { Popup } from '../popup'; import { Popup } from '../popup';
type ModalProps = { type ModalProps = {
visible: boolean; visible: boolean;

View File

@@ -0,0 +1,43 @@
import ReactDOM from 'react-dom';
import React, { useMemo, useEffect, ReactNode } from 'react';
interface Props {
visible: boolean;
children: ReactNode;
}
const Modal: React.FC<Props> = ({ visible, children }) => {
const elm = useMemo(() => {
const newElm = document.createElement('div');
newElm.style.position = 'fixed';
newElm.style.display = 'flex';
newElm.style.flexDirection = 'column';
newElm.style.left = '0px';
newElm.style.top = '0px';
newElm.style.width = '100%';
newElm.style.height = '100%';
newElm.style.transition = 'transform 0.3s';
newElm.style.transform = 'translateY(100%)';
return newElm;
}, []);
useEffect(() => {
document.body.appendChild(elm);
return () => {
document.body.removeChild(elm);
};
}, [elm]);
useEffect(() => {
if (visible) {
elm.style.transform = 'translateY(0)';
} else {
elm.style.transform = 'translateY(100%)';
}
}, [elm, visible]);
return ReactDOM.createPortal(
<>{children}</>,
elm,
);
};
export default Modal;

View File

@@ -0,0 +1,3 @@
import { Modal } from 'react-native';
export default Modal;

View File

@@ -2269,6 +2269,13 @@
resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==
"@types/react-dom@^18.0.3":
version "18.0.3"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.3.tgz#a022ea08c75a476fe5e96b675c3e673363853831"
integrity sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==
dependencies:
"@types/react" "*"
"@types/react-native@^0.65": "@types/react-native@^0.65":
version "0.65.22" version "0.65.22"
resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.65.22.tgz#58eaf2f64eb37fcb5fa9725547a8e043c4ecbc07" resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.65.22.tgz#58eaf2f64eb37fcb5fa9725547a8e043c4ecbc07"