Ready for deployment

This commit is contained in:
Ben Grant 2021-03-05 05:23:20 +11:00
parent 76a36ed35f
commit f9c216ad00
10 changed files with 200 additions and 147 deletions

View file

@ -1,4 +1,4 @@
# Crabfit # Crabfit <img width="100" align="right" src="crabfit-frontend/src/res/logo.svg" alt="avatar">
Align your schedules to find the perfect time that works for everyone. Align your schedules to find the perfect time that works for everyone.
@ -8,3 +8,13 @@ Align your schedules to find the perfect time that works for everyone.
2. Run `yarn` in both folders. 2. Run `yarn` in both folders.
3. Run `node index.js` in the backend folder to start the API. 3. Run `node index.js` in the backend folder to start the API.
4. Run `yarn start` in the frontend folder to start the front end. 4. Run `yarn start` in the frontend folder to start the front end.
## Deploy
### Frontend
1. In the frontend folder `cd crabfit-frontend`
2. Run `./deploy.sh` to compile and deploy.
### Backend
1. Deploy the backend `cd crabfit-backend && gcloud app deploy --project=crabfit`
2. Deploy the endpoints service `cd crabfit-backend && gcloud endpoints services deploy swagger.yaml`

View file

@ -22,7 +22,7 @@ const datastore = new Datastore({
}); });
app.use(cors({ app.use(cors({
origin: 'http://localhost:3000', origin: process.env.NODE_ENV === 'production' ? 'https://crab.fit' : 'http://localhost:3000',
})); }));
app.use(express.json()); app.use(express.json());
app.use((req, res, next) => { app.use((req, res, next) => {

View file

@ -1,23 +1,31 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8">
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="theme-color" content="#F79E00" /> <meta name="theme-color" content="#F79E00">
<meta
name="keywords"
content="crab, fit, crabfit, schedule, availability, availabilities, when2meet, doodle, meet, plan, time, timezone"
>
<meta <meta
name="description" name="description"
content="Crab Fit your schedules together to find a time that works for everyone!" content="Enter your availability to find a time that works for everyone!"
/> >
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="stylesheet" href="%PUBLIC_URL%/index.css" /> <meta property="og:title" content="Crab Fit">
<meta property="og:description" content="Enter your availability to find a time that works for everyone!">
<meta property="og:url" content="https://crab.fit">
<link rel="stylesheet" href="%PUBLIC_URL%/index.css">
<title>Crab Fit</title> <title>Crab Fit</title>
</head> </head>
<body> <body>
<noscript>Crab Fit doesn't work without Javascript. Enable it or try a different browser.</noscript> <noscript><h1>🦀 Crab Fit doesn't work without Javascript 🏋️</h1><p>Enable Javascript or try a different browser.<p></noscript>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View file

@ -22,7 +22,7 @@ const App = () => {
return ( return (
<BrowserRouter> <BrowserRouter>
<ThemeProvider theme={theme[isDark ? 'dark' : 'light']}> <ThemeProvider theme={theme[isDark ? 'dark' : 'light']}>
<button onClick={() => setIsDark(!isDark)} style={{ position: 'absolute', top: 0, left: 0 }}>{isDark ? 'dark' : 'light'}</button> {process.env.NODE_ENV !== 'production' && <button onClick={() => setIsDark(!isDark)} style={{ position: 'absolute', top: 0, left: 0 }}>{isDark ? 'dark' : 'light'}</button>}
<Global <Global
styles={theme => ({ styles={theme => ({
html: { html: {
@ -38,6 +38,25 @@ const App = () => {
a: { a: {
color: theme.primary, color: theme.primary,
}, },
'*::-webkit-scrollbar': {
width: 16,
height: 16,
},
'*::-webkit-scrollbar-track': {
background: `${theme.primaryBackground}`,
},
'*::-webkit-scrollbar-thumb': {
borderRadius: 100,
border: `4px solid ${theme.primaryBackground}`,
width: 12,
background: `${theme.primaryLight}AA`,
},
'*::-webkit-scrollbar-thumb:hover': {
background: `${theme.primaryLight}CC`,
},
'*::-webkit-scrollbar-thumb:active': {
background: `${theme.primaryLight}`,
},
})} })}
/> />
<Switch> <Switch>

View file

@ -11,6 +11,7 @@ const SelectField = ({
id, id,
options = [], options = [],
inline = false, inline = false,
defaultOption,
register, register,
...props ...props
}) => ( }) => (
@ -23,7 +24,7 @@ const SelectField = ({
ref={register} ref={register}
{...props} {...props}
> >
<option value="">Select...</option> {defaultOption && <option value="">{defaultOption}</option>}
{options.map((value, i) => {options.map((value, i) =>
<option key={i} value={value}>{value}</option> <option key={i} value={value}>{value}</option>
)} )}

View file

@ -68,10 +68,10 @@ const Event = (props) => {
setEvent(response.data); setEvent(response.data);
document.title = `${response.data.name} | Crab Fit`; document.title = `${response.data.name} | Crab Fit`;
setIsLoading(false);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
//TODO: 404 } finally {
setIsLoading(false);
} }
}; };
@ -249,137 +249,150 @@ const Event = (props) => {
<Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>Create your own</Center> <Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>Create your own</Center>
</Link> </Link>
<EventName isLoading={isLoading}>{event?.name}</EventName> {(!!event || isLoading) ? (
<ShareInfo>https://crab.fit/{id}</ShareInfo> <>
<ShareInfo isLoading={isLoading}> <EventName isLoading={isLoading}>{event?.name}</EventName>
{!!event?.name && <ShareInfo>https://crab.fit/{id}</ShareInfo>
<>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 isLoading={isLoading}>
} {!!event?.name &&
</ShareInfo> <>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>
</>
) : (
<div style={{ margin: '100px 0' }}>
<EventName>Event not found</EventName>
<ShareInfo>Check that the url you entered is correct.</ShareInfo>
</div>
)}
</StyledMain> </StyledMain>
<LoginSection id="login"> {(!!event || isLoading) && (
<StyledMain> <>
{user ? ( <LoginSection id="login">
<h2>Signed in as {user.name}</h2> <StyledMain>
{user ? (
<h2>Signed in as {user.name}</h2>
) : (
<>
<h2>Sign in to add your availability</h2>
<LoginForm onSubmit={handleSubmit(onSubmit)}>
<TextField
label="Your name"
type="text"
name="name"
id="name"
inline
required
register={register}
/>
<TextField
label="Password (optional)"
type="password"
name="password"
id="password"
inline
register={register}
/>
<Button
type="submit"
isLoading={isLoginLoading}
disabled={isLoginLoading || isLoading}
>Login</Button>
</LoginForm>
{error && <Error onClose={() => setError(null)}>{error}</Error>}
<Info>These details are only for this event. Use a password to prevent others from changing your availability.</Info>
</>
)}
<SelectField
label="Your time zone"
name="timezone"
id="timezone"
inline
value={timezone}
onChange={event => setTimezone(event.currentTarget.value)}
options={timezones}
/>
</StyledMain>
</LoginSection>
<StyledMain>
<Tabs>
<Tab
href="#you"
onClick={e => {
e.preventDefault();
if (user) {
setTab('you');
}
}}
selected={tab === 'you'}
disabled={!user}
title={user ? '' : 'Login to set your availability'}
>Your availability</Tab>
<Tab
href="#group"
onClick={e => {
e.preventDefault();
setTab('group');
}}
selected={tab === 'group'}
>Group availability</Tab>
</Tabs>
</StyledMain>
{tab === 'group' ? (
<section id="group">
<StyledMain>
<Legend
min={min}
max={max}
total={people.filter(p => p.availability.length > 0).length}
/>
<Center>Hover or tap the calendar below to see who is available</Center>
</StyledMain>
<AvailabilityViewer
times={times}
timeLabels={timeLabels}
dates={dates}
people={people.filter(p => p.availability.length > 0)}
min={min}
max={max}
/>
</section>
) : ( ) : (
<> <section id="you">
<h2>Sign in to add your availability</h2> <StyledMain>
<LoginForm onSubmit={handleSubmit(onSubmit)}> <Center>Click and drag the calendar below to set your availabilities</Center>
<TextField </StyledMain>
label="Your name" <AvailabilityEditor
type="text" times={times}
name="name" timeLabels={timeLabels}
id="name" dates={dates}
inline value={user.availability}
required onChange={async availability => {
register={register} const oldAvailability = [...user.availability];
/> const utcAvailability = availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'));
setUser({ ...user, availability });
<TextField try {
label="Password (optional)" await api.patch(`/event/${id}/people/${user.name}`, {
type="password" person: {
name="password" password,
id="password" availability: utcAvailability,
inline },
register={register} });
/> } catch (e) {
console.log(e);
<Button setUser({ ...user, oldAvailability });
type="submit" }
isLoading={isLoginLoading} }}
disabled={isLoginLoading || isLoading} />
>Login</Button> </section>
</LoginForm>
{error && <Error onClose={() => setError(null)}>{error}</Error>}
<Info>These details are only for this event. Use a password to prevent others from changing your availability.</Info>
</>
)} )}
</>
<SelectField
label="Your time zone"
name="timezone"
id="timezone"
inline
value={timezone}
onChange={event => setTimezone(event.currentTarget.value)}
options={timezones}
/>
</StyledMain>
</LoginSection>
<StyledMain>
<Tabs>
<Tab
href="#you"
onClick={e => {
e.preventDefault();
if (user) {
setTab('you');
}
}}
selected={tab === 'you'}
disabled={!user}
title={user ? '' : 'Login to set your availability'}
>Your availability</Tab>
<Tab
href="#group"
onClick={e => {
e.preventDefault();
setTab('group');
}}
selected={tab === 'group'}
>Group availability</Tab>
</Tabs>
</StyledMain>
{tab === 'group' ? (
<section id="group">
<StyledMain>
<Legend
min={min}
max={max}
total={people.filter(p => p.availability.length > 0).length}
/>
<Center>Hover or tap the calendar below to see who is available</Center>
</StyledMain>
<AvailabilityViewer
times={times}
timeLabels={timeLabels}
dates={dates}
people={people.filter(p => p.availability.length > 0)}
min={min}
max={max}
/>
</section>
) : (
<section id="you">
<StyledMain>
<Center>Click and drag the calendar below to set your availabilities</Center>
</StyledMain>
<AvailabilityEditor
times={times}
timeLabels={timeLabels}
dates={dates}
value={user.availability}
onChange={async availability => {
const oldAvailability = [...user.availability];
const utcAvailability = availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'));
setUser({ ...user, availability });
try {
await api.patch(`/event/${id}/people/${user.name}`, {
person: {
password,
availability: utcAvailability,
},
});
} catch (e) {
console.log(e);
setUser({ ...user, oldAvailability });
}
}}
/>
</section>
)} )}
<Footer id="donate"> <Footer id="donate">

View file

@ -24,7 +24,7 @@ export const Title = styled.span`
display: block; display: block;
font-size: 2rem; font-size: 2rem;
color: ${props => props.theme.primary}; color: ${props => props.theme.primary};
font-family: 'Molot'; font-family: 'Molot', sans-serif;
font-weight: 400; font-weight: 400;
text-shadow: 0 2px 0 ${props => props.theme.primaryDark}; text-shadow: 0 2px 0 ${props => props.theme.primaryDark};
line-height: 1em; line-height: 1em;

View file

@ -69,6 +69,7 @@ const Home = () => {
}; };
fetch(); fetch();
document.title = 'Crab Fit';
}, []); }, []);
const onSubmit = async data => { const onSubmit = async data => {
@ -176,6 +177,7 @@ const Home = () => {
register={register} register={register}
options={timezones} options={timezones}
required required
defaultOption="Select..."
/> />
{error && ( {error && (

View file

@ -14,7 +14,7 @@ export const TitleSmall = styled.span`
margin: 0; margin: 0;
font-size: 3rem; font-size: 3rem;
text-align: center; text-align: center;
font-family: 'Samurai Bob'; font-family: 'Samurai Bob', sans-serif;
font-weight: 400; font-weight: 400;
color: ${props => props.theme.primaryDark}; color: ${props => props.theme.primaryDark};
line-height: 1em; line-height: 1em;
@ -25,7 +25,7 @@ export const TitleLarge = styled.h1`
font-size: 4rem; font-size: 4rem;
text-align: center; text-align: center;
color: ${props => props.theme.primary}; color: ${props => props.theme.primary};
font-family: 'Molot'; font-family: 'Molot', sans-serif;
font-weight: 400; font-weight: 400;
text-shadow: 0 4px 0 ${props => props.theme.primaryDark}; text-shadow: 0 4px 0 ${props => props.theme.primaryDark};
line-height: 1em; line-height: 1em;

View file

@ -1,7 +1,7 @@
import axios from 'axios'; import axios from 'axios';
export const instance = axios.create({ export const instance = axios.create({
baseURL: 'http://localhost:8080', baseURL: process.env.NODE_ENV === 'production' ? 'https://api-dot-crabfit.uc.r.appspot.com' : 'http://localhost:8080',
timeout: 1000 * 300, timeout: 1000 * 300,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',