Name generation and gradient calculation

This commit is contained in:
Ben Grant 2021-03-04 19:00:38 +11:00
parent 8e5954e0ca
commit ba1697ffc7
12 changed files with 418 additions and 54 deletions

View file

@ -37,7 +37,7 @@ app.get('/event/:eventId', getEvent);
app.post('/event', createEvent); app.post('/event', createEvent);
app.get('/event/:eventId/people', getPeople); app.get('/event/:eventId/people', getPeople);
app.post('/event/:eventId/people', createPerson); app.post('/event/:eventId/people', createPerson);
app.get('/event/:eventId/people/:personName', login); app.post('/event/:eventId/people/:personName', login);
app.patch('/event/:eventId/people/:personName', updatePerson); app.patch('/event/:eventId/people/:personName', updatePerson);
app.listen(port, () => { app.listen(port, () => {

View file

@ -0,0 +1,201 @@
[
"adorable",
"adventurous",
"aggressive",
"agreeable",
"alert",
"alive",
"amused",
"angry",
"annoyed",
"annoying",
"anxious",
"arrogant",
"ashamed",
"attractive",
"average",
"beautiful",
"better",
"bewildered",
"blue",
"blushing",
"bored",
"brainy",
"brave",
"breakable",
"bright",
"busy",
"calm",
"careful",
"cautious",
"charming",
"cheerful",
"clean",
"clear",
"clever",
"cloudy",
"clumsy",
"colorful",
"comfortable",
"concerned",
"confused",
"cooperative",
"courageous",
"crazy",
"creepy",
"crowded",
"curious",
"cute",
"dangerous",
"dark",
"defiant",
"delightful",
"depressed",
"determined",
"different",
"difficult",
"disgusted",
"distinct",
"disturbed",
"dizzy",
"doubtful",
"drab",
"dull",
"eager",
"easy",
"elated",
"elegant",
"embarrassed",
"enchanting",
"encouraging",
"energetic",
"enthusiastic",
"envious",
"evil",
"excited",
"expensive",
"exuberant",
"fair",
"faithful",
"famous",
"fancy",
"fantastic",
"fierce",
"fine",
"foolish",
"fragile",
"frail",
"frantic",
"friendly",
"frightened",
"funny",
"gentle",
"gifted",
"glamorous",
"gleaming",
"glorious",
"good",
"gorgeous",
"graceful",
"grumpy",
"handsome",
"happy",
"healthy",
"helpful",
"hilarious",
"homely",
"hungry",
"important",
"impossible",
"inexpensive",
"innocent",
"inquisitive",
"itchy",
"jealous",
"jittery",
"jolly",
"joyous",
"kind",
"lazy",
"light",
"lively",
"lonely",
"long",
"lovely",
"lucky",
"magnificent",
"misty",
"modern",
"motionless",
"muddy",
"mushy",
"mysterious",
"naughty",
"nervous",
"nice",
"nutty",
"obedient",
"obnoxious",
"odd",
"old-fashioned",
"open",
"outrageous",
"outstanding",
"panicky",
"perfect",
"plain",
"pleasant",
"poised",
"powerful",
"precious",
"prickly",
"proud",
"puzzled",
"quaint",
"real",
"relieved",
"scary",
"selfish",
"shiny",
"shy",
"silly",
"sleepy",
"smiling",
"smoggy",
"sparkling",
"splendid",
"spotless",
"stormy",
"strange",
"successful",
"super",
"talented",
"tame",
"tasty",
"tender",
"tense",
"terrible",
"thankful",
"thoughtful",
"thoughtless",
"tired",
"tough",
"uninterested",
"unsightly",
"unusual",
"upset",
"uptight",
"vast",
"victorious",
"vivacious",
"wandering",
"weary",
"wicked",
"wide-eyed",
"wild",
"witty",
"worried",
"worrisome",
"zany",
"zealous"
]

View file

@ -0,0 +1,47 @@
[
"American Horseshoe",
"Atlantic Ghost",
"Baja Elbow",
"Big Claw Purple Hermit",
"Coldwater Mole",
"Cuata Swim",
"Deepwater Frog",
"Dwarf Teardrop",
"Elegant Hermit",
"Flat Spider",
"Ghost",
"Globe Purse",
"Green",
"Halloween",
"Harbor Spider",
"Inflated Spider",
"Left Clawed Hermit",
"Lumpy Claw",
"Magnificent Hermit",
"Mexican Spider",
"Mouthless Land",
"Northern Lemon Rock",
"Pacific Arrow",
"Pacific Mole",
"Paco Box",
"Panamic Spider",
"Purple Shore",
"Red Rock",
"Red Swim",
"Red-leg Hermit",
"Robust Swim",
"Rough Swim",
"Sand Swim",
"Sally Lightfoot",
"Shamed-face Box",
"Shamed-face Heart Box",
"Shell",
"Small Arched Box",
"Southern Kelp",
"Spotted Box",
"Striated Mole",
"Striped Shore",
"Tropical Mole",
"Walking Rock",
"Yellow Shore"
]

View file

@ -1,24 +1,33 @@
const dayjs = require('dayjs'); const dayjs = require('dayjs');
const adjectives = require('../res/adjectives.json');
const crabs = require('../res/crabs.json');
String.prototype.capitalize = () => this.charAt(0).toUpperCase() + this.slice(1);
const generateId = (name) => { const generateId = (name) => {
const id = name.trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-'); const id = name.trim().toLowerCase().replace(/[^A-Za-z0-9 ]/g, '').replace(/\s+/g, '-');
const number = Math.floor(100000 + Math.random() * 900000); const number = Math.floor(100000 + Math.random() * 900000);
return `${id}-${number}`; return `${id}-${number}`;
}; };
const generateName = () => {
return `${adjectives[Math.floor(Math.random() * adjectives.length)].capitalize()} ${crabs[Math.floor(Math.random() * crabs.length)]} Crab`;
};
module.exports = async (req, res) => { module.exports = async (req, res) => {
const { event } = req.body; const { event } = req.body;
try { try {
const eventId = generateId(event.name); const name = event.name.trim() === '' ? generateName() : event.name.trim();
const eventId = generateId(name);
const currentTime = dayjs().unix(); const currentTime = dayjs().unix();
const entity = { const entity = {
key: req.datastore.key(['Event', eventId]), key: req.datastore.key(['Event', eventId]),
data: { data: {
name: event.name.trim(), name: name,
created: currentTime, created: currentTime,
timezone: event.timezone,
startTime: event.startTime, startTime: event.startTime,
endTime: event.endTime, endTime: event.endTime,
dates: event.dates, dates: event.dates,
@ -29,9 +38,8 @@ module.exports = async (req, res) => {
res.status(201).send({ res.status(201).send({
id: eventId, id: eventId,
name: event.name.trim(), name: name,
created: currentTime, created: currentTime,
timezone: event.timezone,
startTime: event.startTime, startTime: event.startTime,
endTime: event.endTime, endTime: event.endTime,
dates: event.dates, dates: event.dates,

View file

@ -7,9 +7,13 @@ module.exports = async (req, res) => {
try { try {
const event = (await req.datastore.get(req.datastore.key(['Event', eventId])))[0]; const event = (await req.datastore.get(req.datastore.key(['Event', eventId])))[0];
const query = req.datastore.createQuery('Person')
.filter('eventId', eventId)
.filter('name', person.name);
let personResult = (await req.datastore.runQuery(query))[0][0];
if (event) { if (event) {
if (person) { if (person && personResult === undefined) {
const currentTime = dayjs().unix(); const currentTime = dayjs().unix();
// If password // If password

View file

@ -18,8 +18,6 @@ definitions:
type: "string" type: "string"
created: created:
type: "integer" type: "integer"
timezone:
type: "string"
startTime: startTime:
type: "string" type: "string"
endTime: endTime:
@ -84,8 +82,6 @@ paths:
properties: properties:
name: name:
type: "string" type: "string"
timezone:
type: "string"
startTime: startTime:
type: "integer" type: "integer"
endTime: endTime:
@ -152,7 +148,7 @@ paths:
400: 400:
description: "Invalid data" description: "Invalid data"
"/event/{eventId}/people/{personName}": "/event/{eventId}/people/{personName}":
get: post:
summary: "Login as this person" summary: "Login as this person"
operationId: "getPerson" operationId: "getPerson"
parameters: parameters:

View file

@ -27,6 +27,8 @@ const AvailabilityViewer = ({
dates, dates,
times, times,
people = [], people = [],
min = 0,
max = 0,
...props ...props
}) => { }) => {
const [tooltip, setTooltip] = useState(null); const [tooltip, setTooltip] = useState(null);
@ -52,14 +54,16 @@ const AvailabilityViewer = ({
{times.map((time, i) => { {times.map((time, i) => {
const peopleHere = people.filter(person => person.availability.includes(`${time}-${date}`)).map(person => person.name); const peopleHere = people.filter(person => person.availability.includes(`${time}-${date}`)).map(person => person.name);
return ( return (
<Time <Time
key={i} key={i}
time={time} time={time}
className="time" className="time"
people={peopleHere} peopleCount={peopleHere.length}
aria-label={peopleHere.join(', ')} aria-label={peopleHere.join(', ')}
totalPeople={people.length} maxPeople={max}
minPeople={min}
onMouseEnter={(e) => { onMouseEnter={(e) => {
const cellBox = e.currentTarget.getBoundingClientRect(); const cellBox = e.currentTarget.getBoundingClientRect();
setTooltip({ setTooltip({

View file

@ -58,7 +58,9 @@ export const Time = styled.div`
border-top: 1px dotted ${props.theme.primaryDark}; border-top: 1px dotted ${props.theme.primaryDark};
`} `}
background-color: ${props => `${props.theme.primary}${Math.round((props.people.length/(props.totalPeople))*255).toString(16)}`}; background-color: ${props => `${props.theme.primary}${Math.round(((props.peopleCount-props.minPeople)/(props.maxPeople-props.minPeople))*255).toString(16)}`};
count: ${props => props.peopleCount};
max: ${props => props.maxPeople};
`; `;
export const Spacer = styled.div` export const Spacer = styled.div`

View file

@ -10,13 +10,14 @@ import {
const Legend = ({ const Legend = ({
min, min,
max, max,
total,
...props ...props
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Wrapper> <Wrapper>
<Label>{min}/{max} available</Label> <Label>{min}/{total} available</Label>
<Bar> <Bar>
{[...Array(max-min+1).keys()].map(i => {[...Array(max-min+1).keys()].map(i =>
@ -24,7 +25,7 @@ const Legend = ({
)} )}
</Bar> </Bar>
<Label>{max}/{max} available</Label> <Label>{max}/{total} available</Label>
</Wrapper> </Wrapper>
); );
}; };

View file

@ -1,6 +1,6 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { import {
Center, Center,
@ -11,6 +11,7 @@ import {
Legend, Legend,
AvailabilityViewer, AvailabilityViewer,
AvailabilityEditor, AvailabilityEditor,
Error,
} from 'components'; } from 'components';
import { import {
@ -40,13 +41,27 @@ const Event = (props) => {
const [password, setPassword] = useState(null); const [password, setPassword] = useState(null);
const [tab, setTab] = useState(user ? 'you' : 'group'); const [tab, setTab] = useState(user ? 'you' : 'group');
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isLoginLoading, setIsLoginLoading] = useState(false);
const [error, setError] = useState(null);
const [event, setEvent] = useState(null); const [event, setEvent] = useState(null);
const [people, setPeople] = useState([]); const [people, setPeople] = useState([]);
const [min, setMin] = useState(0);
const [max, setMax] = useState(0);
const fetchPeople = useCallback(async () => {
try {
const response = await api.get(`/event/${id}/people`);
setPeople(response.data.people);
} catch (e) {
console.error(e);
}
}, [id]);
useEffect(() => { useEffect(() => {
const fetchEvent = async () => { const fetchEvent = async () => {
const response = await api.get(`/event/${id}`); try {
if (response.status === 200) { const response = await api.get(`/event/${id}`);
let times = []; let times = [];
for (let i = response.data.startTime; i < response.data.endTime; i++) { for (let i = response.data.startTime; i < response.data.endTime; i++) {
let hour = `${i}`.padStart(2, '0'); let hour = `${i}`.padStart(2, '0');
@ -63,26 +78,93 @@ const Event = (props) => {
times, times,
}); });
setIsLoading(false); setIsLoading(false);
} else { } catch (e) {
console.error(response); console.error(e);
//TODO: 404 //TODO: 404
} }
}; };
const fetchPeople = async () => {
const response = await api.get(`/event/${id}/people`);
if (response.status === 200) {
setPeople(response.data.people);
} else {
console.error(response);
}
};
fetchEvent(); fetchEvent();
fetchPeople(); fetchPeople();
}, [id]); }, [fetchPeople, id]);
const onSubmit = data => console.log('submit', data); useEffect(() => {
if (tab === 'group') {
fetchPeople();
}
}, [fetchPeople, tab]);
useEffect(() => {
if (event && !!people.length) {
const datetimes = event.dates.reduce(
(dates, date) => {
let times = [];
event.times.forEach(time => {
times.push(`${time}-${date}`);
});
return [...dates, ...times];
}
, []
);
setMin(datetimes.reduce((min, time) => {
let total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total,
0
);
return total < min ? total : min;
},
Infinity
));
setMax(datetimes.reduce((max, time) => {
let total = people.reduce(
(total, person) => person.availability.includes(time) ? total+1 : total,
0
);
return total > max ? total : max;
},
-Infinity
));
}
}, [event, people]);
const onSubmit = async data => {
setIsLoginLoading(true);
setError(null);
try {
const response = await api.post(`/event/${id}/people/${data.name}`, {
person: {
password: data.password,
},
});
setPassword(data.password);
setUser(response.data);
setTab('you');
} catch (e) {
if (e.status === 401) {
setError('Password is incorrect. Check your name is spelled right.');
} 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('Failed to create user. Please try again.');
}
}
} finally {
setIsLoginLoading(false);
}
};
return ( return (
<> <>
@ -92,6 +174,7 @@ const Event = (props) => {
<Logo src={logo} alt="" /> <Logo src={logo} alt="" />
<Title>CRAB FIT</Title> <Title>CRAB FIT</Title>
</Center> </Center>
<Center style={{ textDecoration: 'underline', fontSize: 14, paddingTop: 6 }}>Create your own</Center>
</Link> </Link>
<EventName isLoading={isLoading}>{event?.name}</EventName> <EventName isLoading={isLoading}>{event?.name}</EventName>
@ -105,7 +188,9 @@ const Event = (props) => {
<LoginSection id="login"> <LoginSection id="login">
<StyledMain> <StyledMain>
{!user && ( {user ? (
<h2>Signed in as {user.name}</h2>
) : (
<> <>
<h2>Sign in to add your availability</h2> <h2>Sign in to add your availability</h2>
<LoginForm onSubmit={handleSubmit(onSubmit)}> <LoginForm onSubmit={handleSubmit(onSubmit)}>
@ -129,8 +214,12 @@ const Event = (props) => {
/> />
<Button <Button
type="submit"
isLoading={isLoginLoading}
disabled={isLoginLoading || isLoading}
>Login</Button> >Login</Button>
</LoginForm> </LoginForm>
{error && <Error onClose={() => setError(null)}>{error}</Error>}
<Info>These details are only for this event. Use a password to prevent others from changing your availability.</Info> <Info>These details are only for this event. Use a password to prevent others from changing your availability.</Info>
</> </>
)} )}
@ -175,13 +264,19 @@ const Event = (props) => {
{tab === 'group' ? ( {tab === 'group' ? (
<section id="group"> <section id="group">
<StyledMain> <StyledMain>
<Legend min={0} max={people.length} /> <Legend
min={min}
max={max}
total={people.filter(p => p.availability.length > 0).length}
/>
<Center>Hover or tap the calendar below to see who is available</Center> <Center>Hover or tap the calendar below to see who is available</Center>
</StyledMain> </StyledMain>
<AvailabilityViewer <AvailabilityViewer
dates={event?.dates ?? []} dates={event?.dates ?? []}
times={event?.times ?? []} times={event?.times ?? []}
people={people} people={people.filter(p => p.availability.length > 0)}
min={min}
max={max}
/> />
</section> </section>
) : ( ) : (

View file

@ -2,6 +2,10 @@ import { useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
import { import {
TextField, TextField,
CalendarField, CalendarField,
@ -34,6 +38,9 @@ import api from 'services';
import logo from 'res/logo.svg'; import logo from 'res/logo.svg';
import timezones from 'res/timezones.json'; import timezones from 'res/timezones.json';
dayjs.extend(utc);
dayjs.extend(timezone);
const Home = () => { const Home = () => {
const { register, handleSubmit } = useForm({ const { register, handleSubmit } = useForm({
defaultValues: { defaultValues: {
@ -51,9 +58,11 @@ const Home = () => {
useEffect(() => { useEffect(() => {
const fetch = async () => { const fetch = async () => {
const response = await api.get('/stats'); try {
if (response.status === 200) { const response = await api.get('/stats');
setStats(response.data); setStats(response.data);
} catch (e) {
console.error(e);
} }
}; };
@ -65,23 +74,20 @@ const Home = () => {
setError(null); setError(null);
try { try {
const times = JSON.parse(data.times); const times = JSON.parse(data.times);
const dates = JSON.parse(data.dates);
const start = dayjs().tz(data.timezone).hour(times.start);
const end = dayjs().tz(data.timezone).hour(times.end);
const response = await api.post('/event', { const response = await api.post('/event', {
event: { event: {
name: data.name, name: data.name,
timezone: data.timezone, startTime: start.utc().hour(),
startTime: times.start, endTime: end.utc().hour(),
endTime: times.end, dates: dates,
dates: JSON.parse(data.dates),
}, },
}); });
push(`/${response.data.id}`);
if (response.status === 201) {
// Success
push(`/${response.data.id}`);
} else {
setError('An error ocurred while creating the event. Please try again later.');
console.error(response.status);
}
} catch (e) { } catch (e) {
setError('An error ocurred while creating the event. Please try again later.'); setError('An error ocurred while creating the event. Please try again later.');
console.error(e); console.error(e);
@ -164,7 +170,7 @@ const Home = () => {
</Stats> </Stats>
<P>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.</P> <P>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.</P>
{/* eslint-disable-next-line */} {/* eslint-disable-next-line */}
<P>Create by <a href="https://bengrant.dev" target="_blank">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</P> <P>Created by <a href="https://bengrant.dev" target="_blank">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</P>
<P>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">repository</a>.</P> <P>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">repository</a>.</P>
</StyledMain> </StyledMain>
</AboutSection> </AboutSection>

View file

@ -12,13 +12,13 @@ const handleError = error => {
if (error.response && error.response.status) { if (error.response && error.response.status) {
console.log('[Error handler] res:', error.response); console.log('[Error handler] res:', error.response);
} }
return Promise.reject(error); return Promise.reject(error.response);
}; };
const api = { const api = {
get: async (endpoint, data = {}) => { get: async (endpoint, data) => {
try { try {
const response = await instance.get(endpoint, { params: data }); const response = await instance.get(endpoint, data);
return Promise.resolve(response); return Promise.resolve(response);
} catch (error) { } catch (error) {
return handleError(error); return handleError(error);