Chrome and Firefox extension

This commit is contained in:
Ben Grant 2021-04-15 18:04:28 +10:00
parent b5bf833dcf
commit 9d4b2ffbb5
13 changed files with 419 additions and 4 deletions

View file

@ -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"
}
}

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body {
width: 360px;
height: 500px;
margin: 0;
}
iframe {
height: 100%;
width: 100%;
border: 0;
}
</style>
</head>
<body>
<iframe src="https://crab.fit/create" allow="clipboard-write"></iframe>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -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 = () => {
<Suspense fallback={<Loading />}>
<Home offline={offline} {...props} />
</Suspense>
)} />
<Route path="/create" exact render={props => (
<Suspense fallback={<Loading />}>
<Create offline={offline} {...props} />
</Suspense>
)} />
<Route path="/:id" exact render={props => (
<Suspense fallback={<Loading />}>

View file

@ -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};

View file

@ -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 (
<>
<StyledMain>
<TitleSmall>CREATE A</TitleSmall>
<TitleLarge>CRAB FIT</TitleLarge>
</StyledMain>
{createdEvent ? (
<StyledMain>
<OfflineMessage>
<h2>Created {createdEvent.name}</h2>
<ShareInfo
onClick={() => 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}`}</ShareInfo>
<ShareInfo>
{/* eslint-disable-next-line */}
Click the link above to copy it to your clipboard, or share via <a onClick={() => 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</a>.
</ShareInfo>
<Footer>
<span>Thank you for using Crab Fit. If you like it, consider donating.</span>
<Donate />
</Footer>
</OfflineMessage>
</StyledMain>
) : (
<>
{!!recentsStore.recents.length && (
<AboutSection id="recents">
<StyledMain>
<h2>Recently visited</h2>
{recentsStore.recents.map(event => (
<Recent href={`/${event.id}`} key={event.id}>
<span className="name">{event.name}</span>
<span className="date">Created {dayjs.unix(event.created).format('D MMMM, YYYY')}</span>
</Recent>
))}
</StyledMain>
</AboutSection>
)}
<StyledMain>
{offline ? (
<OfflineMessage>
<h1>🦀📵</h1>
<P>You can't create a Crab Fit when you don't have an internet connection. Please make sure you're connected.</P>
</OfflineMessage>
) : (
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
<TextField
label="Give your event a name!"
subLabel="Or leave blank to generate one"
type="text"
name="name"
id="name"
register={register}
/>
<CalendarField
label="What dates might work?"
subLabel="Click and drag to select"
name="dates"
id="dates"
required
register={register}
/>
<TimeRangeField
label="What times might work?"
subLabel="Click and drag to select a time range"
name="times"
id="times"
required
register={register}
/>
<SelectField
label="And the timezone"
name="timezone"
id="timezone"
register={register}
options={timezones}
required
defaultOption="Select..."
/>
{error && (
<Error onClose={() => setError(null)}>{error}</Error>
)}
<Button type="submit" isLoading={isLoading} disabled={isLoading} buttonWidth="100%">Create</Button>
</CreateForm>
)}
</StyledMain>
</>
)}
</>
);
};
export default Create;

View file

@ -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;
}
`;

View file

@ -164,7 +164,7 @@ const Home = ({ offline }) => {
<StyledMain>
<h2>Recently visited</h2>
{recentsStore.recents.map(event => (
<Recent href={`/${event.id}`}>
<Recent href={`/${event.id}`} key={event.id}>
<span className="name">{event.name}</span>
<span className="date">Created {dayjs.unix(event.created).format('D MMMM, YYYY')}</span>
</Recent>

View file

@ -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 {

View file

@ -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';