Create event with API and load event details
This commit is contained in:
parent
855477570f
commit
8e5954e0ca
|
|
@ -2,6 +2,7 @@ require('dotenv').config();
|
||||||
|
|
||||||
const { Datastore } = require('@google-cloud/datastore');
|
const { Datastore } = require('@google-cloud/datastore');
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const cors = require('cors');
|
||||||
|
|
||||||
const package = require('./package.json');
|
const package = require('./package.json');
|
||||||
|
|
||||||
|
|
@ -20,6 +21,9 @@ const datastore = new Datastore({
|
||||||
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: 'http://localhost:3000',
|
||||||
|
}));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.datastore = datastore;
|
req.datastore = datastore;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google-cloud/datastore": "^6.3.1",
|
"@google-cloud/datastore": "^6.3.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1"
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ module.exports = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const query = req.datastore.createQuery(['__Stat_Kind__']);
|
const query = req.datastore.createQuery(['__Stat_Kind__']);
|
||||||
|
|
||||||
eventCount = (await req.datastore.runQuery(query.filter('kind_name', 'Event')))[0].count;
|
eventCount = (await req.datastore.runQuery(query.filter('kind_name', 'Event')))[0][0].count;
|
||||||
personCount = (await req.datastore.runQuery(query.filter('kind_name', 'Person')))[0].count;
|
personCount = (await req.datastore.runQuery(query.filter('kind_name', 'Person')))[0][0].count;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -306,6 +306,14 @@ core-util-is@~1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
|
||||||
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
|
||||||
|
|
||||||
|
cors@^2.8.5:
|
||||||
|
version "2.8.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
|
||||||
|
integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
|
||||||
|
dependencies:
|
||||||
|
object-assign "^4"
|
||||||
|
vary "^1"
|
||||||
|
|
||||||
dayjs@^1.10.4:
|
dayjs@^1.10.4:
|
||||||
version "1.10.4"
|
version "1.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
|
||||||
|
|
@ -851,7 +859,7 @@ number-is-nan@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||||
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
|
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
|
||||||
|
|
||||||
object-assign@^4.1.0:
|
object-assign@^4, object-assign@^4.1.0:
|
||||||
version "4.1.1"
|
version "4.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||||
|
|
@ -1180,7 +1188,7 @@ utils-merge@1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||||
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
|
||||||
|
|
||||||
vary@~1.1.2:
|
vary@^1, vary@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||||
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
"@types/node": "^14.14.31",
|
"@types/node": "^14.14.31",
|
||||||
"@types/react": "^17.0.2",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-dom": "^17.0.1",
|
"@types/react-dom": "^17.0.1",
|
||||||
|
"axios": "^0.21.1",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ const AvailabilityEditor = ({
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Container>
|
<Container>
|
||||||
<TimeLabels>
|
<TimeLabels>
|
||||||
{times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) =>
|
{!!times.length && times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) =>
|
||||||
<TimeSpace key={i} time={time}>
|
<TimeSpace key={i} time={time}>
|
||||||
{time.slice(-2) === '00' && <TimeLabel>{dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}</TimeLabel>}
|
{time.slice(-2) === '00' && <TimeLabel>{dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}</TimeLabel>}
|
||||||
</TimeSpace>
|
</TimeSpace>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ const AvailabilityViewer = ({
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
<Container>
|
<Container>
|
||||||
<TimeLabels>
|
<TimeLabels>
|
||||||
{times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) =>
|
{!!times.length && times.concat([`${parseInt(times[times.length-1].slice(0, 2))+1}00`]).map((time, i) =>
|
||||||
<TimeSpace key={i} time={time}>
|
<TimeSpace key={i} time={time}>
|
||||||
{time.slice(-2) === '00' && <TimeLabel>{dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}</TimeLabel>}
|
{time.slice(-2) === '00' && <TimeLabel>{dayjs().hour(time.slice(0, 2)).minute(time.slice(-2)).format('h A')}</TimeLabel>}
|
||||||
</TimeSpace>
|
</TimeSpace>
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,34 @@ export const Top = styled.button`
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
filter: brightness(1.2);
|
filter: brightness(1.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${props => props.isLoading && `
|
||||||
|
text-shadow: none;
|
||||||
|
color: transparent;
|
||||||
|
cursor: wait;
|
||||||
|
|
||||||
|
@keyframes load {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: calc(50% - 12px);
|
||||||
|
left: calc(50% - 12px);
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
border: 3px solid #FFF;
|
||||||
|
border-left-color: transparent;
|
||||||
|
border-radius: 100px;
|
||||||
|
animation: load .5s linear infinite;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Bottom = styled.div`
|
export const Bottom = styled.div`
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ const Donate = () => (
|
||||||
buttonHeight="30px"
|
buttonHeight="30px"
|
||||||
buttonWidth="90px"
|
buttonWidth="90px"
|
||||||
type="button"
|
type="button"
|
||||||
|
tabIndex="-1"
|
||||||
>Donate</Button>
|
>Donate</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
16
crabfit-frontend/src/components/Error/Error.tsx
Normal file
16
crabfit-frontend/src/components/Error/Error.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { Wrapper, CloseButton } from './errorStyle';
|
||||||
|
|
||||||
|
const Error = ({
|
||||||
|
children,
|
||||||
|
onClose,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<Wrapper {...props}>
|
||||||
|
{children}
|
||||||
|
<CloseButton type="button" onClick={onClose}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||||
|
</CloseButton>
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Error;
|
||||||
26
crabfit-frontend/src/components/Error/errorStyle.ts
Normal file
26
crabfit-frontend/src/components/Error/errorStyle.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
export const Wrapper = styled.div`
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: ${props => props.theme.error};
|
||||||
|
color: #FFFFFF;
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 18px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const CloseButton = styled.button`
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: 16px;
|
||||||
|
`;
|
||||||
|
|
@ -7,6 +7,7 @@ export { default as Button } from './Button/Button';
|
||||||
export { default as Legend } from './Legend/Legend';
|
export { default as Legend } from './Legend/Legend';
|
||||||
export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer';
|
export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer';
|
||||||
export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor';
|
export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor';
|
||||||
|
export { default as Error } from './Error/Error';
|
||||||
|
|
||||||
export { default as Center } from './Center/Center';
|
export { default as Center } from './Center/Center';
|
||||||
export { default as Donate } from './Donate/Donate';
|
export { default as Donate } from './Donate/Donate';
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Center,
|
Center,
|
||||||
|
|
@ -27,18 +27,60 @@ import {
|
||||||
Tab,
|
Tab,
|
||||||
} from './eventStyle';
|
} from './eventStyle';
|
||||||
|
|
||||||
|
import api from 'services';
|
||||||
|
|
||||||
import logo from 'res/logo.svg';
|
import logo from 'res/logo.svg';
|
||||||
import timezones from 'res/timezones.json';
|
import timezones from 'res/timezones.json';
|
||||||
|
|
||||||
const Event = (props) => {
|
const Event = (props) => {
|
||||||
const { register, handleSubmit } = useForm();
|
const { register, handleSubmit } = useForm();
|
||||||
const id = props.match.params.id;
|
const { id } = props.match.params;
|
||||||
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||||
const [user, setUser] = useState({
|
const [user, setUser] = useState(null);
|
||||||
name: 'Benji',
|
const [password, setPassword] = useState(null);
|
||||||
availability: [],
|
|
||||||
});
|
|
||||||
const [tab, setTab] = useState(user ? 'you' : 'group');
|
const [tab, setTab] = useState(user ? 'you' : 'group');
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [event, setEvent] = useState(null);
|
||||||
|
const [people, setPeople] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchEvent = async () => {
|
||||||
|
const response = await api.get(`/event/${id}`);
|
||||||
|
if (response.status === 200) {
|
||||||
|
let times = [];
|
||||||
|
for (let i = response.data.startTime; i < response.data.endTime; i++) {
|
||||||
|
let hour = `${i}`.padStart(2, '0');
|
||||||
|
times.push(
|
||||||
|
`${hour}00`,
|
||||||
|
`${hour}15`,
|
||||||
|
`${hour}30`,
|
||||||
|
`${hour}45`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEvent({
|
||||||
|
...response.data,
|
||||||
|
times,
|
||||||
|
});
|
||||||
|
setIsLoading(false);
|
||||||
|
} else {
|
||||||
|
console.error(response);
|
||||||
|
//TODO: 404
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPeople = async () => {
|
||||||
|
const response = await api.get(`/event/${id}/people`);
|
||||||
|
if (response.status === 200) {
|
||||||
|
setPeople(response.data.people);
|
||||||
|
} else {
|
||||||
|
console.error(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchEvent();
|
||||||
|
fetchPeople();
|
||||||
|
}, [id]);
|
||||||
|
|
||||||
const onSubmit = data => console.log('submit', data);
|
const onSubmit = data => console.log('submit', data);
|
||||||
|
|
||||||
|
|
@ -52,9 +94,13 @@ const Event = (props) => {
|
||||||
</Center>
|
</Center>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<EventName>Event name ({id})</EventName>
|
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
||||||
<ShareInfo>https://page.url</ShareInfo>
|
<ShareInfo isLoading={isLoading}>{!!event?.name && `https://crab.fit/${id}`}</ShareInfo>
|
||||||
<ShareInfo>Copy the link to this page, or share via <a href="#test">Email</a> or <a href="#test">Facebook</a>.</ShareInfo>
|
<ShareInfo isLoading={isLoading}>
|
||||||
|
{!!event?.name &&
|
||||||
|
<>Copy the link to this page, or share via <a href={`mailto:?subject=${encodeURIComponent(`Scheduling ${event?.name}`)}&body=${encodeURIComponent(`Visit this link to enter your availabilities: https://crab.fit/${id}`)}`}>Email</a>.</>
|
||||||
|
}
|
||||||
|
</ShareInfo>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
||||||
<LoginSection id="login">
|
<LoginSection id="login">
|
||||||
|
|
@ -129,47 +175,13 @@ const Event = (props) => {
|
||||||
{tab === 'group' ? (
|
{tab === 'group' ? (
|
||||||
<section id="group">
|
<section id="group">
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<Legend min={0} max={3} />
|
<Legend min={0} max={people.length} />
|
||||||
<Center>Hover or tap the calendar below to see who is available</Center>
|
<Center>Hover or tap the calendar below to see who is available</Center>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
<AvailabilityViewer
|
<AvailabilityViewer
|
||||||
dates={['03032021', '04032021', '05032021', '07032021', '08032021']}
|
dates={event?.dates ?? []}
|
||||||
times={['0900', '0915', '0930', '0945', '1000', '1015', '1030', '1045', '1100', '1115', '1130', '1145', '1200', '1215', '1230', '1245', '1300', '1315', '1330', '1345', '1400', '1415', '1430', '1445', '1500', '1515', '1530', '1545', '1600', '1615', '1630', '1645']}
|
times={event?.times ?? []}
|
||||||
people={[{
|
people={people}
|
||||||
name: 'James',
|
|
||||||
availability: [
|
|
||||||
'0900-04032021',
|
|
||||||
'0915-04032021',
|
|
||||||
'0930-04032021',
|
|
||||||
'0945-04032021',
|
|
||||||
'1000-04032021',
|
|
||||||
'1500-04032021',
|
|
||||||
'1515-04032021',
|
|
||||||
'1230-07032021',
|
|
||||||
'1245-07032021',
|
|
||||||
'1300-07032021',
|
|
||||||
'1315-07032021',
|
|
||||||
'1400-08032021',
|
|
||||||
'1430-08032021',
|
|
||||||
],
|
|
||||||
},{
|
|
||||||
name: 'Phoebe',
|
|
||||||
availability: [
|
|
||||||
'1100-07032021',
|
|
||||||
'1115-07032021',
|
|
||||||
'1130-07032021',
|
|
||||||
'1145-07032021',
|
|
||||||
'1200-07032021',
|
|
||||||
'1215-07032021',
|
|
||||||
'1230-07032021',
|
|
||||||
'1245-07032021',
|
|
||||||
'1300-07032021',
|
|
||||||
'1315-07032021',
|
|
||||||
'1330-07032021',
|
|
||||||
'1345-07032021',
|
|
||||||
'1400-07032021',
|
|
||||||
],
|
|
||||||
},user]}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -178,10 +190,23 @@ const Event = (props) => {
|
||||||
<Center>Click and drag the calendar below to set your availabilities</Center>
|
<Center>Click and drag the calendar below to set your availabilities</Center>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
<AvailabilityEditor
|
<AvailabilityEditor
|
||||||
dates={['03032021', '04032021', '05032021', '07032021', '08032021']}
|
dates={event?.dates ?? []}
|
||||||
times={['0900', '0915', '0930', '0945', '1000', '1015', '1030', '1045', '1100', '1115', '1130', '1145', '1200', '1215', '1230', '1245', '1300', '1315', '1330', '1345', '1400', '1415', '1430', '1445', '1500', '1515', '1530', '1545', '1600', '1615', '1630', '1645']}
|
times={event?.times ?? []}
|
||||||
value={user.availability}
|
value={user.availability}
|
||||||
onChange={availability => setUser({ ...user, availability })}
|
onChange={async availability => {
|
||||||
|
const oldAvailability = [...user.availability];
|
||||||
|
setUser({ ...user, availability });
|
||||||
|
const response = await api.patch(`/event/${id}/people/${user.name}`, {
|
||||||
|
person: {
|
||||||
|
password,
|
||||||
|
availability,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
console.log(response);
|
||||||
|
setUser({ ...user, oldAvailability });
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,18 @@ export const EventName = styled.h1`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
margin: 20px 0 14px;
|
margin: 20px 0 14px;
|
||||||
|
|
||||||
|
${props => props.isLoading && `
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 1em;
|
||||||
|
width: 300px;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: ${props.theme.loading};
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LoginForm = styled.form`
|
export const LoginForm = styled.form`
|
||||||
|
|
@ -65,6 +77,18 @@ export const ShareInfo = styled.p`
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|
||||||
|
${props => props.isLoading && `
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
height: 1em;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: ${props.theme.loading};
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tabs = styled.div`
|
export const Tabs = styled.div`
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -8,6 +10,7 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Donate,
|
Donate,
|
||||||
|
Error,
|
||||||
} from 'components';
|
} from 'components';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,8 +23,14 @@ import {
|
||||||
AboutSection,
|
AboutSection,
|
||||||
Footer,
|
Footer,
|
||||||
P,
|
P,
|
||||||
|
Stats,
|
||||||
|
Stat,
|
||||||
|
StatNumber,
|
||||||
|
StatLabel,
|
||||||
} from './homeStyle';
|
} from './homeStyle';
|
||||||
|
|
||||||
|
import api from 'services';
|
||||||
|
|
||||||
import logo from 'res/logo.svg';
|
import logo from 'res/logo.svg';
|
||||||
import timezones from 'res/timezones.json';
|
import timezones from 'res/timezones.json';
|
||||||
|
|
||||||
|
|
@ -31,8 +40,55 @@ const Home = () => {
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [stats, setStats] = useState({
|
||||||
|
eventCount: null,
|
||||||
|
personCount: null,
|
||||||
|
version: 'loading...',
|
||||||
|
});
|
||||||
|
const { push } = useHistory();
|
||||||
|
|
||||||
const onSubmit = data => console.log('submit', data);
|
useEffect(() => {
|
||||||
|
const fetch = async () => {
|
||||||
|
const response = await api.get('/stats');
|
||||||
|
if (response.status === 200) {
|
||||||
|
setStats(response.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSubmit = async data => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const times = JSON.parse(data.times);
|
||||||
|
const response = await api.post('/event', {
|
||||||
|
event: {
|
||||||
|
name: data.name,
|
||||||
|
timezone: data.timezone,
|
||||||
|
startTime: times.start,
|
||||||
|
endTime: times.end,
|
||||||
|
dates: JSON.parse(data.dates),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 201) {
|
||||||
|
// Success
|
||||||
|
push(`/${response.data.id}`);
|
||||||
|
} else {
|
||||||
|
setError('An error ocurred while creating the event. Please try again later.');
|
||||||
|
console.error(response.status);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setError('An error ocurred while creating the event. Please try again later.');
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -83,8 +139,12 @@ const Home = () => {
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Error onClose={() => setError(null)}>{error}</Error>
|
||||||
|
)}
|
||||||
|
|
||||||
<Center>
|
<Center>
|
||||||
<Button type="submit">Create</Button>
|
<Button type="submit" isLoading={isLoading} disabled={isLoading}>Create</Button>
|
||||||
</Center>
|
</Center>
|
||||||
</CreateForm>
|
</CreateForm>
|
||||||
</StyledMain>
|
</StyledMain>
|
||||||
|
|
@ -92,6 +152,16 @@ const Home = () => {
|
||||||
<AboutSection id="about">
|
<AboutSection id="about">
|
||||||
<StyledMain>
|
<StyledMain>
|
||||||
<h2>About Crab Fit</h2>
|
<h2>About Crab Fit</h2>
|
||||||
|
<Stats>
|
||||||
|
<Stat>
|
||||||
|
<StatNumber>{stats.eventCount ?? '10+'}</StatNumber>
|
||||||
|
<StatLabel>Events created</StatLabel>
|
||||||
|
</Stat>
|
||||||
|
<Stat>
|
||||||
|
<StatNumber>{stats.peopleCount ?? '10+'}</StatNumber>
|
||||||
|
<StatLabel>Availabilities entered</StatLabel>
|
||||||
|
</Stat>
|
||||||
|
</Stats>
|
||||||
<P>Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.</P>
|
<P>Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free.</P>
|
||||||
{/* eslint-disable-next-line */}
|
{/* eslint-disable-next-line */}
|
||||||
<P>Create by <a href="https://bengrant.dev" target="_blank">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</P>
|
<P>Create by <a href="https://bengrant.dev" target="_blank">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</P>
|
||||||
|
|
|
||||||
|
|
@ -63,3 +63,28 @@ export const P = styled.p`
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Stats = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Stat = styled.div`
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
min-width: 160px;
|
||||||
|
margin: 10px 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StatNumber = styled.span`
|
||||||
|
display: block;
|
||||||
|
font-weight: 900;
|
||||||
|
color: ${props => props.theme.primaryDark};
|
||||||
|
font-size: 2em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StatLabel = styled.span`
|
||||||
|
display: block;
|
||||||
|
`;
|
||||||
|
|
|
||||||
45
crabfit-frontend/src/services/index.js
Normal file
45
crabfit-frontend/src/services/index.js
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export const instance = axios.create({
|
||||||
|
baseURL: 'http://localhost:8080',
|
||||||
|
timeout: 1000 * 300,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleError = error => {
|
||||||
|
if (error.response && error.response.status) {
|
||||||
|
console.log('[Error handler] res:', error.response);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
get: async (endpoint, data = {}) => {
|
||||||
|
try {
|
||||||
|
const response = await instance.get(endpoint, { params: data });
|
||||||
|
return Promise.resolve(response);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
post: async (endpoint, data, options = {}) => {
|
||||||
|
try {
|
||||||
|
const response = await instance.post(endpoint, data, options);
|
||||||
|
return Promise.resolve(response);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
patch: async (endpoint, data) => {
|
||||||
|
try {
|
||||||
|
const response = await instance.patch(endpoint, data);
|
||||||
|
return Promise.resolve(response);
|
||||||
|
} catch (error) {
|
||||||
|
return handleError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
|
|
@ -7,6 +7,8 @@ const theme = {
|
||||||
primaryDark: '#F48600',
|
primaryDark: '#F48600',
|
||||||
primaryLight: '#F4BB60',
|
primaryLight: '#F4BB60',
|
||||||
primaryBackground: '#FEF2DD',
|
primaryBackground: '#FEF2DD',
|
||||||
|
error: '#D32F2F',
|
||||||
|
loading: '#DDDDDD',
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
mode: 'dark',
|
mode: 'dark',
|
||||||
|
|
@ -16,6 +18,8 @@ const theme = {
|
||||||
primaryDark: '#F4BB60',
|
primaryDark: '#F4BB60',
|
||||||
primaryLight: '#F48600',
|
primaryLight: '#F48600',
|
||||||
primaryBackground: '#30240F',
|
primaryBackground: '#30240F',
|
||||||
|
error: '#E53935',
|
||||||
|
loading: '#444444',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2614,6 +2614,13 @@ axe-core@^4.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224"
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.1.2.tgz#7cf783331320098bfbef620df3b3c770147bc224"
|
||||||
integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
|
integrity sha512-V+Nq70NxKhYt89ArVcaNL9FDryB3vQOd+BFXZIfO3RP6rwtj+2yqqqdHEkacutglPaZLkJeuXKCjCJDMGPtPqg==
|
||||||
|
|
||||||
|
axios@^0.21.1:
|
||||||
|
version "0.21.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.1.tgz#22563481962f4d6bde9a76d516ef0e5d3c09b2b8"
|
||||||
|
integrity sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "^1.10.0"
|
||||||
|
|
||||||
axobject-query@^2.2.0:
|
axobject-query@^2.2.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
|
||||||
|
|
@ -5131,6 +5138,11 @@ follow-redirects@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147"
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147"
|
||||||
integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==
|
integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==
|
||||||
|
|
||||||
|
follow-redirects@^1.10.0:
|
||||||
|
version "1.13.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
|
||||||
|
integrity sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==
|
||||||
|
|
||||||
for-in@^1.0.2:
|
for-in@^1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue