Tabs -> spaces
I have become my own worst enemy
This commit is contained in:
parent
e94559c4f6
commit
fdb7f0ef67
49 changed files with 2424 additions and 2424 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 && `
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
`}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue