Merge pull request #4 from GRA0007/dev
Recently visited and browser extension
This commit is contained in:
commit
4aeb580548
25
crabfit-browser-extension/manifest.json
Normal file
25
crabfit-browser-extension/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
crabfit-browser-extension/popup.html
Normal file
22
crabfit-browser-extension/popup.html
Normal 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>
|
||||||
BIN
crabfit-browser-extension/res/icon128.png
Normal file
BIN
crabfit-browser-extension/res/icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
crabfit-browser-extension/res/icon16.png
Normal file
BIN
crabfit-browser-extension/res/icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 620 B |
BIN
crabfit-browser-extension/res/icon32.png
Normal file
BIN
crabfit-browser-extension/res/icon32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
crabfit-browser-extension/res/icon48.png
Normal file
BIN
crabfit-browser-extension/res/icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -15,6 +15,7 @@ const EGG_PATTERN = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft'
|
||||||
|
|
||||||
const Home = lazy(() => import('pages/Home/Home'));
|
const Home = lazy(() => import('pages/Home/Home'));
|
||||||
const Event = lazy(() => import('pages/Event/Event'));
|
const Event = lazy(() => import('pages/Event/Event'));
|
||||||
|
const Create = lazy(() => import('pages/Create/Create'));
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const colortheme = useSettingsStore(state => state.theme);
|
const colortheme = useSettingsStore(state => state.theme);
|
||||||
|
|
@ -113,6 +114,11 @@ const App = () => {
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Home offline={offline} {...props} />
|
<Home offline={offline} {...props} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
)} />
|
||||||
|
<Route path="/create" exact render={props => (
|
||||||
|
<Suspense fallback={<Loading />}>
|
||||||
|
<Create offline={offline} {...props} />
|
||||||
|
</Suspense>
|
||||||
)} />
|
)} />
|
||||||
<Route path="/:id" exact render={props => (
|
<Route path="/:id" exact render={props => (
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@ export const HiddenInput = styled.input`
|
||||||
height: 0;
|
height: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -1000px;
|
left: -1000px;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
&:checked + label {
|
&:checked + label {
|
||||||
color: ${props => props.theme.background};
|
color: ${props => props.theme.background};
|
||||||
|
|
|
||||||
248
crabfit-frontend/src/pages/Create/Create.tsx
Normal file
248
crabfit-frontend/src/pages/Create/Create.tsx
Normal 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;
|
||||||
106
crabfit-frontend/src/pages/Create/createStyle.ts
Normal file
106
crabfit-frontend/src/pages/Create/createStyle.ts
Normal 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;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
@ -34,7 +34,7 @@ import {
|
||||||
} from './eventStyle';
|
} from './eventStyle';
|
||||||
|
|
||||||
import api from 'services';
|
import api from 'services';
|
||||||
import { useSettingsStore } from 'stores';
|
import { useSettingsStore, useRecentsStore } from 'stores';
|
||||||
|
|
||||||
import logo from 'res/logo.svg';
|
import logo from 'res/logo.svg';
|
||||||
import timezones from 'res/timezones.json';
|
import timezones from 'res/timezones.json';
|
||||||
|
|
@ -47,6 +47,8 @@ const Event = (props) => {
|
||||||
const timeFormat = useSettingsStore(state => state.timeFormat);
|
const timeFormat = useSettingsStore(state => state.timeFormat);
|
||||||
const weekStart = useSettingsStore(state => state.weekStart);
|
const weekStart = useSettingsStore(state => state.weekStart);
|
||||||
|
|
||||||
|
const addRecent = useRecentsStore(state => state.addRecent);
|
||||||
|
|
||||||
const { register, handleSubmit } = useForm();
|
const { register, handleSubmit } = useForm();
|
||||||
const { id } = props.match.params;
|
const { id } = props.match.params;
|
||||||
const { offline } = props;
|
const { offline } = props;
|
||||||
|
|
@ -74,6 +76,11 @@ const Event = (props) => {
|
||||||
const response = await api.get(`/event/${id}`);
|
const response = await api.get(`/event/${id}`);
|
||||||
|
|
||||||
setEvent(response.data);
|
setEvent(response.data);
|
||||||
|
addRecent({
|
||||||
|
id: response.data.id,
|
||||||
|
created: response.data.created,
|
||||||
|
name: response.data.name,
|
||||||
|
});
|
||||||
document.title = `${response.data.name} | Crab Fit`;
|
document.title = `${response.data.name} | Crab Fit`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -83,7 +90,7 @@ const Event = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchEvent();
|
fetchEvent();
|
||||||
}, [id]);
|
}, [id, addRecent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPeople = async () => {
|
const fetchPeople = async () => {
|
||||||
|
|
|
||||||
|
|
@ -33,9 +33,11 @@ import {
|
||||||
StatNumber,
|
StatNumber,
|
||||||
StatLabel,
|
StatLabel,
|
||||||
OfflineMessage,
|
OfflineMessage,
|
||||||
|
Recent,
|
||||||
} from './homeStyle';
|
} from './homeStyle';
|
||||||
|
|
||||||
import api from 'services';
|
import api from 'services';
|
||||||
|
import { useRecentsStore } from 'stores';
|
||||||
|
|
||||||
import logo from 'res/logo.svg';
|
import logo from 'res/logo.svg';
|
||||||
import timezones from 'res/timezones.json';
|
import timezones from 'res/timezones.json';
|
||||||
|
|
@ -58,6 +60,7 @@ const Home = ({ offline }) => {
|
||||||
version: 'loading...',
|
version: 'loading...',
|
||||||
});
|
});
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
|
const recentsStore = useRecentsStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetch = async () => {
|
const fetch = async () => {
|
||||||
|
|
@ -154,7 +157,23 @@ const Home = ({ offline }) => {
|
||||||
<Links>
|
<Links>
|
||||||
<a href="#about">About</a> / <a href="#donate">Donate</a>
|
<a href="#about">About</a> / <a href="#donate">Donate</a>
|
||||||
</Links>
|
</Links>
|
||||||
|
</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 ? (
|
{offline ? (
|
||||||
<OfflineMessage>
|
<OfflineMessage>
|
||||||
<h1>🦀📵</h1>
|
<h1>🦀📵</h1>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ export const StyledMain = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CreateForm = styled.form`
|
export const CreateForm = styled.form`
|
||||||
|
margin: 0 0 60px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TitleSmall = styled.span`
|
export const TitleSmall = styled.span`
|
||||||
|
|
@ -45,7 +46,7 @@ export const Links = styled.nav`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const AboutSection = styled.section`
|
export const AboutSection = styled.section`
|
||||||
margin: 60px 0 0;
|
margin: 30px 0 0;
|
||||||
background-color: ${props => props.theme.primaryBackground};
|
background-color: ${props => props.theme.primaryBackground};
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
`;
|
`;
|
||||||
|
|
@ -93,3 +94,30 @@ export const OfflineMessage = styled.div`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 50px 0 20px;
|
margin: 50px 0 20px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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 {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
export { default as Home } from './Home/Home';
|
export { default as Home } from './Home/Home';
|
||||||
export { default as Event } from './Event/Event';
|
export { default as Event } from './Event/Event';
|
||||||
|
export { default as Create } from './Create/Create';
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,22 @@ export const useSettingsStore = create(persist(
|
||||||
}),
|
}),
|
||||||
{ name: 'crabfit-settings' },
|
{ name: 'crabfit-settings' },
|
||||||
));
|
));
|
||||||
|
|
||||||
|
export const useRecentsStore = create(persist(
|
||||||
|
set => ({
|
||||||
|
recents: [],
|
||||||
|
|
||||||
|
addRecent: event => set(state => {
|
||||||
|
const recents = state.recents.filter(e => e.id !== event.id);
|
||||||
|
recents.unshift(event);
|
||||||
|
recents.length = Math.min(recents.length, 5);
|
||||||
|
return { recents };
|
||||||
|
}),
|
||||||
|
removeRecent: id => set(state => {
|
||||||
|
const recents = state.recents.filter(e => e.id !== id);
|
||||||
|
return { recents };
|
||||||
|
}),
|
||||||
|
clearRecents: () => set({ recents: [] }),
|
||||||
|
}),
|
||||||
|
{ name: 'crabfit-recent' },
|
||||||
|
));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue