Refactor node packages to yarn workspaces & ditch next.js for CRA. (#161)

* chore: restructure project into yarn workspaces, remove next

* fix tests, remove webapp from terraform

* remove more ui deployment bits

* remove pages, fix FUNDING.yml

* remove isomorphism

* remove next providers

* fix linting issues

* feat: start basis of new web ui system on CRA

* chore: move types to @roleypoly/types package

* chore: move src/common/utils to @roleypoly/misc-utils

* chore: remove roleypoly/ path remappers

* chore: renmove vercel config

* chore: re-add worker-types to api package

* chore: fix type linting scope for api

* fix(web): craco should include all of packages dir

* fix(ci): change api webpack path for wrangler

* chore: remove GAR actions from CI

* chore: update codeql job

* chore: test better github dar matcher in lint-staged
This commit is contained in:
41666 2021-03-12 18:04:49 -05:00 committed by GitHub
parent 49e308507e
commit 2ff6588030
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
328 changed files with 16624 additions and 3525 deletions

View file

@ -0,0 +1,8 @@
import * as React from 'react';
import { DemoDiscord } from './DemoDiscord';
export default {
title: 'Molecules/Role Demos',
};
export const Discord = () => <DemoDiscord />;

View file

@ -0,0 +1,67 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import styled, { keyframes } from 'styled-components';
export const Base = styled.div`
background-color: ${palette.discord100};
border: solid 1px rgba(0, 0, 0, 0.15);
border-radius: 3px;
padding: 10px;
user-select: none;
`;
export const Timestamp = styled.span`
padding: 0 5px;
font-size: 0.7em;
opacity: 0.3;
`;
export const TextParts = styled.span`
padding: 0 5px;
`;
export const Username = styled(TextParts)`
&:hover {
text-decoration: underline;
cursor: pointer;
}
`;
export const InputBox = styled.div`
margin-top: 10px;
background-color: ${palette.discord200};
padding: 7px 10px;
border-radius: 3px;
`;
const lineBlink = keyframes`
0% {
opacity: 1;
}
40% {
opacity: 1;
}
60% {
opacity: 0;
}
100% {
opacity: 0;
}
`;
export const Line = styled.div`
background-color: ${palette.grey600};
width: 1px;
height: 1.5em;
display: inline-block;
position: absolute;
right: -5px;
animation: ${lineBlink} 0.5s ease-in-out infinite alternate-reverse;
`;
export const InputTextAlignment = styled.div`
position: relative;
display: inline-block;
`;

View file

@ -0,0 +1,53 @@
import { Typist } from '@roleypoly/design-system/atoms/typist';
import { demoData } from '@roleypoly/types/demoData';
import * as React from 'react';
import {
Base,
InputBox,
InputTextAlignment,
Line,
TextParts,
Timestamp,
Username,
} from './DemoDiscord.styled';
export const DemoDiscord = () => {
const time = new Date();
const timeString = time.toTimeString();
const [easterEggCount, setEasterEggCount] = React.useState(0);
return (
<Base>
<Timestamp>
{time.getHours() % 12}:{timeString.slice(3, 5)}&nbsp;
{time.getHours() <= 12 ? 'AM' : 'PM'}
</Timestamp>
<Username onClick={() => setEasterEggCount(easterEggCount + 1)}>
okano&nbsp;cat
</Username>
<TextParts>
{easterEggCount >= 15
? `NYAAAAAAA${'A'.repeat(easterEggCount - 15)}`
: easterEggCount >= 11
? `I'm.. I'm gonna...`
: easterEggCount >= 10
? `S-senpai... Be careful...`
: easterEggCount >= 5
? `H-hey... Stop that..`
: `Hey, I'd like some roles!`}
</TextParts>
<InputBox>
<InputTextAlignment>
&nbsp;
<Typist
resetTimeout={2000}
charTimeout={75}
lines={demoData.map((role) => `.iam ${role.name}`)}
/>
<Line />
</InputTextAlignment>
</InputBox>
</Base>
);
};

View file

@ -0,0 +1 @@
export * from './DemoDiscord';

View file

@ -0,0 +1,8 @@
import * as React from 'react';
import { DemoPicker } from './DemoPicker';
export default {
title: 'Molecules/Role Demos',
};
export const Picker = () => <DemoPicker />;

View file

@ -0,0 +1,46 @@
import { Role } from '@roleypoly/design-system/atoms/role';
import { Role as RPCRole } from '@roleypoly/types';
import { demoData } from '@roleypoly/types/demoData';
import * as React from 'react';
import styled from 'styled-components';
const Container = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
align-content: center;
height: 95px;
`;
const RoleWrap = styled.div`
padding: 2.5px;
display: inline-block;
`;
export const DemoPicker = () => {
const [selectedStates, setSelectedStates] = React.useState<
{
[key in RPCRole['id']]: boolean;
}
>(demoData.reduce((acc, role) => ({ ...acc, [role.id]: false }), {}));
return (
<Container>
{demoData.map((role) => (
<RoleWrap key={`role${role.id}`}>
<Role
role={role}
selected={selectedStates[role.id]}
onClick={() => {
setSelectedStates({
...selectedStates,
[role.id]: !selectedStates[role.id],
});
}}
/>
</RoleWrap>
))}
</Container>
);
};

View file

@ -0,0 +1 @@
export * from './DemoPicker';

View file

@ -0,0 +1,19 @@
import * as React from 'react';
import { mockCategory, roleCategory, roleCategory2 } from '../../fixtures/storyData';
import { EditorCategory } from './EditorCategory';
export default {
title: 'Molecules/Editor/Category',
};
export const CategoryEditor = () => {
const [categoryData, setCategoryData] = React.useState(mockCategory);
return (
<EditorCategory
category={categoryData}
onChange={(category) => setCategoryData(category)}
uncategorizedRoles={roleCategory}
guildRoles={[...roleCategory, ...roleCategory2]}
/>
);
};

View file

@ -0,0 +1,6 @@
import styled from 'styled-components';
export const RoleContainer = styled.div`
display: flex;
margin: 10px;
`;

View file

@ -0,0 +1,145 @@
import { FaderOpacity } from '@roleypoly/design-system/atoms/fader';
import { HorizontalSwitch } from '@roleypoly/design-system/atoms/horizontal-switch';
import { Popover } from '@roleypoly/design-system/atoms/popover';
import { Role } from '@roleypoly/design-system/atoms/role';
import { Space } from '@roleypoly/design-system/atoms/space';
import { TextInput, TextInputWithIcon } from '@roleypoly/design-system/atoms/text-input';
import { Text } from '@roleypoly/design-system/atoms/typography';
import { RoleSearch } from '@roleypoly/design-system/molecules/role-search';
import { Category, CategoryType, Role as RoleType } from '@roleypoly/types';
import * as React from 'react';
import { GoSearch } from 'react-icons/go';
import { RoleContainer } from './EditorCategory.styled';
type Props = {
category: Category;
uncategorizedRoles: RoleType[];
guildRoles: RoleType[];
onChange: (category: Category) => void;
};
const typeEnumToSwitch = (typeData: CategoryType) => {
if (typeData === CategoryType.Single) {
return 'Single';
} else {
return 'Multiple';
}
};
const switchToTypeEnum = (typeData: 'Single' | 'Multiple') => {
if (typeData === 'Single') {
return CategoryType.Single;
} else {
return CategoryType.Multi;
}
};
export const EditorCategory = (props: Props) => {
const [roleSearchPopoverActive, setRoleSearchPopoverActive] = React.useState(false);
const [roleSearchTerm, updateSearchTerm] = React.useState('');
const onUpdate = (
key: keyof typeof props.category,
pred?: (newValue: any) => any
) => (newValue: any) => {
props.onChange({
...props.category,
[key]: pred ? pred(newValue) : newValue,
});
};
const handleRoleSelect = (role: RoleType) => {
setRoleSearchPopoverActive(false);
updateSearchTerm('');
props.onChange({
...props.category,
roles: [...props.category.roles, role.id],
});
};
const handleRoleDeselect = (role: RoleType) => () => {
props.onChange({
...props.category,
roles: props.category.roles.filter((x) => x !== role.id),
});
};
return (
<div>
<Text>Category Name</Text>
<TextInput
placeholder="Pronouns, Political, Colors..."
value={props.category.name}
onChange={onUpdate('name', (x) => x.target.value)}
/>
<Space />
<Text>Selection Type</Text>
<div>
<HorizontalSwitch
items={['Multiple', 'Single']}
value={typeEnumToSwitch(props.category.type)}
onChange={onUpdate('type', switchToTypeEnum)}
/>
</div>
<Space />
<Text>Visiblity</Text>
<div>
<HorizontalSwitch
items={['Visible', 'Hidden']}
value={props.category.hidden ? 'Hidden' : 'Visible'}
onChange={onUpdate('hidden', (a) => a === 'Hidden')}
/>
</div>
<Space />
<Text>Roles</Text>
<Popover
position={'top left'}
headContent={null}
active={roleSearchPopoverActive}
onExit={() => setRoleSearchPopoverActive(false)}
>
{() => (
<RoleSearch
placeholder={'Type or drag a role...'}
roles={props.uncategorizedRoles}
onSelect={handleRoleSelect}
searchTerm={roleSearchTerm}
onSearchUpdate={(newTerm) => updateSearchTerm(newTerm)}
/>
)}
</Popover>
<FaderOpacity isVisible={!roleSearchPopoverActive}>
<TextInputWithIcon
icon={<GoSearch />}
placeholder={'Type or drag a role...'}
onFocus={() => setRoleSearchPopoverActive(true)}
value={roleSearchTerm}
onChange={(x) => updateSearchTerm(x.target.value)}
/>
<RoleContainer>
{props.category.roles.map((id) => {
const role = props.guildRoles.find((x) => x.id === id);
if (!role) {
return <></>;
}
return (
<Role
role={role}
selected={false}
key={id}
type="delete"
onClick={handleRoleDeselect(role)}
/>
);
})}
</RoleContainer>
</FaderOpacity>
</div>
);
};

View file

@ -0,0 +1 @@
export * from './EditorCategory';

View file

@ -0,0 +1,26 @@
import * as React from 'react';
import { ErrorBanner } from './ErrorBanner';
export default {
title: 'Molecules/Error Banner',
argTypes: {
english: { control: 'text' },
japanese: { control: 'text' },
friendlyCode: { control: 'text' },
},
args: {
english: 'Oh no! I lost it!',
japanese: 'ちょっとにんげんだよ',
friendlyCode: '404',
},
};
export const ErrorBanner_ = ({ english, japanese, friendlyCode }) => (
<ErrorBanner
message={{
english,
japanese,
friendlyCode,
}}
/>
);

View file

@ -0,0 +1,41 @@
import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
import { palette } from '@roleypoly/design-system/atoms/colors';
import { text300, text500, text700 } from '@roleypoly/design-system/atoms/typography';
import styled, { css } from 'styled-components';
export const ErrorWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
${onSmallScreen(css`
display: block;
text-align: center;
`)}
`;
export const ErrorDivider = styled.div`
width: 1px;
height: 3em;
background: ${palette.grey600};
margin: 0 1em;
${onSmallScreen(css`
display: none;
`)}
`;
export const ErrorSideCode = styled.div`
${text700}
${onSmallScreen(css`
margin-bottom: 0.4em;
`)}
`;
export const ErrorText = styled.div`
${text500}
`;
export const ErrorTextLower = styled.div`
${text300}
color: ${palette.taupe500};
`;

View file

@ -0,0 +1,29 @@
import * as React from 'react';
import {
ErrorDivider,
ErrorSideCode,
ErrorText,
ErrorTextLower,
ErrorWrapper,
} from './ErrorBanner.styled';
export type ErrorMessage = {
english: string;
japanese?: string;
friendlyCode?: string;
};
type ErrorBannerProps = {
message: Required<ErrorMessage>;
};
export const ErrorBanner = (props: ErrorBannerProps) => (
<ErrorWrapper>
<ErrorSideCode>{props.message.friendlyCode}</ErrorSideCode>
<ErrorDivider />
<div>
<ErrorText>{props.message.english}</ErrorText>
<ErrorTextLower>{props.message.japanese}</ErrorTextLower>
</div>
</ErrorWrapper>
);

View file

@ -0,0 +1 @@
export * from './ErrorBanner';

View file

@ -0,0 +1,94 @@
import * as React from 'react';
type FlagsProps = {
width?: number | string;
height?: number | string;
};
export const Flags = (props: FlagsProps) => (
<svg width={props.width} height={props.height} viewBox="0 0 3372 900" version="1.1">
<defs>
<rect id="path-3" x="1772" y="0" width="1600" height="900" rx="100"></rect>
</defs>
<g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
<g id="Rectangle-6"></g>
<g id="Trans">
<rect
id="Rectangle"
fill="#55CDFC"
x="0"
y="0"
width="1600"
height="900"
rx="100"
></rect>
<rect
id="Rectangle-2"
fill="#F7A8B8"
x="0"
y="170"
width="1600"
height="560"
></rect>
<rect
id="Rectangle-3"
fill="#FFFFFF"
x="0"
y="350"
width="1600"
height="200"
></rect>
</g>
<mask id="mask-4" fill="white">
<use href="#path-3"></use>
</mask>
<g id="Rectangle-5"></g>
<g id="Geyy" mask="url(#mask-4)">
<g transform="translate(1772.000000, 0.000000)" id="Rectangle-4">
<rect
fill="#F9238B"
x="0"
y="0"
width="1600"
height="151.006711"
></rect>
<rect
fill="#FB7B04"
x="0"
y="150"
width="1600"
height="151.006711"
></rect>
<rect
fill="#FFCA66"
x="0"
y="300"
width="1600"
height="151.006711"
></rect>
<rect
fill="#00B289"
x="0"
y="450"
width="1600"
height="151.006711"
></rect>
<rect
fill="#5A38B5"
x="0"
y="598.993289"
width="1600"
height="151.006711"
></rect>
<rect
fill="#B413F5"
x="0"
y="748.993289"
width="1600"
height="151.006711"
></rect>
</g>
</g>
</g>
</svg>
);

View file

@ -0,0 +1,9 @@
import * as React from 'react';
import { Footer as FooterComponent } from './Footer';
export default {
title: 'Molecules',
component: FooterComponent,
};
export const Footer = (args) => <FooterComponent {...args} />;

View file

@ -0,0 +1,30 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import { transitions } from '@roleypoly/design-system/atoms/timings';
import styled from 'styled-components';
export const FooterWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
text-align: center;
a {
color: ${palette.taupe500};
text-decoration: none;
transition: color ${transitions.actionable}s ease-in-out;
&:hover {
color: ${palette.taupe600};
}
}
`;
export const HoverColor = styled.div`
opacity: 0.3;
filter: saturate(0);
transition: all ${transitions.in2in}s ease-in-out;
&:hover {
opacity: 1;
filter: none;
}
`;

View file

@ -0,0 +1,27 @@
import { AmbientLarge } from '@roleypoly/design-system/atoms/typography';
import * as React from 'react';
import { FaHeart } from 'react-icons/fa';
import { Flags } from './Flags';
import { FooterWrapper, HoverColor } from './Footer.styled';
const year = new Date().getFullYear();
export const Footer = () => (
<FooterWrapper>
<AmbientLarge>
<div>
&copy; {year} Roleypoly &ndash; Made with{' '}
<FaHeart size={'0.8em'} color={'#fe4365'} />
&nbsp;in Raleigh, NC
</div>
<div>
<a href="https://discord.gg/Xj6rK3E">Discord/Support</a> &ndash;&nbsp;
<a href="https://patreon.com/kata">Patreon</a> &ndash;&nbsp;
<a href="https://github.com/roleypoly">GitHub</a>
</div>
<HoverColor>
<Flags height={'1em'} />
</HoverColor>
</AmbientLarge>
</FooterWrapper>
);

View file

@ -0,0 +1 @@
export * from './Footer';

View file

@ -0,0 +1,21 @@
import { PopoverBase } from '@roleypoly/design-system/atoms/popover/Popover.styled';
import * as React from 'react';
import { mastheadSlugs } from '../../fixtures/storyData';
import { GuildNav } from './GuildNav';
export default {
title: 'Molecules/Guild Nav',
component: GuildNav,
};
export const HasGuilds = () => (
<PopoverBase active>
<GuildNav guilds={mastheadSlugs} />
</PopoverBase>
);
export const NoGuilds = () => (
<PopoverBase active>
<GuildNav guilds={[]} />
</PopoverBase>
);

View file

@ -0,0 +1,21 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import { transitions } from '@roleypoly/design-system/atoms/timings';
import styled from 'styled-components';
export const GuildNavItem = styled.a`
display: flex;
align-items: center;
transition: border ${transitions.in2in}s ease-in-out;
border: 1px solid transparent;
border-radius: 3px;
box-sizing: border-box;
margin: 5px;
user-select: none;
color: unset;
text-decoration: none;
&:hover {
border-color: ${palette.taupe300};
cursor: pointer;
}
`;

View file

@ -0,0 +1,49 @@
import { NavSlug } from '@roleypoly/design-system/molecules/nav-slug';
import { sortBy } from '@roleypoly/misc-utils/sortBy';
import { GuildSlug, UserGuildPermissions } from '@roleypoly/types';
import * as React from 'react';
import Scrollbars from 'react-custom-scrollbars';
import { GoStar, GoZap } from 'react-icons/go';
import ReactTooltip from 'react-tooltip';
import { GuildNavItem } from './GuildNav.styled';
type Props = {
guilds: GuildSlug[];
};
const tooltipId = 'guildnav';
const Badges = (props: { guild: GuildSlug }) => {
return React.useMemo(() => {
if (props.guild.permissionLevel === UserGuildPermissions.Admin) {
return <GoStar data-tip="Administrator" data-for={tooltipId} />;
}
if (props.guild.permissionLevel === UserGuildPermissions.Manager) {
return <GoZap data-tip="Role Editor" data-for={tooltipId} />;
}
return null;
}, [props.guild.permissionLevel]);
};
export const GuildNav = (props: Props) => (
<div>
<Scrollbars
universal
autoHide
// autoHeight
style={{ height: 'calc(100vh - 45px - 1.4em)', overflowX: 'hidden' }}
>
{sortBy(props.guilds, 'name', (a: string, b: string) =>
a.toLowerCase() > b.toLowerCase() ? 1 : -1
).map((guild) => (
<GuildNavItem href={`/s/${guild.id}`} key={guild.id}>
<NavSlug guild={guild || null} key={guild.id} />
<Badges guild={guild} />
</GuildNavItem>
))}
<ReactTooltip id={tooltipId} />
</Scrollbars>
</div>
);

View file

@ -0,0 +1 @@
export * from './GuildNav';

View file

@ -0,0 +1,10 @@
load("//hack/bazel/js:react.bzl", "react_library")
package(default_visibility = ["//visibility:public"])
react_library(
name = "help-page-base",
deps = [
"//src/design-system/atoms/colors",
],
)

View file

@ -0,0 +1,14 @@
import * as React from 'react';
import { HelpStoryWrapper } from './storyDecorator';
export default {
title: 'Molecules/Help Page',
decorators: [HelpStoryWrapper],
};
export const Base = () => (
<>
<h1>What is the world but vibrations?</h1>
<p>Vibrations that synchronize and tie it together, running free forever.</p>
</>
);

View file

@ -0,0 +1,21 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import * as React from 'react';
import styled from 'styled-components';
export type HelpPageProps = {
children: React.ReactNode;
};
const Container = styled.div`
background: ${palette.taupe300};
padding: 2em 3em;
width: 1024px;
max-width: 98vw;
margin: 0 auto;
margin-top: 75px;
box-sizing: border-box;
`;
export const HelpPageBase = (props: HelpPageProps) => (
<Container>{props.children}</Container>
);

View file

@ -0,0 +1 @@
export * from './HelpPageBase';

View file

@ -0,0 +1,9 @@
import { Content } from '@roleypoly/design-system/organisms/app-shell/AppShell.styled';
import * as React from 'react';
import { HelpPageBase } from './HelpPageBase';
export const HelpStoryWrapper = (storyFn: any): React.ReactNode => (
<Content>
<HelpPageBase>{storyFn()}</HelpPageBase>
</Content>
);

View file

@ -0,0 +1,11 @@
import * as React from 'react';
import { guild } from '../../fixtures/storyData';
import { NavSlug } from './NavSlug';
export default {
title: 'Molecules/Server Slug',
component: NavSlug,
};
export const Empty = () => <NavSlug guild={null} />;
export const Example = () => <NavSlug guild={guild} />;

View file

@ -0,0 +1,16 @@
import styled from 'styled-components';
export const SlugContainer = styled.div`
display: flex;
align-items: center;
justify-content: flex-start;
padding: 5px;
`;
export const SlugName = styled.div`
padding: 0 10px;
position: relative;
top: -1px;
white-space: nowrap;
text-overflow: ellipsis;
`;

View file

@ -0,0 +1,27 @@
import { Avatar, utils } from '@roleypoly/design-system/atoms/avatar';
import { GuildSlug } from '@roleypoly/types';
import * as React from 'react';
import { GoOrganization } from 'react-icons/go';
import { SlugContainer, SlugName } from './NavSlug.styled';
type Props = {
guild: GuildSlug | null;
};
export const NavSlug = (props: Props) => (
<SlugContainer>
<Avatar
hash={props.guild ? props.guild.icon : undefined}
src={
props.guild
? utils.avatarHash(props.guild.id, props.guild.icon)
: undefined
}
deliberatelyEmpty={!props.guild}
size={35}
>
{props.guild ? utils.initialsFromName(props.guild.name) : <GoOrganization />}
</Avatar>
<SlugName>{props.guild?.name || <>Your Guilds</>}</SlugName>
</SlugContainer>
);

View file

@ -0,0 +1 @@
export * from './NavSlug';

View file

@ -0,0 +1,34 @@
import * as React from 'react';
import { mockCategory, roleCategory, roleWikiData } from '../../fixtures/storyData';
import { PickerCategory } from './PickerCategory';
export default {
title: 'Molecules/Picker Category',
component: PickerCategory,
args: {
title: 'Pronouns',
roles: roleCategory,
category: mockCategory,
selectedRoles: [],
},
};
export const Default = (args) => {
return <PickerCategory {...args} />;
};
export const Single = (args) => {
return <PickerCategory {...args} type="single" />;
};
Single.args = {
type: 'single',
};
export const Multi = (args) => {
return <PickerCategory {...args} type="single" />;
};
Multi.args = {
type: 'multi',
};
export const Wiki = (args) => {
return <PickerCategory {...args} wikiMode roleWikiData={roleWikiData} />;
};

View file

@ -0,0 +1,20 @@
import styled from 'styled-components';
export const Head = styled.div`
margin: 7px 5px;
line-height: 200%;
display: flex;
align-items: center;
justify-content: space-between;
`;
export const HeadTitle = styled.div`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`;
export const HeadSub = styled.div`
flex-shrink: 0;
margin-top: -4px;
`;

View file

@ -0,0 +1,64 @@
import { Role } from '@roleypoly/design-system/atoms/role';
import { AmbientLarge, LargeText } from '@roleypoly/design-system/atoms/typography';
import { sortBy } from '@roleypoly/misc-utils/sortBy';
import { Category as RPCCategory, Role as RPCRole, RoleSafety } from '@roleypoly/types';
import * as React from 'react';
import ReactTooltip from 'react-tooltip';
import styled from 'styled-components';
import { Head, HeadSub, HeadTitle } from './PickerCategory.styled';
export type CategoryProps = {
title: string;
roles: RPCRole[];
category: RPCCategory;
selectedRoles: string[];
onChange: (role: RPCRole) => (newState: boolean) => void;
type: 'single' | 'multi';
} & (
| {
wikiMode: true;
roleWikiData: { [roleId: string]: string };
}
| {
wikiMode: false;
}
);
const Category = styled.div`
display: flex;
flex-wrap: wrap;
`;
const Container = styled.div`
overflow: hidden;
padding: 5px;
`;
export const PickerCategory = (props: CategoryProps) => (
<div>
<Head>
<HeadTitle>
<LargeText>{props.title}</LargeText>
</HeadTitle>
{props.type === 'single' && (
<HeadSub>
<AmbientLarge>Pick one</AmbientLarge>
</HeadSub>
)}
</Head>
<Category>
{sortBy(props.roles, 'position').map((role, idx) => (
<Container key={idx}>
<Role
role={role}
selected={props.selectedRoles.includes(role.id)}
onClick={props.onChange(role)}
disabled={role.safety !== RoleSafety.Safe}
tooltipId={props.category.id}
/>
</Container>
))}
</Category>
<ReactTooltip id={props.category.id} />
</div>
);

View file

@ -0,0 +1 @@
export { PickerCategory } from './PickerCategory';

View file

@ -0,0 +1,13 @@
import * as React from 'react';
import { mastheadSlugs } from '../../fixtures/storyData';
import { PreauthGreeting } from './PreauthGreeting';
export default {
title: 'Molecules/Preauth/Greeting',
component: PreauthGreeting,
args: {
guildSlug: mastheadSlugs[0],
},
};
export const Greeting = (args) => <PreauthGreeting {...args} />;

View file

@ -0,0 +1,41 @@
import { Avatar, utils as avatarUtils } from '@roleypoly/design-system/atoms/avatar';
import { Space } from '@roleypoly/design-system/atoms/space';
import { AccentTitle } from '@roleypoly/design-system/atoms/typography';
import { GuildSlug } from '@roleypoly/types';
import * as React from 'react';
import styled from 'styled-components';
type GreetingProps = {
guildSlug: GuildSlug;
};
const Center = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
text-align: center;
`;
export const PreauthGreeting = (props: GreetingProps) => (
<Center>
<Avatar
size={64}
src={avatarUtils.avatarHash(
props.guildSlug.id,
props.guildSlug.icon,
'icons',
512
)}
hash={props.guildSlug.icon}
>
{avatarUtils.initialsFromName(props.guildSlug.name)}
</Avatar>
<AccentTitle>
Hi there. <b>{props.guildSlug.name}</b> uses Roleypoly to help assign you
roles.
</AccentTitle>
<Space />
<Space />
</Center>
);

View file

@ -0,0 +1 @@
export * from './PreauthGreeting';

View file

@ -0,0 +1,23 @@
import { Button } from '@roleypoly/design-system/atoms/button';
import { shallow } from 'enzyme';
import * as React from 'react';
import { ResetSubmit } from './ResetSubmit';
const onReset = jest.fn();
const onSubmit = jest.fn();
it('calls onReset when reset is clicked', () => {
const view = shallow(<ResetSubmit onSubmit={onSubmit} onReset={onReset} />);
view.find(Button).at(0).simulate('click');
expect(onReset).toBeCalled();
});
it('calls onSubmit when submit is clicked', () => {
const view = shallow(<ResetSubmit onSubmit={onSubmit} onReset={onReset} />);
view.find(Button).at(1).simulate('click');
expect(onSubmit).toBeCalled();
});

View file

@ -0,0 +1,9 @@
import * as React from 'react';
import { ResetSubmit } from './ResetSubmit';
export default {
title: 'Molecules',
component: ResetSubmit,
};
export const ResetAndSubmit = (args) => <ResetSubmit {...args} />;

View file

@ -0,0 +1,19 @@
import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
import styled from 'styled-components';
export const Buttons = styled.div`
display: flex;
flex-wrap: wrap;
`;
export const Left = styled.div`
flex: 0;
${onSmallScreen`
flex: 1 1 100%;
order: 2;
`}
`;
export const Right = styled.div`
flex: 1;
`;

View file

@ -0,0 +1,42 @@
import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
import { Button } from '@roleypoly/design-system/atoms/button';
import * as React from 'react';
import { MdRestore } from 'react-icons/md';
import styled from 'styled-components';
type Props = {
onSubmit: () => void;
onReset: () => void;
};
const Buttons = styled.div`
display: flex;
flex-wrap: wrap;
`;
const Left = styled.div`
flex: 0;
${onSmallScreen`
flex: 1 1 100%;
order: 2;
`}
`;
const Right = styled.div`
flex: 1;
`;
export const ResetSubmit = (props: Props) => {
return (
<Buttons>
<Left>
<Button color="muted" icon={<MdRestore />} onClick={props.onReset}>
Reset
</Button>
</Left>
<Right>
<Button onClick={props.onSubmit}>Submit</Button>
</Right>
</Buttons>
);
};

View file

@ -0,0 +1 @@
export * from './ResetSubmit';

View file

@ -0,0 +1,14 @@
import * as React from 'react';
import { roleCategory } from '../../fixtures/storyData';
import { RoleSearch } from './RoleSearch';
export default {
title: 'Molecules/Role Search',
component: RoleSearch,
args: {
roles: roleCategory,
searchTerm: '',
},
};
export const Search = (args) => <RoleSearch {...args} />;

View file

@ -0,0 +1,57 @@
import { Role } from '@roleypoly/design-system/atoms/role';
import { Space } from '@roleypoly/design-system/atoms/space';
import { TextInputWithIcon } from '@roleypoly/design-system/atoms/text-input';
import { Role as RoleType } from '@roleypoly/types';
import Fuse from 'fuse.js';
import * as React from 'react';
import { GoSearch } from 'react-icons/go';
import styled from 'styled-components';
type Props = {
roles: RoleType[];
placeholder?: string;
onSelect: (role: RoleType) => void;
onSearchUpdate: (newTerm: string) => void;
searchTerm: string;
};
export const RoleSearch = (props: Props) => {
const fuse = new Fuse(props.roles, { includeScore: true, keys: ['name'] });
const results =
props.searchTerm !== ''
? fuse.search(props.searchTerm)
: props.roles.map((role) => ({
item: role,
}));
const handleClick = (role: RoleType) => () => {
props.onSelect(role);
};
return (
<div>
<TextInputWithIcon
icon={<GoSearch />}
placeholder={props.placeholder || 'Search or drag a role...'}
value={props.searchTerm}
onChange={(x) => props.onSearchUpdate(x.target.value)}
/>
<Space />
{results.map((resultRole, idx) => (
<RoleInliner key={idx}>
<Role
selected={false}
role={resultRole.item}
onClick={handleClick(resultRole.item)}
key={`${idx}role`}
/>
</RoleInliner>
))}
</div>
);
};
const RoleInliner = styled.div`
display: flex;
margin: 5px 0;
`;

View file

@ -0,0 +1 @@
export * from './RoleSearch';

View file

@ -0,0 +1,12 @@
import { roleypolyGuild } from '../../fixtures/storyData';
import { ServerListingCard } from './ServerListingCard';
export default {
title: 'Molecules/Server Listing Card',
component: ServerListingCard,
args: {
guild: { ...roleypolyGuild, permissionLevel: 4 },
},
};
export const serverListingCard = (args) => <ServerListingCard {...args} />;

View file

@ -0,0 +1,90 @@
import { onSmallScreen, onTablet } from '@roleypoly/design-system/atoms/breakpoints';
import { palette } from '@roleypoly/design-system/atoms/colors';
import { transitions } from '@roleypoly/design-system/atoms/timings';
import { text200, text500 } from '@roleypoly/design-system/atoms/typography';
import styled, { css } from 'styled-components';
export const CardLine = styled.div<{ left?: boolean }>`
justify-content: center;
align-items: center;
display: flex;
padding: 5px;
box-sizing: border-box;
${(props) =>
props.left &&
css`
flex: 1;
justify-content: flex-end;
align-items: flex-end;
`}
`;
export const MaxWidthTitle = styled.div`
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const PermissionTagStyled = styled.div<{ hiddenOnSmall?: boolean }>`
${text200}
display: inline-block;
background-color: ${palette.taupe200};
padding: 4px 6px;
border-radius: 2px;
svg {
position: relative;
top: 1px;
${onTablet(
css`
margin-right: 2px;
`
)}
}
${(props) =>
props.hiddenOnSmall &&
onSmallScreen(
css`
display: none;
`
)}
`;
export const CardBase = styled.div`
${text500}
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
background-color: ${palette.taupe300};
overflow-x: hidden;
text-align: center;
display: flex;
align-items: center;
padding: 10px;
border-radius: 3px;
cursor: pointer;
user-select: none;
transform: translate(0);
transition: transform ease-in-out ${transitions.actionable}s,
box-shadow ease-in-out ${transitions.actionable}s,
border-color ease-in-out ${transitions.out2in}s;
box-sizing: border-box;
max-width: 98vw;
:hover {
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.25);
transform: translate(0, -1px);
}
:active {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
transform: translate(0);
}
${onTablet(css`
flex-direction: column;
justify-content: left;
`)}
`;

View file

@ -0,0 +1,58 @@
import { Avatar, utils } from '@roleypoly/design-system/atoms/avatar';
import { Collapse } from '@roleypoly/design-system/atoms/collapse';
import { GuildSlug, UserGuildPermissions } from '@roleypoly/types';
import * as React from 'react';
import { GoPerson, GoStar, GoZap } from 'react-icons/go';
import {
CardBase,
CardLine,
MaxWidthTitle,
PermissionTagStyled,
} from './ServerListingCard.styled';
type ServerListingProps = {
guild: GuildSlug;
};
export const ServerListingCard = (props: ServerListingProps) => (
<CardBase>
<CardLine>
<Avatar
hash={props.guild.icon}
src={utils.avatarHash(props.guild.id, props.guild.icon, 'icons')}
>
{utils.initialsFromName(props.guild.name)}
</Avatar>
</CardLine>
<MaxWidthTitle>{props.guild.name}</MaxWidthTitle>
<CardLine left>
<PermissionTag permissionLevel={props.guild.permissionLevel} />
</CardLine>
</CardBase>
);
const PermissionTag = (props: { permissionLevel: UserGuildPermissions }) => {
switch (props.permissionLevel) {
case UserGuildPermissions.Admin:
return (
<PermissionTagStyled>
<GoStar />
<Collapse>Administrator</Collapse>
</PermissionTagStyled>
);
case UserGuildPermissions.Manager:
return (
<PermissionTagStyled>
<GoZap />
<Collapse>Role Manager</Collapse>
</PermissionTagStyled>
);
default:
return (
<PermissionTagStyled hiddenOnSmall>
<GoPerson />
<Collapse>Member</Collapse>
</PermissionTagStyled>
);
}
};

View file

@ -0,0 +1 @@
export * from './ServerListingCard';

View file

@ -0,0 +1,19 @@
jest.unmock('./ServerMasthead');
import { shallow } from 'enzyme';
import * as React from 'react';
import { mastheadSlugs } from '../../fixtures/storyData';
import { ServerMasthead } from './ServerMasthead';
import { Editable } from './ServerMasthead.styled';
it('shows Edit Server when editable is true', () => {
const view = shallow(<ServerMasthead editable={true} guild={mastheadSlugs[0]} />);
expect(view.find(Editable).length).not.toBe(0);
});
it('hides Edit Server when editable is true', () => {
const view = shallow(<ServerMasthead editable={false} guild={mastheadSlugs[0]} />);
expect(view.find(Editable).length).toBe(0);
});

View file

@ -0,0 +1,17 @@
import * as React from 'react';
import { guild } from '../../fixtures/storyData';
import { ServerMasthead } from './ServerMasthead';
export default {
title: 'Molecules/Server Masthead',
args: {
editable: false,
guild,
},
};
export const Default = (args) => <ServerMasthead {...args} />;
export const Editable = (args) => <ServerMasthead {...args} />;
Editable.args = {
editable: true,
};

View file

@ -0,0 +1,36 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import { transitions } from '@roleypoly/design-system/atoms/timings';
import styled from 'styled-components';
export const Wrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
`;
export const Name = styled.div`
margin: 0 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
flex-wrap: wrap;
flex-direction: column;
justify-content: flex-start;
`;
export const Icon = styled.div`
flex-shrink: 0;
`;
export const Editable = styled.div`
color: ${palette.taupe500};
display: flex;
align-items: center;
user-select: none;
transition: color ${transitions.actionable}s ease-in-out;
cursor: pointer;
&:hover {
color: ${palette.taupe600};
}
`;

View file

@ -0,0 +1,38 @@
import { Avatar, utils } from '@roleypoly/design-system/atoms/avatar';
import { AccentTitle, AmbientLarge } from '@roleypoly/design-system/atoms/typography';
import { GuildSlug } from '@roleypoly/types';
import * as React from 'react';
import { GoPencil } from 'react-icons/go';
import { Editable, Icon, Name, Wrapper } from './ServerMasthead.styled';
export type ServerMastheadProps = {
guild: GuildSlug;
editable: boolean;
};
export const ServerMasthead = (props: ServerMastheadProps) => {
return (
<Wrapper>
<Icon>
<Avatar
hash={props.guild.icon}
size={props.editable ? 60 : 48}
src={utils.avatarHash(props.guild.id, props.guild.icon, 'icons', 512)}
>
{utils.initialsFromName(props.guild.name)}
</Avatar>
</Icon>
<Name>
<AccentTitle>{props.guild.name}</AccentTitle>
{props.editable && (
<a href={`/s/${props.guild.id}/edit`}>
<Editable role="button">
<GoPencil />
&nbsp; <AmbientLarge>Edit Server</AmbientLarge>
</Editable>
</a>
)}
</Name>
</Wrapper>
);
};

View file

@ -0,0 +1 @@
export * from './ServerMasthead';

View file

@ -0,0 +1,19 @@
import { Hero } from '@roleypoly/design-system/atoms/hero';
import * as React from 'react';
import { user } from '../../fixtures/storyData';
import { UserAvatarGroup } from './UserAvatarGroup';
export default {
title: 'Molecules/User Avatar Group',
component: UserAvatarGroup,
args: {
user,
preventCollapse: true,
},
};
export const Default = (args) => (
<Hero>
<UserAvatarGroup {...args} />
</Hero>
);

View file

@ -0,0 +1,29 @@
import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
import { palette } from '@roleypoly/design-system/atoms/colors';
import styled, { css } from 'styled-components';
export const Collapse = styled.div<{ preventCollapse: boolean }>`
${(props) =>
!props.preventCollapse &&
onSmallScreen(css`
display: none;
`)}
`;
export const Group = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
white-space: nowrap;
`;
export const Discriminator = styled.span`
color: ${palette.taupe500};
font-size: 75%;
padding: 0 5px;
`;
export const GroupText = styled.span`
position: relative;
top: -2px;
`;

View file

@ -0,0 +1,28 @@
import { Avatar, utils } from '@roleypoly/design-system/atoms/avatar';
import { DiscordUser } from '@roleypoly/types';
import * as React from 'react';
import { Collapse, Discriminator, Group, GroupText } from './UserAvatarGroup.styled';
type Props = {
user: DiscordUser;
preventCollapse?: boolean;
};
export const UserAvatarGroup = (props: Props) => (
<Group>
<Collapse preventCollapse={props.preventCollapse || false}>
<GroupText>
{props.user.username}
<Discriminator>#{props.user.discriminator}</Discriminator>
</GroupText>
&nbsp;
</Collapse>
<Avatar
size={34}
hash={props.user.avatar}
src={utils.avatarHash(props.user.id, props.user.avatar, 'avatars')}
>
{utils.initialsFromName(props.user.username)}
</Avatar>
</Group>
);

View file

@ -0,0 +1 @@
export * from './UserAvatarGroup';

View file

@ -0,0 +1,18 @@
import { PopoverBase } from '@roleypoly/design-system/atoms/popover/Popover.styled';
import * as React from 'react';
import { user } from '../../fixtures/storyData';
import { UserPopover as UserPopoverComponent } from './UserPopover';
export default {
title: 'Molecules/User Popover',
component: UserPopoverComponent,
args: {
user,
},
};
export const UserPopover = (args) => (
<PopoverBase active>
<UserPopoverComponent {...args} />
</PopoverBase>
);

View file

@ -0,0 +1,33 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import { transitions } from '@roleypoly/design-system/atoms/timings';
import styled from 'styled-components';
export const Base = styled.div`
text-align: right;
display: flex;
flex-direction: column;
user-select: none;
`;
export const NavAction = styled.div`
height: 2.25em;
display: flex;
align-items: center;
justify-content: flex-end;
transition: color ${transitions.actionable}s ease-in-out;
color: ${palette.taupe500};
box-sizing: border-box;
&:hover {
cursor: pointer;
color: ${palette.taupe600};
}
svg {
font-size: 120%;
box-sizing: content-box;
padding: 5px 8px;
position: relative;
top: 0.1em;
}
`;

View file

@ -0,0 +1,30 @@
import { CompletelyStylelessLink } from '@roleypoly/design-system/atoms/typography';
import { UserAvatarGroup } from '@roleypoly/design-system/molecules/user-avatar-group';
import { DiscordUser } from '@roleypoly/types';
import * as React from 'react';
import { GoGear, GoSignOut } from 'react-icons/go';
import { Base, NavAction } from './UserPopover.styled';
type UserPopoverProps = {
user: DiscordUser;
};
export const UserPopover = (props: UserPopoverProps) => (
<Base>
<UserAvatarGroup user={props.user} preventCollapse={true} />
<NavAction>
<a href="/user/settings">
<CompletelyStylelessLink>
Settings <GoGear />
</CompletelyStylelessLink>
</a>
</NavAction>
<NavAction>
<a href="/machinery/logout">
<CompletelyStylelessLink>
Log Out <GoSignOut />
</CompletelyStylelessLink>
</a>
</NavAction>
</Base>
);

View file

@ -0,0 +1 @@
export * from './UserPopover';