Ready for deployment
This commit is contained in:
parent
76a36ed35f
commit
f9c216ad00
12
README.md
12
README.md
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue