diff --git a/crabfit-browser-extension/manifest.json b/crabfit-browser-extension/manifest.json new file mode 100644 index 0000000..6bcae9b --- /dev/null +++ b/crabfit-browser-extension/manifest.json @@ -0,0 +1,25 @@ +{ + "name": "Crab Fit", + "description": "Enter your availability to find a time that works for everyone!", + "version": "1.0", + "manifest_version": 2, + + "author": "Ben Grant", + "homepage_url": "https://crab.fit", + + "browser_action": { + "default_popup": "popup.html", + "default_icon": { + "16": "res/icon16.png", + "32": "res/icon32.png", + "48": "res/icon48.png", + "128": "res/icon128.png" + } + }, + "icons": { + "16": "res/icon16.png", + "32": "res/icon32.png", + "48": "res/icon48.png", + "128": "res/icon128.png" + } +} diff --git a/crabfit-browser-extension/popup.html b/crabfit-browser-extension/popup.html new file mode 100644 index 0000000..d922600 --- /dev/null +++ b/crabfit-browser-extension/popup.html @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/crabfit-browser-extension/res/icon128.png b/crabfit-browser-extension/res/icon128.png new file mode 100644 index 0000000..6b9643d Binary files /dev/null and b/crabfit-browser-extension/res/icon128.png differ diff --git a/crabfit-browser-extension/res/icon16.png b/crabfit-browser-extension/res/icon16.png new file mode 100644 index 0000000..9c24490 Binary files /dev/null and b/crabfit-browser-extension/res/icon16.png differ diff --git a/crabfit-browser-extension/res/icon32.png b/crabfit-browser-extension/res/icon32.png new file mode 100644 index 0000000..11987f1 Binary files /dev/null and b/crabfit-browser-extension/res/icon32.png differ diff --git a/crabfit-browser-extension/res/icon48.png b/crabfit-browser-extension/res/icon48.png new file mode 100644 index 0000000..11650f4 Binary files /dev/null and b/crabfit-browser-extension/res/icon48.png differ diff --git a/crabfit-frontend/src/App.tsx b/crabfit-frontend/src/App.tsx index 76a2e39..744e0fb 100644 --- a/crabfit-frontend/src/App.tsx +++ b/crabfit-frontend/src/App.tsx @@ -15,6 +15,7 @@ const EGG_PATTERN = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft' const Home = lazy(() => import('pages/Home/Home')); const Event = lazy(() => import('pages/Event/Event')); +const Create = lazy(() => import('pages/Create/Create')); const App = () => { const colortheme = useSettingsStore(state => state.theme); @@ -113,6 +114,11 @@ const App = () => { }> + )} /> + ( + }> + + )} /> ( }> diff --git a/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts b/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts index c33601c..6bfde63 100644 --- a/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts +++ b/crabfit-frontend/src/components/ToggleField/toggleFieldStyle.ts @@ -25,7 +25,8 @@ export const HiddenInput = styled.input` height: 0; width: 0; position: absolute; - right: -1000px; + left: -1000px; + opacity: 0; &:checked + label { color: ${props => props.theme.background}; diff --git a/crabfit-frontend/src/pages/Create/Create.tsx b/crabfit-frontend/src/pages/Create/Create.tsx new file mode 100644 index 0000000..083c8c3 --- /dev/null +++ b/crabfit-frontend/src/pages/Create/Create.tsx @@ -0,0 +1,248 @@ +import { useEffect, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { useForm } from 'react-hook-form'; + +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import timezone from 'dayjs/plugin/timezone'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; + +import { + TextField, + CalendarField, + TimeRangeField, + SelectField, + Button, + Donate, + Error, +} from 'components'; + +import { + StyledMain, + CreateForm, + TitleSmall, + TitleLarge, + P, + OfflineMessage, + ShareInfo, + Footer, + AboutSection, + Recent, +} from './createStyle'; + +import api from 'services'; +import { useRecentsStore } from 'stores'; + +import timezones from 'res/timezones.json'; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(customParseFormat); + +const Create = ({ offline }) => { + const { register, handleSubmit } = useForm({ + defaultValues: { + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + }, + }); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [createdEvent, setCreatedEvent] = useState(null); + const [copied, setCopied] = useState(null); + + const { push } = useHistory(); + + const recentsStore = useRecentsStore(); + + useEffect(() => { + if (window.self === window.top) { + push('/'); + } + document.title = 'Create a Crab Fit'; + }, [push]); + + const onSubmit = async data => { + setIsLoading(true); + setError(null); + try { + const { start, end } = JSON.parse(data.times); + const dates = JSON.parse(data.dates); + + if (dates.length === 0) { + return setError(`You haven't selected any dates!`); + } + const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8; + if (start === end) { + return setError(`The start and end times can't be the same`); + } + + let times = dates.reduce((times, date) => { + let day = []; + for (let i = start; i < (start > end ? 24 : end); i++) { + if (isSpecificDates) { + day.push( + dayjs.tz(date, 'DDMMYYYY', data.timezone) + .hour(i).minute(0).utc().format('HHmm-DDMMYYYY') + ); + } else { + day.push( + dayjs().tz(data.timezone) + .day(date).hour(i).minute(0).utc().format('HHmm-d') + ); + } + } + if (start > end) { + for (let i = 0; i < end; i++) { + if (isSpecificDates) { + day.push( + dayjs.tz(date, 'DDMMYYYY', data.timezone) + .hour(i).minute(0).utc().format('HHmm-DDMMYYYY') + ); + } else { + day.push( + dayjs().tz(data.timezone) + .day(date).hour(i).minute(0).utc().format('HHmm-d') + ); + } + } + } + return [...times, ...day]; + }, []); + + if (times.length === 0) { + return setError(`You don't have any time selected`); + } + + const response = await api.post('/event', { + event: { + name: data.name, + times: times, + }, + }); + setCreatedEvent(response.data); + recentsStore.addRecent({ + id: response.data.id, + created: response.data.created, + name: response.data.name, + }); + gtag('event', 'create_event', { + 'event_category': 'home', + }); + } catch (e) { + setError('An error ocurred while creating the event. Please try again later.'); + console.error(e); + } finally { + setIsLoading(false); + } + }; + + return ( + <> + + CREATE A + CRAB FIT + + + {createdEvent ? ( + + +

Created {createdEvent.name}

+ navigator.clipboard?.writeText(`https://crab.fit/${createdEvent.id}`) + .then(() => { + setCopied('Copied!'); + setTimeout(() => setCopied(null), 1000); + gtag('event', 'copy_link', { + 'event_category': 'event', + }); + }) + .catch((e) => console.error('Failed to copy', e)) + } + title={!!navigator.clipboard ? 'Click to copy' : ''} + >{copied ?? `https://crab.fit/${createdEvent.id}`} + + {/* eslint-disable-next-line */} + Click the link above to copy it to your clipboard, or share via gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(`Scheduling ${createdEvent.name}`)}&body=${encodeURIComponent(`Visit this link to enter your availabilities: https://crab.fit/${createdEvent.id}`)}`} target="_blank">email. + +
+ Thank you for using Crab Fit. If you like it, consider donating. + +
+
+
+ ) : ( + <> + {!!recentsStore.recents.length && ( + + +

Recently visited

+ {recentsStore.recents.map(event => ( + + {event.name} + Created {dayjs.unix(event.created).format('D MMMM, YYYY')} + + ))} +
+
+ )} + + + {offline ? ( + +

🦀📵

+

You can't create a Crab Fit when you don't have an internet connection. Please make sure you're connected.

+
+ ) : ( + + + + + + + + + + {error && ( + setError(null)}>{error} + )} + + + + )} +
+ + )} + + ); +}; + +export default Create; diff --git a/crabfit-frontend/src/pages/Create/createStyle.ts b/crabfit-frontend/src/pages/Create/createStyle.ts new file mode 100644 index 0000000..d6b37b3 --- /dev/null +++ b/crabfit-frontend/src/pages/Create/createStyle.ts @@ -0,0 +1,106 @@ +import styled from '@emotion/styled'; + +export const StyledMain = styled.div` + width: 600px; + margin: 10px auto; + max-width: calc(100% - 30px); +`; + +export const CreateForm = styled.form` + margin: 0 0 30px; +`; + +export const TitleSmall = styled.span` + display: block; + margin: 0; + font-size: 2rem; + text-align: center; + font-family: 'Samurai Bob', sans-serif; + font-weight: 400; + color: ${props => props.theme.primaryDark}; + line-height: 1em; +`; + +export const TitleLarge = styled.h1` + margin: 0; + font-size: 2rem; + text-align: center; + color: ${props => props.theme.primary}; + font-family: 'Molot', sans-serif; + font-weight: 400; + text-shadow: 0 4px 0 ${props => props.theme.primaryDark}; + line-height: 1em; +`; + +export const P = styled.p` + font-weight: 500; + line-height: 1.6em; +`; + +export const Footer = styled.footer` + margin: 60px auto 0; + width: 250px; + + & span { + display: block; + margin-bottom: 20px; + } +`; + +export const OfflineMessage = styled.div` + text-align: center; + margin: 50px 0 20px; +`; + +export const ShareInfo = styled.p` + margin: 6px 0; + text-align: center; + font-size: 15px; + padding: 10px 0; + + ${props => props.onClick && ` + cursor: pointer; + + &:hover { + color: ${props.theme.primaryDark}; + } + `} +`; + +export const AboutSection = styled.section` + margin: 30px 0 0; + background-color: ${props => props.theme.primaryBackground}; + padding: 10px 0; + + & h2 { + margin: 0 0 10px; + font-size: 1.2rem; + } +`; + +export const Recent = styled.a` + text-decoration: none; + color: inherit; + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 0; + flex-wrap: wrap; + + & .name { + font-weight: 700; + color: ${props => props.theme.primaryDark}; + } + & .date { + font-weight: 400; + font-size: .9em; + opacity: .8; + text-align: right; + flex: 1; + white-space: nowrap; + } + + &:hover .name { + text-decoration: underline; + } +`; diff --git a/crabfit-frontend/src/pages/Home/Home.tsx b/crabfit-frontend/src/pages/Home/Home.tsx index 7a4e3ab..59c1510 100644 --- a/crabfit-frontend/src/pages/Home/Home.tsx +++ b/crabfit-frontend/src/pages/Home/Home.tsx @@ -164,7 +164,7 @@ const Home = ({ offline }) => {

Recently visited

{recentsStore.recents.map(event => ( - + {event.name} Created {dayjs.unix(event.created).format('D MMMM, YYYY')} diff --git a/crabfit-frontend/src/pages/Home/homeStyle.ts b/crabfit-frontend/src/pages/Home/homeStyle.ts index f37f112..1e9797f 100644 --- a/crabfit-frontend/src/pages/Home/homeStyle.ts +++ b/crabfit-frontend/src/pages/Home/homeStyle.ts @@ -101,14 +101,20 @@ export const Recent = styled.a` display: flex; align-items: center; justify-content: space-between; - margin: 10px 0; + padding: 5px 0; + flex-wrap: wrap; & .name { - font-weight: 800; + font-weight: 700; font-size: 1.1em; + color: ${props => props.theme.primaryDark}; } & .date { font-weight: 400; + opacity: .8; + text-align: right; + flex: 1; + white-space: nowrap; } &:hover .name { diff --git a/crabfit-frontend/src/pages/index.ts b/crabfit-frontend/src/pages/index.ts index a8cf604..a4f9c82 100644 --- a/crabfit-frontend/src/pages/index.ts +++ b/crabfit-frontend/src/pages/index.ts @@ -1,2 +1,3 @@ export { default as Home } from './Home/Home'; export { default as Event } from './Event/Event'; +export { default as Create } from './Create/Create';