Feature/add timezone frontend (#231)

* Added tz column

* Updated hasura permissions on new table

* Added new dependencies for working with timezones

* Added SetupTimeZone component

* Bumped spacetime-informal to use their types

* Extracted timezone computation into helper, added useMemo hook

* Re-added spacetime types
This commit is contained in:
Alec LaLonde
2020-12-24 23:28:12 -07:00
committed by GitHub
parent 3d62e5b8da
commit 44c706761c
22 changed files with 344 additions and 53 deletions

View File

@@ -0,0 +1,22 @@
declare module 'react-timezone-select' {
// eslint-disable-next-line import/no-extraneous-dependencies
import * as React from 'react';
// eslint-disable-next-line import/no-default-export, react/prefer-stateless-function
export default class TimezoneSelect extends React.Component<SelectTimezoneProps> {}
export interface TimeZone {
value: string;
label: string;
altName: string;
abbrev: string;
}
export interface TimezoneSelectProps extends Props {
value?: TimeZone | string
onBlur?: () => void
onChange?: (timezone:TimeZone) => void
labelStyle: 'original' | 'altName' | 'abbrev'
}
}

View File

@@ -0,0 +1,18 @@
declare module 'spacetime-informal' {
export function find(tz:string): string;
export function display(geo:string): DisplayFormat;
export function version(): string;
export interface DisplayFormat {
iana: string;
standard: TimeZoneInfo;
daylight: TimeZoneInfo;
}
export interface TimeZoneInfo {
name: string;
abbrev: string;
}
}

View File

@@ -27,7 +27,8 @@
"@chakra-ui/core": "next",
"@chakra-ui/icons": "next",
"@chakra-ui/theme": "next",
"react-select": "^3.1.0"
"react-select": "^3.1.0",
"react-timezone-select": "^0.9.7"
},
"devDependencies": {
"@babel/core": "^7.10.5",

View File

@@ -83,6 +83,7 @@ export const selectStyles: Styles = {
},
}),
};
export const SelectSearch: React.FC<SelectProps> = (props) => (
<Select styles={selectStyles} {...props} />
);

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { Styles } from 'react-select';
import TimezoneSelect, { TimezoneSelectProps } from 'react-timezone-select';
import { theme } from './theme';
export const selectStyles: Styles = {
menu: (styles) => ({
...styles,
background: theme.colors.dark,
}),
input: (styles) => ({
...styles,
color: theme.colors.white,
}),
option: (styles) => ({
...styles,
background: theme.colors.dark,
':hover': {
backgroundColor: theme.colors.purpleTag,
color: theme.colors.white,
},
}),
control: (styles) => ({
...styles,
background: theme.colors.dark,
border: theme.colors.dark,
}),
singleValue: (styles) => ({
...styles,
color: theme.colors.white,
}),
dropdownIndicator: (styles) => ({
...styles,
color: theme.colors.white,
cursor: 'pointer',
':hover': {
color: theme.colors.blueLight,
},
}),
};
export const SelectTimeZone: React.FC<TimezoneSelectProps> = (props) => (
<TimezoneSelect styles={selectStyles} {...props} />
);

View File

@@ -53,3 +53,4 @@ export { MetaTag } from './MetaTag';
export { H1, P } from './typography';
export { ResponsiveText } from './ResponsiveText';
export { SelectSearch, selectStyles } from './SelectSearch';
export { SelectTimeZone } from './SelectTimeZone';

View File

@@ -0,0 +1,26 @@
import { Box, HStack, Text } from '@metafam/ds';
import { PlayerFragmentFragment } from 'graphql/autogen/types';
import React, { useMemo } from 'react';
import { FaGlobe } from 'react-icons/fa';
import { getPlayerTimeZoneDisplay } from 'utils/dateHelpers';
type Props = {
player: PlayerFragmentFragment;
};
export const PlayerTimeZone: React.FC<Props> = ({ player }) => {
const tzDisplay = useMemo(() => getPlayerTimeZoneDisplay(player), [player]);
return (
<Box ml={1}>
<Text fontSize="xs" color="blueLight" casing="uppercase" mb={3}>
time zone
</Text>
<HStack alignItems="baseline">
<FaGlobe color="blueLight" />
<Text fontSize="xl" mb="1">{tzDisplay?.timeZone || '-'}</Text>
{tzDisplay?.offset ? <Text fontSize="xs" mr={3}>{tzDisplay?.offset}</Text> : ''}
</HStack>
</Box>
);
};

View File

@@ -1,7 +1,9 @@
import { Box, Divider, HStack, Text } from '@metafam/ds';
import { PlayerFragmentFragment } from 'graphql/autogen/types';
import React from 'react';
import { FaClock, FaGlobe } from 'react-icons/fa';
import { FaClock } from 'react-icons/fa';
import { PlayerTimeZone } from '../PlayerTimeZone';
type Props = { player: PlayerFragmentFragment };
export const PlayerCollab: React.FC<Props> = ({ player }) => {
@@ -12,17 +14,7 @@ export const PlayerCollab: React.FC<Props> = ({ player }) => {
<Divider height="3rem" color="whiteAlpha.400" orientation="vertical" />
}
>
<Box>
<Text fontSize="xs" color="blueLight" casing="uppercase" mb={3}>
Location
</Text>
<HStack alignItems="baseline">
<FaGlobe color="blueLight" />
<Text fontSize="lg" fontFamily="mono" mb="1">
{player.box_profile?.location || '-'}
</Text>
</HStack>
</Box>
<PlayerTimeZone player={player} />
<Box>
<Text fontSize="xs" color="blueLight" casing="uppercase" mb={3}>
Availability

View File

@@ -10,7 +10,7 @@ import {
} from '@metafam/ds';
import { FlexContainer } from 'components/Container';
import { useSetupFlow } from 'contexts/SetupContext';
import { useUpdatePlayerSkillsMutation } from 'graphql/autogen/types';
import { useUpdateProfileMutation } from 'graphql/autogen/types';
import { useUser } from 'lib/hooks';
import React, { useEffect, useState } from 'react';
@@ -20,7 +20,6 @@ export const SetupAvailability: React.FC = () => {
nextButtonLabel,
availability,
setAvailability,
skills,
} = useSetupFlow();
const [invalid, setInvalid] = useState(false);
const { user } = useUser({ redirectTo: '/' });
@@ -31,14 +30,16 @@ export const SetupAvailability: React.FC = () => {
setInvalid(value < 0 || value > 168);
}, [availability]);
const [updateSkillsRes, updateSkills] = useUpdatePlayerSkillsMutation();
const [updateProfileRes, updateProfile] = useUpdateProfileMutation();
const handleNextPress = async () => {
if (!user) return;
const { error } = await updateSkills({
availability_hours: Number(availability),
skills: skills.map((s) => ({ skill_id: s.id })),
const { error } = await updateProfile({
playerId: user.id,
input: {
availability_hours: Number(availability)
}
});
if (error) {
@@ -46,7 +47,7 @@ export const SetupAvailability: React.FC = () => {
console.warn(error);
toast({
title: 'Error',
description: 'Unable to update Player Skills. The octo is sad 😢',
description: 'Unable to update availability. The octo is sad 😢',
status: 'error',
isClosable: true,
});
@@ -87,7 +88,7 @@ export const SetupAvailability: React.FC = () => {
onClick={handleNextPress}
mt={10}
isDisabled={invalid}
isLoading={updateSkillsRes.fetching}
isLoading={updateProfileRes.fetching}
loadingText="Saving"
>
{nextButtonLabel}

View File

@@ -19,6 +19,8 @@ export const SetupProfile: React.FC = () => {
setPlayerType,
availability,
setAvailability,
timeZone,
setTimeZone,
skills,
setSkills,
} = useSetupFlow();
@@ -26,35 +28,39 @@ export const SetupProfile: React.FC = () => {
const { address } = useWeb3();
useEffect(() => {
if (user?.player) {
const {player} = user;
if (
user.player.username &&
user.player.username.toLowerCase() !== address?.toLowerCase() &&
player.username &&
player.username.toLowerCase() !== address?.toLowerCase() &&
!username
) {
setUsername(user.player.username);
setUsername(player.username);
}
if (user.player.availability_hours && !availability) {
setAvailability(user.player.availability_hours.toString());
if (player.availability_hours && !availability) {
setAvailability(player.availability_hours.toString());
}
if (user.player.EnneagramType && !personalityType) {
setPersonalityType(PersonalityTypes[user.player.EnneagramType.name]);
if (player.EnneagramType && !personalityType) {
setPersonalityType(PersonalityTypes[player.EnneagramType.name]);
}
if (user.player.playerType && !playerType) {
setPlayerType(user.player.playerType);
if (player.playerType && !playerType) {
setPlayerType(player.playerType);
}
if (
user.player.Player_Skills &&
user.player.Player_Skills.length > 0 &&
player.Player_Skills &&
player.Player_Skills.length > 0 &&
skills.length === 0
) {
setSkills(
user.player.Player_Skills.map((s) => ({
player.Player_Skills.map((s) => ({
value: s.Skill.id,
label: s.Skill.name,
...s.Skill,
})),
);
}
if (player.tz && !timeZone) {
setTimeZone(player.tz);
}
}
}, [
user,
@@ -67,6 +73,8 @@ export const SetupProfile: React.FC = () => {
setPlayerType,
availability,
setAvailability,
timeZone,
setTimeZone,
skills,
setSkills,
]);

View File

@@ -4,11 +4,13 @@ import {
MetaTheme,
SelectSearch,
selectStyles,
useToast,
} from '@metafam/ds';
import { FlexContainer } from 'components/Container';
import { useSetupFlow } from 'contexts/SetupContext';
import { SkillCategory_Enum } from 'graphql/autogen/types';
import { SkillCategory_Enum, useUpdatePlayerSkillsMutation } from 'graphql/autogen/types';
import { SkillColors } from 'graphql/types';
import { useUser } from 'lib/hooks';
import React from 'react';
import { SkillOption } from 'utils/skillHelpers';
@@ -20,6 +22,31 @@ export const SetupSkills: React.FC = () => {
onNextPress,
nextButtonLabel,
} = useSetupFlow();
const { user } = useUser({ redirectTo: '/' });
const toast = useToast();
const [updateSkillsRes, updateSkills] = useUpdatePlayerSkillsMutation();
const handleNextPress = async () => {
if (!user) return;
const { error } = await updateSkills({
skills: skills.map((s) => ({ skill_id: s.id })),
});
if (error) {
toast({
title: 'Error',
description: 'Unable to update player skills. The octo is sad 😢',
status: 'error',
isClosable: true,
});
return;
}
onNextPress();
};
const styles: typeof selectStyles = {
...selectStyles,
@@ -60,7 +87,12 @@ export const SetupSkills: React.FC = () => {
placeholder="ADD YOUR SKILLS"
/>
</FlexContainer>
<MetaButton onClick={onNextPress} mt={10}>
<MetaButton
onClick={handleNextPress}
mt={10}
isLoading={updateSkillsRes.fetching}
loadingText="Saving"
>
{nextButtonLabel}
</MetaButton>
</FlexContainer>

View File

@@ -0,0 +1,70 @@
import {
MetaButton,
MetaHeading,
SelectTimeZone,
useToast,
} from '@metafam/ds';
import { FlexContainer } from 'components/Container';
import { useSetupFlow } from 'contexts/SetupContext';
import { useUpdateProfileMutation } from 'graphql/autogen/types';
import { useUser } from 'lib/hooks';
import React from 'react';
export const SetupTimeZone: React.FC = () => {
const {
onNextPress,
nextButtonLabel,
timeZone,
setTimeZone
} = useSetupFlow();
const { user } = useUser({ redirectTo: '/' });
const toast = useToast();
const [updateProfileRes, updateProfile] = useUpdateProfileMutation();
const handleNextPress = async () => {
if (!user) return;
const { error } = await updateProfile({
playerId: user.id,
input: {
tz: timeZone
}
});
if (error) {
toast({
title: 'Error',
description: 'Unable to update time zone. The octo is sad 😢',
status: 'error',
isClosable: true,
});
return;
}
onNextPress();
};
return (
<FlexContainer>
<MetaHeading mb={10} mt={-64} textAlign="center">
Which time zone are you in?
</MetaHeading>
<FlexContainer w="100%" align="stretch" maxW="30rem">
<SelectTimeZone
value={timeZone}
onChange={(tz) => setTimeZone(tz.value)}
labelStyle="abbrev"
/>
</FlexContainer>
<MetaButton
onClick={handleNextPress}
mt={10}
isLoading={updateProfileRes.fetching}
loadingText="Saving"
>
{nextButtonLabel}
</MetaButton>
</FlexContainer>
);
};

View File

@@ -38,6 +38,8 @@ type SetupContextType = {
setPlayerType: React.Dispatch<React.SetStateAction<PlayerType | undefined>>;
availability: string;
setAvailability: React.Dispatch<React.SetStateAction<string>>;
timeZone: string;
setTimeZone: React.Dispatch<React.SetStateAction<string>>;
memberships: Array<Membership> | null | undefined;
setMemberships: React.Dispatch<
React.SetStateAction<Array<Membership> | null | undefined>
@@ -65,6 +67,8 @@ export const SetupContext = React.createContext<SetupContextType>({
setPlayerType: () => undefined,
availability: '',
setAvailability: () => undefined,
timeZone: '',
setTimeZone: () => undefined,
memberships: undefined,
setMemberships: () => undefined,
});
@@ -133,6 +137,7 @@ export const SetupContextProvider: React.FC<Props> = ({
const [personalityType, setPersonalityType] = useState<PersonalityType>();
const [playerType, setPlayerType] = useState<PlayerType>();
const [availability, setAvailability] = useState<string>('');
const [timeZone, setTimeZone] = useState<string>('');
const [memberships, setMemberships] = useState<
Array<Membership> | null | undefined
>();
@@ -166,6 +171,9 @@ export const SetupContextProvider: React.FC<Props> = ({
// availability
availability,
setAvailability,
// time zone
timeZone,
setTimeZone,
// memberships
memberships,
setMemberships,

View File

@@ -8,6 +8,7 @@ export const PlayerFragment = gql`
rank
ethereum_address
availability_hours
tz
EnneagramType {
description
name

View File

@@ -4,7 +4,6 @@ export const UpdateAboutYouMutation = gql`
mutation UpdateAboutYou($playerId: uuid!, $input: Player_set_input!) {
update_Player_by_pk(pk_columns: { id: $playerId }, _set: $input) {
enneagram
availability_hours
playerType {
description
id

View File

@@ -0,0 +1,11 @@
import gql from 'fake-tag';
export const UpdateProfileMutation = gql`
mutation UpdateProfile($playerId: uuid!, $input: Player_set_input!) {
update_Player_by_pk(pk_columns: { id: $playerId }, _set: $input) {
id
availability_hours
tz
}
}
`;

View File

@@ -2,7 +2,6 @@ import gql from 'fake-tag';
export const UpdateSkillsMutation = gql`
mutation UpdatePlayerSkills(
$availability_hours: Int!
$skills: [Player_Skill_insert_input!]!
) {
delete_Player_Skill(where: {}) {
@@ -11,21 +10,5 @@ export const UpdateSkillsMutation = gql`
insert_Player_Skill(objects: $skills) {
affected_rows
}
update_Player(
_set: { availability_hours: $availability_hours }
where: {}
) {
returning {
id
availability_hours
Player_Skills {
Skill {
id
category
name
}
}
}
}
}
`;

View File

@@ -29,6 +29,8 @@
"react-dom": "^16.13.1",
"react-icons": "^3.11.0",
"react-is": "16.13.1",
"spacetime": "^6.12.1",
"spacetime-informal": "^0.5.0",
"urql": "^1.9.7",
"web3modal": "^1.9.0"
}

View File

@@ -0,0 +1,36 @@
import { PlayerFragmentFragment } from "graphql/autogen/types";
import spacetime from 'spacetime';
import { display } from 'spacetime-informal';
export interface TimeZoneDisplay {
timeZone?: string;
offset?: string;
}
export const getPlayerTimeZoneDisplay = (player: PlayerFragmentFragment): TimeZoneDisplay => {
let tzLabel;
let offsetLabel;
if (player?.tz) {
const timeZone = spacetime.now().goto(player.tz)
const tzDisplay = display(player.tz)
if (tzDisplay && tzDisplay.daylight && tzDisplay.standard) {
tzLabel = timeZone.isDST()
? tzDisplay.daylight.abbrev
: tzDisplay.standard.abbrev;
const {offset} = timeZone.timezone().current;
if (offset > 0) {
offsetLabel = `(GMT +${offset})`;
} else if (offset < 0) {
offsetLabel = `(GMT ${offset})`;
}
} else {
tzLabel = player.tz;
}
}
return {
timeZone: tzLabel,
offset: offsetLabel
}
}

View File

@@ -4,6 +4,7 @@ import { SetupMemberships } from 'components/Setup/SetupMemberships';
import { SetupPersonalityType } from 'components/Setup/SetupPersonalityType';
import { SetupPlayerType } from 'components/Setup/SetupPlayerType';
import { SetupSkills } from 'components/Setup/SetupSkills';
import { SetupTimeZone } from 'components/Setup/SetupTimeZone';
import { SetupUsername } from 'components/Setup/SetupUsername';
import React from 'react';
@@ -42,6 +43,10 @@ export const options = [
label: 'Availability',
component: <SetupAvailability />,
},
{
label: 'Time Zone',
component: <SetupTimeZone />
},
{
label: 'Memberships',
component: <SetupMemberships />,

View File

@@ -33,3 +33,8 @@ export const parseSkills = (
});
return Object.values(skillsMap);
};
export type TimeZoneOption = {
value: string;
label: string;
};

View File

@@ -25115,6 +25115,15 @@ react-textarea-autosize@^8.1.1:
use-composed-ref "^1.0.0"
use-latest "^1.0.0"
react-timezone-select@^0.9.7:
version "0.9.8"
resolved "https://registry.yarnpkg.com/react-timezone-select/-/react-timezone-select-0.9.8.tgz#a5d9ead1fa0b40dab1b3a77d9cdcc20354fe2ec4"
integrity sha512-CzIOs9IwAb5hmjs5jnB1uShQvw9zJGUVeH5dIpRVy1dgVnwkasvXc2fGwnggI6nYXAmN7uOCt5kFIcQOpZmUNw==
dependencies:
react-select "^3.1.0"
spacetime "^6.6.2"
spacetime-informal "^0.3.0"
react-transition-group@4.4.1, react-transition-group@^4.3.0, react-transition-group@^4.4.0, react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@@ -27104,6 +27113,21 @@ space-separated-tokens@^1.0.0, space-separated-tokens@^1.1.2:
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
integrity sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==
spacetime-informal@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/spacetime-informal/-/spacetime-informal-0.3.0.tgz#6d0feffe291697f686b737b678b59ee2dcef3297"
integrity sha512-HtFTwtkzDl7gswCfeWPQAxvQ+Fmrdt7WqoDT0Lq2FX7RVZKZ/+zmgBOAVAuH8oVcf4uXza9NvdX9QppGRiG8oQ==
spacetime-informal@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/spacetime-informal/-/spacetime-informal-0.5.0.tgz#0301d45621d7207d6573d97ba0e6700dab37c667"
integrity sha512-cdSsniJJfJJTBdeVvXtooxyXzrRfoBVjAl3usQl9DgGExB3XN3deA3MwjInnD/26C/lANf3dU54bT2YweAGrOw==
spacetime@^6.12.1, spacetime@^6.6.2:
version "6.12.1"
resolved "https://registry.yarnpkg.com/spacetime/-/spacetime-6.12.1.tgz#a0a7b583fe2646b4288bd7456bcf027337a33ba6"
integrity sha512-mUA1xnVDX0RxF+HaAA0pOui3ePjTCkVx81DsMpSVwidQJjJgMxD39N5cKjCkOqDKKHjPmq73cF6X14b6+Oy9aA==
sparse-array@^1.3.1:
version "1.3.2"
resolved "https://registry.yarnpkg.com/sparse-array/-/sparse-array-1.3.2.tgz#0e1a8b71706d356bc916fe754ff496d450ec20b0"