commit
2abdece66f
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
/graphics
|
/graphics
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ const datastore = new Datastore({
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
req.datastore = datastore;
|
req.datastore = datastore;
|
||||||
|
req.types = {
|
||||||
|
event: process.env.NODE_ENV === 'production' ? 'Event' : 'DevEvent',
|
||||||
|
person: process.env.NODE_ENV === 'production' ? 'Person' : 'DevPerson',
|
||||||
|
};
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
app.options('*', cors(corsOptions));
|
app.options('*', cors(corsOptions));
|
||||||
|
|
@ -43,5 +47,5 @@ app.post('/event/:eventId/people/:personName', login);
|
||||||
app.patch('/event/:eventId/people/:personName', updatePerson);
|
app.patch('/event/:eventId/people/:personName', updatePerson);
|
||||||
|
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`Crabfit API listening at http://localhost:${port}`)
|
console.log(`Crabfit API listening at http://localhost:${port} in ${process.env.NODE_ENV === 'production' ? 'prod' : 'dev'} mode`)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "crabfit-backend",
|
"name": "crabfit-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "API for Crabfit",
|
"description": "API for Crabfit",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "Ben Grant",
|
"author": "Ben Grant",
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ module.exports = async (req, res) => {
|
||||||
const currentTime = dayjs().unix();
|
const currentTime = dayjs().unix();
|
||||||
|
|
||||||
const entity = {
|
const entity = {
|
||||||
key: req.datastore.key(['Event', eventId]),
|
key: req.datastore.key([req.types.event, eventId]),
|
||||||
data: {
|
data: {
|
||||||
name: name,
|
name: name,
|
||||||
created: currentTime,
|
created: currentTime,
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ module.exports = async (req, res) => {
|
||||||
const { person } = req.body;
|
const { person } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const event = (await req.datastore.get(req.datastore.key(['Event', eventId])))[0];
|
const event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0];
|
||||||
const query = req.datastore.createQuery('Person')
|
const query = req.datastore.createQuery(req.types.person)
|
||||||
.filter('eventId', eventId)
|
.filter('eventId', eventId)
|
||||||
.filter('name', person.name);
|
.filter('name', person.name);
|
||||||
let personResult = (await req.datastore.runQuery(query))[0][0];
|
let personResult = (await req.datastore.runQuery(query))[0][0];
|
||||||
|
|
@ -23,7 +23,7 @@ module.exports = async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const entity = {
|
const entity = {
|
||||||
key: req.datastore.key('Person'),
|
key: req.datastore.key(req.types.person),
|
||||||
data: {
|
data: {
|
||||||
name: person.name.trim(),
|
name: person.name.trim(),
|
||||||
password: hash,
|
password: hash,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ module.exports = async (req, res) => {
|
||||||
const { eventId } = req.params;
|
const { eventId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const event = (await req.datastore.get(req.datastore.key(['Event', eventId])))[0];
|
const event = (await req.datastore.get(req.datastore.key([req.types.event, eventId])))[0];
|
||||||
|
|
||||||
if (event) {
|
if (event) {
|
||||||
res.send({
|
res.send({
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ module.exports = async (req, res) => {
|
||||||
const { eventId } = req.params;
|
const { eventId } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query = req.datastore.createQuery('Person').filter('eventId', eventId);
|
const query = req.datastore.createQuery(req.types.person).filter('eventId', eventId);
|
||||||
let people = (await req.datastore.runQuery(query))[0];
|
let people = (await req.datastore.runQuery(query))[0];
|
||||||
people = people.map(person => ({
|
people = people.map(person => ({
|
||||||
name: person.name,
|
name: person.name,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ module.exports = async (req, res) => {
|
||||||
const { person } = req.body;
|
const { person } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query = req.datastore.createQuery('Person')
|
const query = req.datastore.createQuery(req.types.person)
|
||||||
.filter('eventId', eventId)
|
.filter('eventId', eventId)
|
||||||
.filter('name', personName);
|
.filter('name', personName);
|
||||||
let personResult = (await req.datastore.runQuery(query))[0][0];
|
let personResult = (await req.datastore.runQuery(query))[0][0];
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ module.exports = async (req, res) => {
|
||||||
let personCount = null;
|
let personCount = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const eventQuery = req.datastore.createQuery(['__Stat_Kind__']).filter('kind_name', 'Event');
|
const eventQuery = req.datastore.createQuery(['__Stat_Kind__']).filter('kind_name', req.types.event);
|
||||||
const personQuery = req.datastore.createQuery(['__Stat_Kind__']).filter('kind_name', 'Person');
|
const personQuery = req.datastore.createQuery(['__Stat_Kind__']).filter('kind_name', req.types.person);
|
||||||
|
|
||||||
eventCount = (await req.datastore.runQuery(eventQuery))[0][0].count;
|
eventCount = (await req.datastore.runQuery(eventQuery))[0][0].count;
|
||||||
personCount = (await req.datastore.runQuery(personQuery))[0][0].count;
|
personCount = (await req.datastore.runQuery(personQuery))[0][0].count;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ module.exports = async (req, res) => {
|
||||||
const { person } = req.body;
|
const { person } = req.body;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query = req.datastore.createQuery('Person')
|
const query = req.datastore.createQuery(req.types.person)
|
||||||
.filter('eventId', eventId)
|
.filter('eventId', eventId)
|
||||||
.filter('name', personName);
|
.filter('name', personName);
|
||||||
let personResult = (await req.datastore.runQuery(query))[0][0];
|
let personResult = (await req.datastore.runQuery(query))[0][0];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "crabfit-frontend",
|
"name": "crabfit-frontend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.1.5",
|
"@emotion/react": "^11.1.5",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, Suspense, lazy } from 'react';
|
import { useState, useEffect, Suspense, lazy } from 'react';
|
||||||
import {
|
import {
|
||||||
BrowserRouter,
|
BrowserRouter,
|
||||||
Switch,
|
Switch,
|
||||||
|
|
@ -8,16 +8,22 @@ import { ThemeProvider, Global } from '@emotion/react';
|
||||||
|
|
||||||
import { Settings, Loading } from 'components';
|
import { Settings, Loading } from 'components';
|
||||||
|
|
||||||
|
import { useSettingsStore } from 'stores';
|
||||||
import theme from 'theme';
|
import theme from 'theme';
|
||||||
|
|
||||||
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 App = () => {
|
const App = () => {
|
||||||
|
const colortheme = useSettingsStore(state => state.theme);
|
||||||
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
const darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
const [isDark, setIsDark] = useState(darkQuery.matches);
|
const [isDark, setIsDark] = useState(darkQuery.matches);
|
||||||
|
|
||||||
darkQuery.addListener(e => setIsDark(e.matches));
|
darkQuery.addListener(e => colortheme === 'System' && setIsDark(e.matches));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsDark(colortheme === 'System' ? darkQuery.matches : colortheme === 'Dark');
|
||||||
|
}, [colortheme, darkQuery.matches]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Button } from 'components';
|
||||||
|
|
||||||
const Donate = () => (
|
const Donate = () => (
|
||||||
<div style={{ marginTop: 6, marginLeft: 12 }}>
|
<div style={{ marginTop: 6, marginLeft: 12 }}>
|
||||||
<a href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank" rel="noreferrer">
|
<a onClick={() => gtag('event', 'donate', { 'event_category': 'donate' })} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation¤cy_code=AUD" target="_blank" rel="noreferrer">
|
||||||
<Button
|
<Button
|
||||||
buttonHeight="30px"
|
buttonHeight="30px"
|
||||||
buttonWidth="90px"
|
buttonWidth="90px"
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,15 @@ const Settings = () => {
|
||||||
value={store.timeFormat}
|
value={store.timeFormat}
|
||||||
onChange={value => store.setTimeFormat(value)}
|
onChange={value => store.setTimeFormat(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ToggleField
|
||||||
|
label="Theme"
|
||||||
|
name="theme"
|
||||||
|
id="theme"
|
||||||
|
options={['System', 'Light', 'Dark']}
|
||||||
|
value={store.theme}
|
||||||
|
onChange={value => store.setTheme(value)}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ export const Modal = styled.div`
|
||||||
top: 70px;
|
top: 70px;
|
||||||
right: 12px;
|
right: 12px;
|
||||||
background-color: ${props => props.theme.background};
|
background-color: ${props => props.theme.background};
|
||||||
border: 1px solid ${props => props.theme.primaryBackground};
|
${props => props.theme.mode === 'dark' && `
|
||||||
|
border: 1px solid ${props.theme.primaryBackground};
|
||||||
|
`}
|
||||||
z-index: 150;
|
z-index: 150;
|
||||||
padding: 10px 18px;
|
padding: 10px 18px;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ const times = {
|
||||||
'21',
|
'21',
|
||||||
'22',
|
'22',
|
||||||
'23',
|
'23',
|
||||||
'0',
|
'24',
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -124,6 +124,7 @@ const TimeRangeField = ({
|
||||||
<Handle
|
<Handle
|
||||||
value={start}
|
value={start}
|
||||||
label={times[timeFormat][start]}
|
label={times[timeFormat][start]}
|
||||||
|
extraPadding={end - start === 1 ? 'padding-right: 20px;' : (start - end === 1 ? 'padding-left: 20px;' : '')}
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
isStartMoving.current = true;
|
isStartMoving.current = true;
|
||||||
|
|
@ -146,6 +147,7 @@ const TimeRangeField = ({
|
||||||
<Handle
|
<Handle
|
||||||
value={end}
|
value={end}
|
||||||
label={times[timeFormat][end]}
|
label={times[timeFormat][end]}
|
||||||
|
extraPadding={end - start === 1 ? 'padding-left: 20px;' : (start - end === 1 ? 'padding-right: 20px;' : '')}
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
isEndMoving.current = true;
|
isEndMoving.current = true;
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ export const Handle = styled.div`
|
||||||
text-align: center;
|
text-align: center;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
|
${props => props.extraPadding}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ const Event = (props) => {
|
||||||
const [min, setMin] = useState(0);
|
const [min, setMin] = useState(0);
|
||||||
const [max, setMax] = useState(0);
|
const [max, setMax] = useState(0);
|
||||||
|
|
||||||
|
const [copied, setCopied] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchEvent = async () => {
|
const fetchEvent = async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -255,6 +257,9 @@ const Event = (props) => {
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoginLoading(false);
|
setIsLoginLoading(false);
|
||||||
|
gtag('event', 'login', {
|
||||||
|
'event_category': 'event',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -272,10 +277,22 @@ const Event = (props) => {
|
||||||
{(!!event || isLoading) ? (
|
{(!!event || isLoading) ? (
|
||||||
<>
|
<>
|
||||||
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
<EventName isLoading={isLoading}>{event?.name}</EventName>
|
||||||
<ShareInfo>https://crab.fit/{id}</ShareInfo>
|
<ShareInfo
|
||||||
|
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${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/${id}`}</ShareInfo>
|
||||||
<ShareInfo isLoading={isLoading}>
|
<ShareInfo isLoading={isLoading}>
|
||||||
{!!event?.name &&
|
{!!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>.</>
|
<>Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(`Scheduling ${event?.name}`)}&body=${encodeURIComponent(`Visit this link to enter your availabilities: https://crab.fit/${id}`)}`}>email</a>.</>
|
||||||
}
|
}
|
||||||
</ShareInfo>
|
</ShareInfo>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,14 @@ export const ShareInfo = styled.p`
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
||||||
|
${props => props.onClick && `
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${props.theme.primaryDark};
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Tabs = styled.div`
|
export const Tabs = styled.div`
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,9 @@ const Home = () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
push(`/${response.data.id}`);
|
push(`/${response.data.id}`);
|
||||||
|
gtag('event', 'create_event', {
|
||||||
|
'event_category': 'home',
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError('An error ocurred while creating the event. Please try again later.');
|
setError('An error ocurred while creating the event. Please try again later.');
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,11 @@ export const useSettingsStore = create(persist(
|
||||||
set => ({
|
set => ({
|
||||||
weekStart: 0,
|
weekStart: 0,
|
||||||
timeFormat: '12h',
|
timeFormat: '12h',
|
||||||
|
theme: 'System',
|
||||||
|
|
||||||
setWeekStart: weekStart => set({ weekStart }),
|
setWeekStart: weekStart => set({ weekStart }),
|
||||||
setTimeFormat: timeFormat => set({ timeFormat }),
|
setTimeFormat: timeFormat => set({ timeFormat }),
|
||||||
|
setTheme: theme => set({ theme }),
|
||||||
}),
|
}),
|
||||||
{ name: 'crabfit-settings' },
|
{ name: 'crabfit-settings' },
|
||||||
));
|
));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue