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.
+
+
+
+
+ ) : (
+ <>
+ {!!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';