Tabs -> spaces

I have become my own worst enemy
This commit is contained in:
Ben Grant 2021-06-19 12:04:52 +10:00
parent e94559c4f6
commit fdb7f0ef67
49 changed files with 2424 additions and 2424 deletions

View file

@ -9,21 +9,21 @@ import timezone from 'dayjs/plugin/timezone';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import {
TextField,
CalendarField,
TimeRangeField,
SelectField,
Button,
Error,
TextField,
CalendarField,
TimeRangeField,
SelectField,
Button,
Error,
Recents,
Footer,
} from 'components';
import {
StyledMain,
CreateForm,
TitleSmall,
TitleLarge,
StyledMain,
CreateForm,
TitleSmall,
TitleLarge,
P,
OfflineMessage,
ShareInfo,
@ -39,14 +39,14 @@ dayjs.extend(timezone);
dayjs.extend(customParseFormat);
const Create = ({ offline }) => {
const { register, handleSubmit, setValue } = useForm({
defaultValues: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const [createdEvent, setCreatedEvent] = useState(null);
const { register, handleSubmit, setValue } = 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 [showFooter, setShowFooter] = useState(true);
@ -55,11 +55,11 @@ const Create = ({ offline }) => {
const addRecent = useRecentsStore(state => state.addRecent);
useEffect(() => {
useEffect(() => {
if (window.self === window.top) {
push('/');
}
document.title = 'Create a Crab Fit';
document.title = 'Create a Crab Fit';
if (window.parent) {
window.parent.postMessage('crabfit-create', '*');
@ -71,67 +71,67 @@ const Create = ({ offline }) => {
once: true
});
}
}, [push]);
}, [push]);
const onSubmit = async data => {
setIsLoading(true);
setError(null);
try {
const { start, end } = JSON.parse(data.times);
const dates = JSON.parse(data.dates);
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(t('home:form.errors.no_dates'));
}
if (dates.length === 0) {
return setError(t('home:form.errors.no_dates'));
}
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
if (start === end) {
return setError(t('home:form.errors.same_times'));
}
if (start === end) {
return setError(t('home:form.errors.same_times'));
}
let times = dates.reduce((times, date) => {
let day = [];
for (let i = start; i < (start > end ? 24 : end); i++) {
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')
);
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 (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')
);
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];
}, []);
}
}
return [...times, ...day];
}, []);
if (times.length === 0) {
return setError(t('home:form.errors.no_time'));
}
if (times.length === 0) {
return setError(t('home:form.errors.no_time'));
}
const response = await api.post('/event', {
event: {
name: data.name,
times: times,
const response = await api.post('/event', {
event: {
name: data.name,
times: times,
timezone: data.timezone,
},
});
},
});
setCreatedEvent(response.data);
addRecent({
id: response.data.id,
@ -141,19 +141,19 @@ const Create = ({ offline }) => {
gtag('event', 'create_event', {
'event_category': 'create',
});
} catch (e) {
setError(t('home:form.errors.unknown'));
console.error(e);
} finally {
setIsLoading(false);
}
};
} catch (e) {
setError(t('home:form.errors.unknown'));
console.error(e);
} finally {
setIsLoading(false);
}
};
return (
<>
<StyledMain>
<TitleSmall>{t('home:create')}</TitleSmall>
<TitleLarge>CRAB FIT</TitleLarge>
return (
<>
<StyledMain>
<TitleSmall>{t('home:create')}</TitleSmall>
<TitleLarge>CRAB FIT</TitleLarge>
</StyledMain>
{createdEvent ? (
@ -173,10 +173,10 @@ const Create = ({ offline }) => {
}
title={!!navigator.clipboard ? t('event:nav.title') : ''}
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
<ShareInfo>
<ShareInfo>
{/* eslint-disable-next-line */}
<Trans i18nKey="event:nav.shareinfo_alt">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(t('event:nav.email_subject', { event_name: createdEvent?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${createdEvent?.id}`)}`} target="_blank">email</a>.</Trans>
</ShareInfo>
<Trans i18nKey="event:nav.shareinfo_alt">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(t('event:nav.email_subject', { event_name: createdEvent?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${createdEvent?.id}`)}`} target="_blank">email</a>.</Trans>
</ShareInfo>
{showFooter && <Footer small />}
</OfflineMessage>
</StyledMain>
@ -191,52 +191,52 @@ const Create = ({ offline }) => {
<P>{t('home:offline')}</P>
</OfflineMessage>
) : (
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
<TextField
label={t('home:form.name.label')}
subLabel={t('home:form.name.sublabel')}
type="text"
id="name"
{...register('name')}
/>
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
<TextField
label={t('home:form.name.label')}
subLabel={t('home:form.name.sublabel')}
type="text"
id="name"
{...register('name')}
/>
<CalendarField
label={t('home:form.dates.label')}
subLabel={t('home:form.dates.sublabel')}
id="dates"
required
<CalendarField
label={t('home:form.dates.label')}
subLabel={t('home:form.dates.sublabel')}
id="dates"
required
setValue={setValue}
{...register('dates')}
/>
/>
<TimeRangeField
label={t('home:form.times.label')}
subLabel={t('home:form.times.sublabel')}
id="times"
required
<TimeRangeField
label={t('home:form.times.label')}
subLabel={t('home:form.times.sublabel')}
id="times"
required
setValue={setValue}
{...register('times')}
/>
/>
<SelectField
label={t('home:form.timezone.label')}
id="timezone"
options={timezones}
required
<SelectField
label={t('home:form.timezone.label')}
id="timezone"
options={timezones}
required
{...register('timezone')}
defaultOption={t('home:form.timezone.defaultOption')}
/>
defaultOption={t('home:form.timezone.defaultOption')}
/>
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Button type="submit" isLoading={isLoading} disabled={isLoading} style={{ width: '100%' }}>{t('home:form.button')}</Button>
</CreateForm>
<Button type="submit" isLoading={isLoading} disabled={isLoading} style={{ width: '100%' }}>{t('home:form.button')}</Button>
</CreateForm>
)}
</StyledMain>
</StyledMain>
</>
)}
</>
);
</>
);
};
export default Create;

View file

@ -1,9 +1,9 @@
import styled from '@emotion/styled';
export const StyledMain = styled.div`
width: 600px;
margin: 10px auto;
max-width: calc(100% - 30px);
width: 600px;
margin: 10px auto;
max-width: calc(100% - 30px);
`;
export const CreateForm = styled.form`
@ -11,43 +11,43 @@ export const CreateForm = styled.form`
`;
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;
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;
text-transform: uppercase;
`;
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 3px 0 ${props => props.theme.primaryDark};
line-height: 1em;
margin: 0;
font-size: 2rem;
text-align: center;
color: ${props => props.theme.primary};
font-family: 'Molot', sans-serif;
font-weight: 400;
text-shadow: 0 3px 0 ${props => props.theme.primaryDark};
line-height: 1em;
text-transform: uppercase;
`;
export const P = styled.p`
font-weight: 500;
line-height: 1.6em;
font-weight: 500;
line-height: 1.6em;
`;
export const OfflineMessage = styled.div`
text-align: center;
text-align: center;
margin: 50px 0 20px;
`;
export const ShareInfo = styled.p`
margin: 6px 0;
text-align: center;
font-size: 15px;
margin: 6px 0;
text-align: center;
font-size: 15px;
padding: 10px 0;
${props => props.onClick && `

View file

@ -9,27 +9,27 @@ import customParseFormat from 'dayjs/plugin/customParseFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import {
Footer,
TextField,
SelectField,
Button,
AvailabilityViewer,
AvailabilityEditor,
Error,
Footer,
TextField,
SelectField,
Button,
AvailabilityViewer,
AvailabilityEditor,
Error,
Logo,
} from 'components';
import { StyledMain } from '../Home/homeStyle';
import {
EventName,
EventName,
EventDate,
LoginForm,
LoginSection,
Info,
ShareInfo,
Tabs,
Tab,
LoginForm,
LoginSection,
Info,
ShareInfo,
Tabs,
Tab,
} from './eventStyle';
import api from 'services';
@ -51,90 +51,90 @@ const Event = (props) => {
const { t } = useTranslation(['common', 'event']);
const { register, handleSubmit, setFocus, reset } = useForm();
const { id } = props.match.params;
const { register, handleSubmit, setFocus, reset } = useForm();
const { id } = props.match.params;
const { offline } = props;
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
const [user, setUser] = useState(null);
const [password, setPassword] = useState(null);
const [tab, setTab] = useState(user ? 'you' : 'group');
const [isLoading, setIsLoading] = useState(true);
const [isLoginLoading, setIsLoginLoading] = useState(false);
const [error, setError] = useState(null);
const [event, setEvent] = useState(null);
const [people, setPeople] = useState([]);
const [timezone, setTimezone] = useState(Intl.DateTimeFormat().resolvedOptions().timeZone);
const [user, setUser] = useState(null);
const [password, setPassword] = useState(null);
const [tab, setTab] = useState(user ? 'you' : 'group');
const [isLoading, setIsLoading] = useState(true);
const [isLoginLoading, setIsLoginLoading] = useState(false);
const [error, setError] = useState(null);
const [event, setEvent] = useState(null);
const [people, setPeople] = useState([]);
const [times, setTimes] = useState([]);
const [timeLabels, setTimeLabels] = useState([]);
const [dates, setDates] = useState([]);
const [min, setMin] = useState(0);
const [max, setMax] = useState(0);
const [times, setTimes] = useState([]);
const [timeLabels, setTimeLabels] = useState([]);
const [dates, setDates] = useState([]);
const [min, setMin] = useState(0);
const [max, setMax] = useState(0);
const [copied, setCopied] = useState(null);
const [copied, setCopied] = useState(null);
useEffect(() => {
const fetchEvent = async () => {
try {
const response = await api.get(`/event/${id}`);
useEffect(() => {
const fetchEvent = async () => {
try {
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`;
} catch (e) {
console.error(e);
} finally {
setIsLoading(false);
}
};
document.title = `${response.data.name} | Crab Fit`;
} catch (e) {
console.error(e);
} finally {
setIsLoading(false);
}
};
fetchEvent();
}, [id, addRecent]);
fetchEvent();
}, [id, addRecent]);
useEffect(() => {
const fetchPeople = async () => {
try {
const response = await api.get(`/event/${id}/people`);
const adjustedPeople = response.data.people.map(person => ({
...person,
availability: (!!person.availability.length && person.availability[0].length === 13)
useEffect(() => {
const fetchPeople = async () => {
try {
const response = await api.get(`/event/${id}/people`);
const adjustedPeople = response.data.people.map(person => ({
...person,
availability: (!!person.availability.length && person.availability[0].length === 13)
? person.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
: person.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
}));
setPeople(adjustedPeople);
} catch (e) {
console.error(e);
}
}
}));
setPeople(adjustedPeople);
} catch (e) {
console.error(e);
}
}
if (tab === 'group') {
fetchPeople();
}
}, [tab, id, timezone]);
if (tab === 'group') {
fetchPeople();
}
}, [tab, id, timezone]);
// Convert to timezone and expand minute segments
useEffect(() => {
if (event) {
// Convert to timezone and expand minute segments
useEffect(() => {
if (event) {
const isSpecificDates = event.times[0].length === 13;
setTimes(event.times.reduce(
(allTimes, time) => {
setTimes(event.times.reduce(
(allTimes, time) => {
const date = isSpecificDates ?
dayjs(time, 'HHmm-DDMMYYYY').utc(true).tz(timezone)
: dayjs(time, 'HHmm').day(time.substring(5)).utc(true).tz(timezone);
const format = isSpecificDates ? 'HHmm-DDMMYYYY' : 'HHmm-d';
return [
...allTimes,
date.minute(0).format(format),
date.minute(15).format(format),
date.minute(30).format(format),
date.minute(45).format(format),
];
},
[]
).sort((a, b) => {
return [
...allTimes,
date.minute(0).format(format),
date.minute(15).format(format),
date.minute(30).format(format),
date.minute(45).format(format),
];
},
[]
).sort((a, b) => {
if (isSpecificDates) {
return dayjs(a, 'HHmm-DDMMYYYY').diff(dayjs(b, 'HHmm-DDMMYYYY'));
} else {
@ -142,154 +142,154 @@ const Event = (props) => {
.diff(dayjs(b, 'HHmm').day((parseInt(b.substring(5))-weekStart % 7 + 7) % 7));
}
}));
}
}, [event, timezone, weekStart]);
}
}, [event, timezone, weekStart]);
useEffect(() => {
if (!!times.length && !!people.length) {
setMin(times.reduce((min, time) => {
let total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total,
0
);
return total < min ? total : min;
},
Infinity
));
setMax(times.reduce((max, time) => {
let total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total,
0
);
return total > max ? total : max;
},
-Infinity
));
}
}, [times, people]);
useEffect(() => {
if (!!times.length && !!people.length) {
setMin(times.reduce((min, time) => {
let total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total,
0
);
return total < min ? total : min;
},
Infinity
));
setMax(times.reduce((max, time) => {
let total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total,
0
);
return total > max ? total : max;
},
-Infinity
));
}
}, [times, people]);
useEffect(() => {
if (!!times.length) {
setTimeLabels(times.reduce((labels, datetime) => {
const time = datetime.substring(0, 4);
if (labels.includes(time)) return labels;
return [...labels, time];
}, [])
.sort((a, b) => parseInt(a) - parseInt(b))
.reduce((labels, time, i, allTimes) => {
if (time.substring(2) === '30') return [...labels, { label: '', time }];
if (allTimes.length - 1 === i) return [
...labels,
{ label: '', time },
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
];
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
...labels,
{ label: '', time },
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
{ label: '', time: 'space' },
{ label: '', time: 'space' },
];
if (time.substring(2) !== '00') return [...labels, { label: '', time }];
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }];
}, []));
useEffect(() => {
if (!!times.length) {
setTimeLabels(times.reduce((labels, datetime) => {
const time = datetime.substring(0, 4);
if (labels.includes(time)) return labels;
return [...labels, time];
}, [])
.sort((a, b) => parseInt(a) - parseInt(b))
.reduce((labels, time, i, allTimes) => {
if (time.substring(2) === '30') return [...labels, { label: '', time }];
if (allTimes.length - 1 === i) return [
...labels,
{ label: '', time },
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: null }
];
if (allTimes.length - 1 > i && parseInt(allTimes[i+1].substring(0, 2))-1 > parseInt(time.substring(0, 2))) return [
...labels,
{ label: '', time },
{ label: dayjs(time, 'HHmm').add(1, 'hour').format(timeFormat === '12h' ? 'h A' : 'HH'), time: 'space' },
{ label: '', time: 'space' },
{ label: '', time: 'space' },
];
if (time.substring(2) !== '00') return [...labels, { label: '', time }];
return [...labels, { label: dayjs(time, 'HHmm').format(timeFormat === '12h' ? 'h A' : 'HH'), time }];
}, []));
setDates(times.reduce((allDates, time) => {
if (time.substring(2, 4) !== '00') return allDates;
const date = time.substring(5);
if (allDates.includes(date)) return allDates;
return [...allDates, date];
}, []));
}
}, [times, timeFormat, locale]);
setDates(times.reduce((allDates, time) => {
if (time.substring(2, 4) !== '00') return allDates;
const date = time.substring(5);
if (allDates.includes(date)) return allDates;
return [...allDates, date];
}, []));
}
}, [times, timeFormat, locale]);
useEffect(() => {
const fetchUser = async () => {
try {
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
const adjustedUser = {
...response.data,
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
useEffect(() => {
const fetchUser = async () => {
try {
const response = await api.post(`/event/${id}/people/${user.name}`, { person: { password } });
const adjustedUser = {
...response.data,
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
};
setUser(adjustedUser);
} catch (e) {
console.log(e);
}
};
};
setUser(adjustedUser);
} catch (e) {
console.log(e);
}
};
if (user) {
fetchUser();
}
// eslint-disable-next-line
}, [timezone]);
if (user) {
fetchUser();
}
// eslint-disable-next-line
}, [timezone]);
const onSubmit = async data => {
const onSubmit = async data => {
if (!data.name || data.name.length === 0) {
setFocus('name');
return setError(t('event:form.errors.name_required'));
}
setIsLoginLoading(true);
setError(null);
setIsLoginLoading(true);
setError(null);
try {
const response = await api.post(`/event/${id}/people/${data.name}`, {
person: {
password: data.password,
},
});
setPassword(data.password);
const adjustedUser = {
...response.data,
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
try {
const response = await api.post(`/event/${id}/people/${data.name}`, {
person: {
password: data.password,
},
});
setPassword(data.password);
const adjustedUser = {
...response.data,
availability: (!!response.data.availability.length && response.data.availability[0].length === 13)
? response.data.availability.map(date => dayjs(date, 'HHmm-DDMMYYYY').utc(true).tz(timezone).format('HHmm-DDMMYYYY'))
: response.data.availability.map(date => dayjs(date, 'HHmm').day(date.substring(5)).utc(true).tz(timezone).format('HHmm-d')),
};
setUser(adjustedUser);
setTab('you');
} catch (e) {
if (e.status === 401) {
setError(t('event:form.errors.password_incorrect'));
} else if (e.status === 404) {
// Create user
try {
await api.post(`/event/${id}/people`, {
person: {
name: data.name,
password: data.password,
},
});
setPassword(data.password);
setUser({
name: data.name,
availability: [],
});
setTab('you');
} catch (e) {
setError(t('event:form.errors.unknown'));
}
}
} finally {
setIsLoginLoading(false);
};
setUser(adjustedUser);
setTab('you');
} catch (e) {
if (e.status === 401) {
setError(t('event:form.errors.password_incorrect'));
} else if (e.status === 404) {
// Create user
try {
await api.post(`/event/${id}/people`, {
person: {
name: data.name,
password: data.password,
},
});
setPassword(data.password);
setUser({
name: data.name,
availability: [],
});
setTab('you');
} catch (e) {
setError(t('event:form.errors.unknown'));
}
}
} finally {
setIsLoginLoading(false);
gtag('event', 'login', {
'event_category': 'event',
});
reset();
}
};
}
};
return (
<>
<StyledMain>
<Logo />
return (
<>
<StyledMain>
<Logo />
{(!!event || isLoading) ? (
<>
<EventName isLoading={isLoading}>{event?.name}</EventName>
{(!!event || isLoading) ? (
<>
<EventName isLoading={isLoading}>{event?.name}</EventName>
<EventDate isLoading={isLoading} locale={locale} title={event?.created && dayjs.unix(event?.created).format('D MMMM, YYYY')}>{event?.created && t('common:created', { date: dayjs.unix(event?.created).fromNow() })}</EventDate>
<ShareInfo
<ShareInfo
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${id}`)
.then(() => {
setCopied(t('event:nav.copied'));
@ -302,81 +302,81 @@ const Event = (props) => {
}
title={!!navigator.clipboard ? t('event:nav.title') : ''}
>{copied ?? `https://crab.fit/${id}`}</ShareInfo>
<ShareInfo isLoading={isLoading}>
{!!event?.name &&
<Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email</a>.</Trans>
}
</ShareInfo>
</>
) : (
<ShareInfo isLoading={isLoading}>
{!!event?.name &&
<Trans i18nKey="event:nav.shareinfo">Copy the link to this page, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: event?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${id}`)}`}>email</a>.</Trans>
}
</ShareInfo>
</>
) : (
offline ? (
<div style={{ margin: '100px 0' }}>
<EventName>{t('event:offline.title')}</EventName>
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
</div>
<EventName>{t('event:offline.title')}</EventName>
<ShareInfo><Trans i18nKey="event:offline.body" /></ShareInfo>
</div>
) : (
<div style={{ margin: '100px 0' }}>
<EventName>{t('event:error.title')}</EventName>
<ShareInfo>{t('event:error.body')}</ShareInfo>
</div>
<div style={{ margin: '100px 0' }}>
<EventName>{t('event:error.title')}</EventName>
<ShareInfo>{t('event:error.body')}</ShareInfo>
</div>
)
)}
</StyledMain>
)}
</StyledMain>
{(!!event || isLoading) && (
<>
<LoginSection id="login">
<StyledMain>
{user ? (
{(!!event || isLoading) && (
<>
<LoginSection id="login">
<StyledMain>
{user ? (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '20px 0', flexWrap: 'wrap', gap: '10px' }}>
<h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2>
<h2 style={{ margin: 0 }}>{t('event:form.signed_in', { name: user.name })}</h2>
<Button small onClick={() => {
setTab('group');
setUser(null);
setPassword(null);
}}>{t('event:form.logout_button')}</Button>
</div>
) : (
<>
<h2>{t('event:form.signed_out')}</h2>
<LoginForm onSubmit={handleSubmit(onSubmit)}>
<TextField
label={t('event:form.name')}
type="text"
id="name"
inline
required
{...register('name')}
/>
) : (
<>
<h2>{t('event:form.signed_out')}</h2>
<LoginForm onSubmit={handleSubmit(onSubmit)}>
<TextField
label={t('event:form.name')}
type="text"
id="name"
inline
required
{...register('name')}
/>
<TextField
label={t('event:form.password')}
type="password"
id="password"
inline
{...register('password')}
/>
<TextField
label={t('event:form.password')}
type="password"
id="password"
inline
{...register('password')}
/>
<Button
type="submit"
isLoading={isLoginLoading}
disabled={isLoginLoading || isLoading}
>{t('event:form.button')}</Button>
</LoginForm>
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Info>{t('event:form.info')}</Info>
</>
)}
<Button
type="submit"
isLoading={isLoginLoading}
disabled={isLoginLoading || isLoading}
>{t('event:form.button')}</Button>
</LoginForm>
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Info>{t('event:form.info')}</Info>
</>
)}
<SelectField
label={t('event:form.timezone')}
name="timezone"
id="timezone"
inline
value={timezone}
onChange={event => setTimezone(event.currentTarget.value)}
options={timezones}
/>
<SelectField
label={t('event:form.timezone')}
name="timezone"
id="timezone"
inline
value={timezone}
onChange={event => setTimezone(event.currentTarget.value)}
options={timezones}
/>
{/* eslint-disable-next-line */}
{event?.timezone && event.timezone !== timezone && <p><Trans i18nKey="event:form.created_in_timezone">This event was created in the timezone <strong>{{timezone: event.timezone}}</strong>. <a href="#" onClick={e => {
e.preventDefault();
@ -395,84 +395,84 @@ const Event = (props) => {
setTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
}}>Click here</a> to use it.</Trans></p>
)}
</StyledMain>
</LoginSection>
</StyledMain>
</LoginSection>
<StyledMain>
<Tabs>
<Tab
href="#you"
onClick={e => {
e.preventDefault();
if (user) {
setTab('you');
} else {
<StyledMain>
<Tabs>
<Tab
href="#you"
onClick={e => {
e.preventDefault();
if (user) {
setTab('you');
} else {
setFocus('name');
}
}}
selected={tab === 'you'}
disabled={!user}
title={user ? '' : t('event:tabs.you_tooltip')}
>{t('event:tabs.you')}</Tab>
<Tab
href="#group"
onClick={e => {
e.preventDefault();
setTab('group');
}}
selected={tab === 'group'}
>{t('event:tabs.group')}</Tab>
</Tabs>
</StyledMain>
}}
selected={tab === 'you'}
disabled={!user}
title={user ? '' : t('event:tabs.you_tooltip')}
>{t('event:tabs.you')}</Tab>
<Tab
href="#group"
onClick={e => {
e.preventDefault();
setTab('group');
}}
selected={tab === 'group'}
>{t('event:tabs.group')}</Tab>
</Tabs>
</StyledMain>
{tab === 'group' ? (
<section id="group">
<AvailabilityViewer
times={times}
timeLabels={timeLabels}
dates={dates}
{tab === 'group' ? (
<section id="group">
<AvailabilityViewer
times={times}
timeLabels={timeLabels}
dates={dates}
isSpecificDates={!!dates.length && dates[0].length === 8}
people={people.filter(p => p.availability.length > 0)}
min={min}
max={max}
/>
</section>
) : (
<section id="you">
<AvailabilityEditor
times={times}
timeLabels={timeLabels}
dates={dates}
timezone={timezone}
people={people.filter(p => p.availability.length > 0)}
min={min}
max={max}
/>
</section>
) : (
<section id="you">
<AvailabilityEditor
times={times}
timeLabels={timeLabels}
dates={dates}
timezone={timezone}
isSpecificDates={!!dates.length && dates[0].length === 8}
value={user.availability}
onChange={async availability => {
const oldAvailability = [...user.availability];
const utcAvailability = (!!availability.length && availability[0].length === 13)
value={user.availability}
onChange={async availability => {
const oldAvailability = [...user.availability];
const utcAvailability = (!!availability.length && availability[0].length === 13)
? availability.map(date => dayjs.tz(date, 'HHmm-DDMMYYYY', timezone).utc().format('HHmm-DDMMYYYY'))
: availability.map(date => dayjs.tz(date, 'HHmm', timezone).day(date.substring(5)).utc().format('HHmm-d'));
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>
)}
</>
)}
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 />
</>
);
<Footer />
</>
);
};
export default Event;

View file

@ -1,21 +1,21 @@
import styled from '@emotion/styled';
export const EventName = styled.h1`
text-align: center;
font-weight: 800;
margin: 20px 0 5px;
text-align: center;
font-weight: 800;
margin: 20px 0 5px;
${props => props.isLoading && `
&:after {
content: '';
display: inline-block;
height: 1em;
width: 400px;
max-width: 100%;
background-color: ${props.theme.loading};
border-radius: 3px;
}
`}
${props => props.isLoading && `
&:after {
content: '';
display: inline-block;
height: 1em;
width: 400px;
max-width: 100%;
background-color: ${props.theme.loading};
border-radius: 3px;
}
`}
`;
export const EventDate = styled.span`
@ -28,63 +28,63 @@ export const EventDate = styled.span`
letter-spacing: .01em;
${props => props.isLoading && `
&:after {
content: '';
display: inline-block;
height: 1em;
width: 200px;
max-width: 100%;
background-color: ${props.theme.loading};
border-radius: 3px;
}
`}
&:after {
content: '';
display: inline-block;
height: 1em;
width: 200px;
max-width: 100%;
background-color: ${props.theme.loading};
border-radius: 3px;
}
`}
`;
export const LoginForm = styled.form`
display: grid;
grid-template-columns: 1fr 1fr auto;
align-items: flex-end;
grid-gap: 18px;
display: grid;
grid-template-columns: 1fr 1fr auto;
align-items: flex-end;
grid-gap: 18px;
@media (max-width: 500px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 400px) {
grid-template-columns: 1fr;
@media (max-width: 500px) {
grid-template-columns: 1fr 1fr;
}
@media (max-width: 400px) {
grid-template-columns: 1fr;
& div:last-child {
--btn-width: 100%;
}
}
}
`;
export const LoginSection = styled.section`
background-color: ${props => props.theme.primaryBackground};
padding: 10px 0;
background-color: ${props => props.theme.primaryBackground};
padding: 10px 0;
`;
export const Info = styled.p`
margin: 18px 0;
opacity: .6;
font-size: 12px;
margin: 18px 0;
opacity: .6;
font-size: 12px;
`;
export const ShareInfo = styled.p`
margin: 6px 0;
text-align: center;
font-size: 15px;
margin: 6px 0;
text-align: center;
font-size: 15px;
${props => props.isLoading && `
&:after {
content: '';
display: inline-block;
height: 1em;
width: 300px;
max-width: 100%;
background-color: ${props.theme.loading};
border-radius: 3px;
}
`}
${props => props.isLoading && `
&:after {
content: '';
display: inline-block;
height: 1em;
width: 300px;
max-width: 100%;
background-color: ${props.theme.loading};
border-radius: 3px;
}
`}
${props => props.onClick && `
cursor: pointer;
@ -96,33 +96,33 @@ export const ShareInfo = styled.p`
`;
export const Tabs = styled.div`
display: flex;
align-items: center;
justify-content: center;
margin: 30px 0 20px;
display: flex;
align-items: center;
justify-content: center;
margin: 30px 0 20px;
`;
export const Tab = styled.a`
user-select: none;
text-decoration: none;
display: block;
color: ${props => props.theme.text};
padding: 8px 18px;
background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary};
border-bottom: 0;
margin: 0 4px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
user-select: none;
text-decoration: none;
display: block;
color: ${props => props.theme.text};
padding: 8px 18px;
background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary};
border-bottom: 0;
margin: 0 4px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
${props => props.selected && `
color: #FFF;
background-color: ${props.theme.primary};
border-color: ${props.theme.primary};
`}
${props => props.selected && `
color: #FFF;
background-color: ${props.theme.primary};
border-color: ${props.theme.primary};
`}
${props => props.disabled && `
opacity: .5;
cursor: not-allowed;
`}
${props => props.disabled && `
opacity: .5;
cursor: not-allowed;
`}
`;

View file

@ -3,17 +3,17 @@ import { Link, useHistory } from 'react-router-dom';
import { useTranslation, Trans } from 'react-i18next';
import {
Button,
Center,
Footer,
Button,
Center,
Footer,
AvailabilityViewer,
Logo,
} from 'components';
import {
StyledMain,
AboutSection,
P,
AboutSection,
P,
} from '../Home/homeStyle';
import {
@ -26,18 +26,18 @@ const Help = () => {
const { push } = useHistory();
const { t } = useTranslation(['common', 'help']);
useEffect(() => {
document.title = t('help:name');
}, [t]);
useEffect(() => {
document.title = t('help:name');
}, [t]);
return (
<>
<StyledMain>
return (
<>
<StyledMain>
<Logo />
</StyledMain>
<StyledMain>
<h1>{t('help:name')}</h1>
<h1>{t('help:name')}</h1>
<P>{t('help:p1')}</P>
<P>{t('help:p2')}</P>
@ -80,17 +80,17 @@ const Help = () => {
min={0}
max={5}
/>
</StyledMain>
</StyledMain>
<AboutSection id="about">
<StyledMain>
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
</StyledMain>
</AboutSection>
<AboutSection id="about">
<StyledMain>
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
</StyledMain>
</AboutSection>
<Footer />
</>
);
<Footer />
</>
);
};
export default Help;

View file

@ -1,43 +1,43 @@
import styled from '@emotion/styled';
export const Step = styled.h2`
text-decoration-color: ${props => props.theme.primary};
text-decoration-color: ${props => props.theme.primary};
text-decoration-style: solid;
text-decoration-line: underline;
margin-top: 30px;
`;
export const FakeCalendar = styled.div`
user-select: none;
user-select: none;
& div {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-gap: 2px;
grid-template-columns: repeat(7, 1fr);
grid-gap: 2px;
}
& .days span {
display: flex;
align-items: center;
justify-content: center;
padding: 3px 0;
font-weight: bold;
user-select: none;
opacity: .7;
align-items: center;
justify-content: center;
padding: 3px 0;
font-weight: bold;
user-select: none;
opacity: .7;
@media (max-width: 350px) {
font-size: 12px;
}
font-size: 12px;
}
}
& .dates span {
background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary};
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
border: 1px solid ${props => props.theme.primary};
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
&.selected {
color: #FFF;
background-color: ${props => props.theme.primary};
background-color: ${props => props.theme.primary};
}
}
& .dates span:first-of-type {
@ -51,45 +51,45 @@ export const FakeCalendar = styled.div`
`;
export const FakeTimeRange = styled.div`
user-select: none;
user-select: none;
background-color: ${props => props.theme.primaryBackground};
border: 1px solid ${props => props.theme.primary};
border-radius: 3px;
height: 50px;
position: relative;
margin: 38px 6px 18px;
border: 1px solid ${props => props.theme.primary};
border-radius: 3px;
height: 50px;
position: relative;
margin: 38px 6px 18px;
& div {
height: calc(100% + 20px);
width: 20px;
border: 1px solid ${props => props.theme.primary};
background-color: ${props => props.theme.primaryLight};
border-radius: 3px;
position: absolute;
top: -10px;
width: 20px;
border: 1px solid ${props => props.theme.primary};
background-color: ${props => props.theme.primaryLight};
border-radius: 3px;
position: absolute;
top: -10px;
&:after {
content: '|||';
font-size: 8px;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: ${props => props.theme.primaryDark};
}
&:after {
content: '|||';
font-size: 8px;
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: ${props => props.theme.primaryDark};
}
&:before {
content: attr(data-label);
position: absolute;
bottom: calc(100% + 8px);
text-align: center;
left: 50%;
transform: translateX(-50%);
}
&:before {
content: attr(data-label);
position: absolute;
bottom: calc(100% + 8px);
text-align: center;
left: 50%;
transform: translateX(-50%);
}
}
& .start {
left: calc(${11 * 4.1666666666666666}% - 11px);
@ -100,11 +100,11 @@ export const FakeTimeRange = styled.div`
&:before {
content: '';
position: absolute;
height: 100%;
left: ${11 * 4.1666666666666666}%;
right: calc(100% - ${17 * 4.1666666666666666}%);
top: 0;
background-color: ${props => props.theme.primary};
border-radius: 2px;
height: 100%;
left: ${11 * 4.1666666666666666}%;
right: calc(100% - ${17 * 4.1666666666666666}%);
top: 0;
background-color: ${props => props.theme.primary};
border-radius: 2px;
}
`;

View file

@ -9,30 +9,30 @@ import timezone from 'dayjs/plugin/timezone';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import {
TextField,
CalendarField,
TimeRangeField,
SelectField,
Button,
Center,
Error,
TextField,
CalendarField,
TimeRangeField,
SelectField,
Button,
Center,
Error,
Footer,
Recents,
} from 'components';
import {
StyledMain,
CreateForm,
TitleSmall,
TitleLarge,
Logo,
Links,
AboutSection,
P,
Stats,
Stat,
StatNumber,
StatLabel,
StyledMain,
CreateForm,
TitleSmall,
TitleLarge,
Logo,
Links,
AboutSection,
P,
Stats,
Stat,
StatNumber,
StatLabel,
OfflineMessage,
ButtonArea,
} from './homeStyle';
@ -49,120 +49,120 @@ dayjs.extend(timezone);
dayjs.extend(customParseFormat);
const Home = ({ offline }) => {
const { register, handleSubmit, setValue } = useForm({
defaultValues: {
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 { register, handleSubmit, setValue } = useForm({
defaultValues: {
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 [browser, setBrowser] = useState(undefined);
const { push } = useHistory();
const { push } = useHistory();
const { t } = useTranslation(['common', 'home']);
const isTWA = useTWAStore(state => state.TWA);
useEffect(() => {
const fetch = async () => {
try {
const response = await api.get('/stats');
setStats(response.data);
} catch (e) {
console.error(e);
}
};
useEffect(() => {
const fetch = async () => {
try {
const response = await api.get('/stats');
setStats(response.data);
} catch (e) {
console.error(e);
}
};
fetch();
document.title = 'Crab Fit';
fetch();
document.title = 'Crab Fit';
setBrowser(detect_browser());
}, []);
}, []);
const onSubmit = async data => {
setIsLoading(true);
setError(null);
try {
const { start, end } = JSON.parse(data.times);
const dates = JSON.parse(data.dates);
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(t('home:form.errors.no_dates'));
}
if (dates.length === 0) {
return setError(t('home:form.errors.no_dates'));
}
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8;
if (start === end) {
return setError(t('home:form.errors.same_times'));
}
if (start === end) {
return setError(t('home:form.errors.same_times'));
}
let times = dates.reduce((times, date) => {
let day = [];
for (let i = start; i < (start > end ? 24 : end); i++) {
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')
);
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 (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')
);
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];
}, []);
}
}
return [...times, ...day];
}, []);
if (times.length === 0) {
return setError(t('home:form.errors.no_time'));
}
if (times.length === 0) {
return setError(t('home:form.errors.no_time'));
}
const response = await api.post('/event', {
event: {
name: data.name,
times: times,
const response = await api.post('/event', {
event: {
name: data.name,
times: times,
timezone: data.timezone,
},
});
push(`/${response.data.id}`);
},
});
push(`/${response.data.id}`);
gtag('event', 'create_event', {
'event_category': 'home',
});
} catch (e) {
setError(t('home:form.errors.unknown'));
console.error(e);
} finally {
setIsLoading(false);
}
};
} catch (e) {
setError(t('home:form.errors.unknown'));
console.error(e);
} finally {
setIsLoading(false);
}
};
return (
<>
<StyledMain>
<Center>
<Logo src={logo} alt="" />
</Center>
<TitleSmall altChars={/[A-Z]/g.test(t('home:create'))}>{t('home:create')}</TitleSmall>
<TitleLarge>CRAB FIT</TitleLarge>
<Links>
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
</Links>
return (
<>
<StyledMain>
<Center>
<Logo src={logo} alt="" />
</Center>
<TitleSmall altChars={/[A-Z]/g.test(t('home:create'))}>{t('home:create')}</TitleSmall>
<TitleLarge>CRAB FIT</TitleLarge>
<Links>
<a href="#about">{t('home:nav.about')}</a> / <a href="#donate">{t('home:nav.donate')}</a>
</Links>
</StyledMain>
<Recents />
@ -174,65 +174,65 @@ const Home = ({ offline }) => {
<P>{t('home:offline')}</P>
</OfflineMessage>
) : (
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
<TextField
label={t('home:form.name.label')}
subLabel={t('home:form.name.sublabel')}
type="text"
id="name"
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
<TextField
label={t('home:form.name.label')}
subLabel={t('home:form.name.sublabel')}
type="text"
id="name"
{...register('name')}
/>
/>
<CalendarField
label={t('home:form.dates.label')}
subLabel={t('home:form.dates.sublabel')}
id="dates"
<CalendarField
label={t('home:form.dates.label')}
subLabel={t('home:form.dates.sublabel')}
id="dates"
required
setValue={setValue}
{...register('dates')}
/>
/>
<TimeRangeField
label={t('home:form.times.label')}
subLabel={t('home:form.times.sublabel')}
id="times"
<TimeRangeField
label={t('home:form.times.label')}
subLabel={t('home:form.times.sublabel')}
id="times"
required
setValue={setValue}
{...register('times')}
/>
/>
<SelectField
label={t('home:form.timezone.label')}
id="timezone"
options={timezones}
<SelectField
label={t('home:form.timezone.label')}
id="timezone"
options={timezones}
required
{...register('timezone')}
defaultOption={t('home:form.timezone.defaultOption')}
/>
defaultOption={t('home:form.timezone.defaultOption')}
/>
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Center>
<Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button>
</Center>
</CreateForm>
<Center>
<Button type="submit" isLoading={isLoading} disabled={isLoading}>{t('home:form.button')}</Button>
</Center>
</CreateForm>
)}
</StyledMain>
</StyledMain>
<AboutSection id="about">
<StyledMain>
<h2>{t('home:about.name')}</h2>
<Stats>
<Stat>
<StatNumber>{stats.eventCount ?? '350+'}</StatNumber>
<StatLabel>{t('home:about.events')}</StatLabel>
</Stat>
<Stat>
<StatNumber>{stats.personCount ?? '550+'}</StatNumber>
<StatLabel>{t('home:about.availabilities')}</StatLabel>
</Stat>
</Stats>
<P><Trans i18nKey="home:about.content.p1">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.<br /><Link to="/how-to" rel="help">Learn more about how to Crab Fit</Link>.</Trans></P>
<AboutSection id="about">
<StyledMain>
<h2>{t('home:about.name')}</h2>
<Stats>
<Stat>
<StatNumber>{stats.eventCount ?? '350+'}</StatNumber>
<StatLabel>{t('home:about.events')}</StatLabel>
</Stat>
<Stat>
<StatNumber>{stats.personCount ?? '550+'}</StatNumber>
<StatLabel>{t('home:about.availabilities')}</StatLabel>
</Stat>
</Stats>
<P><Trans i18nKey="home:about.content.p1">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.<br /><Link to="/how-to" rel="help">Learn more about how to Crab Fit</Link>.</Trans></P>
{isTWA !== true && (
<ButtonArea>
{['chrome', 'firefox', 'safari'].includes(browser) && (
@ -267,16 +267,16 @@ const Home = ({ offline }) => {
>{t('home:about.android_app')}</Button>
</ButtonArea>
)}
<P><Trans i18nKey="home:about.content.p3">Created by <a href="https://bengrant.dev" target="_blank" rel="noreferrer noopener author">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</Trans></P>
<P><Trans i18nKey="home:about.content.p4">The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer noopener">repository</a>. By using Crab Fit you agree to the <Link to="/privacy" rel="license">privacy policy</Link>.</Trans></P>
<P><Trans i18nKey="home:about.content.p3">Created by <a href="https://bengrant.dev" target="_blank" rel="noreferrer noopener author">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</Trans></P>
<P><Trans i18nKey="home:about.content.p4">The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer noopener">repository</a>. By using Crab Fit you agree to the <Link to="/privacy" rel="license">privacy policy</Link>.</Trans></P>
<P>{t('home:about.content.p6')}</P>
<P>{t('home:about.content.p5')}</P>
</StyledMain>
</AboutSection>
</StyledMain>
</AboutSection>
<Footer />
</>
);
<Footer />
</>
);
};
export default Home;

View file

@ -1,9 +1,9 @@
import styled from '@emotion/styled';
export const StyledMain = styled.div`
width: 600px;
margin: 20px auto;
max-width: calc(100% - 60px);
width: 600px;
margin: 20px auto;
max-width: calc(100% - 60px);
`;
export const CreateForm = styled.form`
@ -11,14 +11,14 @@ export const CreateForm = styled.form`
`;
export const TitleSmall = styled.span`
display: block;
margin: 0;
font-size: 3rem;
text-align: center;
font-family: 'Samurai Bob', sans-serif;
font-weight: 400;
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
line-height: 1em;
display: block;
margin: 0;
font-size: 3rem;
text-align: center;
font-family: 'Samurai Bob', sans-serif;
font-weight: 400;
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
line-height: 1em;
text-transform: uppercase;
${props => !props.altChars && `
@ -30,23 +30,23 @@ export const TitleSmall = styled.span`
`;
export const TitleLarge = styled.h1`
margin: 0;
font-size: 4rem;
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;
margin: 0;
font-size: 4rem;
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;
text-transform: uppercase;
@media (max-width: 350px) {
font-size: 3.5rem;
}
@media (max-width: 350px) {
font-size: 3.5rem;
}
`;
export const Logo = styled.img`
width: 80px;
width: 80px;
transition: transform .15s;
animation: jelly .5s 1 .05s;
user-select: none;
@ -81,14 +81,14 @@ export const Logo = styled.img`
`;
export const Links = styled.nav`
text-align: center;
margin: 20px 0;
text-align: center;
margin: 20px 0;
`;
export const AboutSection = styled.section`
margin: 30px 0 0;
background-color: ${props => props.theme.primaryBackground};
padding: 20px 0;
margin: 30px 0 0;
background-color: ${props => props.theme.primaryBackground};
padding: 20px 0;
& a {
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
@ -96,42 +96,42 @@ export const AboutSection = styled.section`
`;
export const P = styled.p`
font-weight: 500;
line-height: 1.6em;
font-weight: 500;
line-height: 1.6em;
`;
export const Stats = styled.div`
display: flex;
justify-content: space-around;
align-items: flex-start;
flex-wrap: wrap;
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;
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.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
font-size: 2em;
display: block;
font-weight: 900;
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};
font-size: 2em;
`;
export const StatLabel = styled.span`
display: block;
display: block;
`;
export const OfflineMessage = styled.div`
text-align: center;
text-align: center;
margin: 50px 0 20px;
`;
export const ButtonArea = styled.div`
display: flex;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;

View file

@ -3,16 +3,16 @@ import { useHistory } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Button,
Center,
Footer,
Button,
Center,
Footer,
Logo,
} from 'components';
import {
StyledMain,
AboutSection,
P,
StyledMain,
AboutSection,
P,
} from '../Home/homeStyle';
import { Note } from './privacyStyle';
@ -24,20 +24,20 @@ const Privacy = () => {
const contentRef = useRef();
const [content, setContent] = useState('');
useEffect(() => {
document.title = `${t('privacy:name')} - Crab Fit`;
}, [t]);
useEffect(() => {
document.title = `${t('privacy:name')} - Crab Fit`;
}, [t]);
useEffect(() => setContent(contentRef.current?.innerText || ''), [contentRef]);
return (
<>
<StyledMain>
return (
<>
<StyledMain>
<Logo />
</StyledMain>
<StyledMain>
<h1>{t('privacy:name')}</h1>
<h1>{t('privacy:name')}</h1>
{!i18n.language.startsWith('en') && (
<p>
@ -98,15 +98,15 @@ const Privacy = () => {
</div>
</StyledMain>
<AboutSection id="about">
<StyledMain>
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
</StyledMain>
</AboutSection>
<AboutSection id="about">
<StyledMain>
<Center><Button onClick={() => push('/')}>{t('common:cta')}</Button></Center>
</StyledMain>
</AboutSection>
<Footer />
</>
);
<Footer />
</>
);
};
export default Privacy;

View file

@ -8,7 +8,7 @@ export const Note = styled.p`
margin: 16px 0;
box-sizing: border-box;
font-weight: 500;
line-height: 1.6em;
line-height: 1.6em;
& a {
color: ${props => props.theme.mode === 'light' ? props.theme.primaryDark : props.theme.primaryLight};