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,20 @@
import * as React from 'react';
import { mastheadSlugs, user } from '../../fixtures/storyData';
import { AppShell } from './AppShell';
export default {
title: 'Organisms/App Shell',
component: AppShell,
};
export const Guest = () => (
<AppShell showFooter user={null}>
<h1>Hello World</h1>
</AppShell>
);
export const LoggedIn = () => (
<AppShell user={user} guilds={mastheadSlugs}>
<h1>Hello World</h1>
</AppShell>
);

View file

@ -0,0 +1,24 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import { fontCSS } from '@roleypoly/design-system/atoms/fonts';
import styled, { createGlobalStyle } from 'styled-components';
export const Content = styled.div<{ small?: boolean }>`
margin: 0 auto;
margin-top: 50px;
width: ${(props) => (props.small ? '960px' : '1024px')};
max-width: 98vw;
max-height: calc(100vh - 50px);
`;
export const GlobalStyles = createGlobalStyle`
body {
background-color: ${palette.taupe200};
color: ${palette.grey600};
overflow-y: hidden;
scroll-behavior: smooth;
${fontCSS}
}
* {
box-sizing: border-box;
}
`;

View file

@ -0,0 +1,42 @@
import { GlobalStyleColors } from '@roleypoly/design-system/atoms/colors';
import { Footer } from '@roleypoly/design-system/molecules/footer';
import * as Masthead from '@roleypoly/design-system/organisms/masthead';
import { DiscordUser, GuildSlug } from '@roleypoly/types';
import * as React from 'react';
import { Scrollbars } from 'react-custom-scrollbars';
import { Content, GlobalStyles } from './AppShell.styled';
export type AppShellProps = {
children: React.ReactNode;
user?: DiscordUser;
showFooter?: boolean;
small?: boolean;
activeGuildId?: string | null;
guilds?: GuildSlug[];
disableGuildPicker?: boolean;
};
export const AppShell = (props: AppShellProps) => (
<>
<GlobalStyles />
<GlobalStyleColors />
{props.user ? (
<Masthead.Authed
disableGuildPicker={props.disableGuildPicker}
guilds={props.guilds || []}
activeGuildId={props.activeGuildId || null}
user={props.user}
/>
) : (
<Masthead.Guest />
)}
<Scrollbars
style={{ height: 'calc(100vh - 25px)', margin: 0, padding: 0 }}
autoHide
universal
>
<Content small={props.small}>{props.children}</Content>
{props.showFooter && <Footer />}
</Scrollbars>
</>
);

View file

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

View file

@ -0,0 +1,10 @@
import * as React from 'react';
import { guildEnum } from '../../fixtures/storyData';
import { EditorShell } from './EditorShell';
export default {
title: 'Organisms/Editor',
component: EditorShell,
};
export const Shell = () => <EditorShell guild={guildEnum.guildsList[0]} />;

View file

@ -0,0 +1,8 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import styled from 'styled-components';
export const CategoryContainer = styled.div`
background-color: ${palette.taupe100};
padding: 10px;
margin: 15px 0;
`;

View file

@ -0,0 +1,31 @@
import { Tab, TabView } from '@roleypoly/design-system/atoms/tab-view';
import { PresentableGuild } from '@roleypoly/types';
import * as React from 'react';
import { EditorCategory } from '../../molecules/editor-category';
import { CategoryContainer } from './EditorShell.styled';
type Props = {
guild: PresentableGuild;
};
export const EditorShell = (props: Props) => (
<TabView initialTab={0}>
<Tab title="Roles">{() => <RolesTab {...props} />}</Tab>
<Tab title="Server Details">{() => <div>hi2!</div>}</Tab>
</TabView>
);
const RolesTab = (props: Props) => (
<div>
{props.guild.data.categories.map((category, idx) => (
<CategoryContainer key={idx}>
<EditorCategory
category={category}
uncategorizedRoles={[]}
guildRoles={props.guild.roles}
onChange={(x) => console.log(x)}
/>
</CategoryContainer>
))}
</div>
);

View file

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

View file

@ -0,0 +1,10 @@
import * as React from 'react';
import { HelpStoryWrapper } from '../../molecules/help-page-base/storyDecorator';
import { WhyNoRoles } from './WhyNoRoles';
export default {
title: 'Organisms/Help Pages',
decorators: [HelpStoryWrapper],
};
export const WhyNoRoles_ = () => <WhyNoRoles />;

View file

@ -0,0 +1,39 @@
import { numberToChroma, palette } from '@roleypoly/design-system/atoms/colors';
import { Role } from '@roleypoly/types';
import styled, { css } from 'styled-components';
export const DiscordBase = styled.div`
background-color: ${palette.discord100};
border: solid 1px rgba(0, 0, 0, 0.15);
border-radius: 3px;
padding: 10px;
user-select: none;
width: 250px;
`;
const hover = (roleColor: string) => css`
color: #efefef;
background-color: ${roleColor};
cursor: pointer;
`;
export const DiscordRole = styled.div<{
discordRole: Role;
isRoleypoly: boolean;
}>`
/* stylelint-disable function-name-case, function-whitespace-after */
/* Disabled due to postcss bug parsing the below functions as CSS and not a JS interpolation */
padding: 6px 10px;
color: ${(props) => numberToChroma(props.discordRole.color).css()};
border-radius: 3px;
:hover {
${(props) => hover(numberToChroma(props.discordRole.color).alpha(0.5).css())}
}
${(props) =>
props.isRoleypoly &&
hover(numberToChroma(props.discordRole.color).alpha(0.5).css())}
`;

View file

@ -0,0 +1,83 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import { HalfsiesContainer, HalfsiesItem } from '@roleypoly/design-system/atoms/halfsies';
import { SparkleOverlay } from '@roleypoly/design-system/atoms/sparkle';
import { Role } from '@roleypoly/types';
import { demoData } from '@roleypoly/types/demoData';
import chroma from 'chroma-js';
import * as React from 'react';
import { FaCheck, FaTimes } from 'react-icons/fa';
import { DiscordBase, DiscordRole } from './WhyNoRoles.styled';
const adminRoles: Role[] = [
{
id: 'roley2',
name: 'Admin',
permissions: '0',
color: chroma('hotpink').num(),
position: -1,
managed: true,
safety: 0,
},
{
id: 'roley3',
name: 'Moderator',
permissions: '0',
color: chroma('lime').num(),
position: -1,
managed: true,
safety: 0,
},
];
const roleypolyRole: Role = {
id: 'roley',
name: 'Roleypoly',
permissions: '0',
color: chroma(palette.taupe500).num(),
position: -1,
managed: true,
safety: 0,
};
const goodRoles = [...adminRoles, roleypolyRole, ...demoData];
const badRoles = [...adminRoles, ...demoData, roleypolyRole];
const MaybeWithOverlay = (props: { children: React.ReactNode; withOverlay: boolean }) => {
if (props.withOverlay) {
return (
<SparkleOverlay size={-5} repeatCount={10}>
{props.children}
</SparkleOverlay>
);
} else {
return <>{props.children}</>;
}
};
const Example = (props: { roles: Role[]; isGood: boolean }) => (
<div>
<DiscordBase>
{props.roles.map((r) => (
<MaybeWithOverlay withOverlay={props.isGood && r.name === 'Roleypoly'}>
<DiscordRole discordRole={r} isRoleypoly={r.name === 'Roleypoly'}>
{r.name}
</DiscordRole>
</MaybeWithOverlay>
))}
</DiscordBase>
</div>
);
export const WhyNoRoles = () => (
<HalfsiesContainer>
<HalfsiesItem>
<FaCheck /> Good
<Example isGood roles={goodRoles} />
</HalfsiesItem>
<HalfsiesItem>
<FaTimes /> Baddd
<Example isGood={false} roles={badRoles} />
</HalfsiesItem>
</HalfsiesContainer>
);

View file

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

View file

@ -0,0 +1,8 @@
import * as React from 'react';
import { Landing } from './Landing';
export default {
title: 'Organisms/Landing',
};
export const Landing_ = () => <Landing />;

View file

@ -0,0 +1,32 @@
import { onTablet } from '@roleypoly/design-system/atoms/breakpoints';
import { text400 } from '@roleypoly/design-system/atoms/typography';
import styled, { css } from 'styled-components';
export const HeroText = styled.div`
${onTablet(css`
text-align: center;
`)}
`;
export const DemoSubtitle = styled.p`
${text400}
text-align: center;
margin-top: 0.4em;
`;
export const DemoAlignment = styled.div`
min-height: 125px;
${onTablet(css`
min-height: 95px;
`)}
`;
export const HeroCentering = styled.div`
display: flex;
flex-direction: column;
min-height: calc(100vh - 200px);
align-items: center;
justify-content: center;
margin-bottom: 2em;
`;

View file

@ -0,0 +1,38 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import { HalfsiesContainer, HalfsiesItem } from '@roleypoly/design-system/atoms/halfsies';
import { Space } from '@roleypoly/design-system/atoms/space';
import { LargeText, LargeTitle } from '@roleypoly/design-system/atoms/typography';
import { DemoDiscord } from '@roleypoly/design-system/molecules/demo-discord';
import { DemoPicker } from '@roleypoly/design-system/molecules/demo-picker';
import * as React from 'react';
import { DemoAlignment, DemoSubtitle, HeroCentering, HeroText } from './Landing.styled';
export const Landing = () => (
<HeroCentering>
<HeroText>
<div>
<LargeTitle>Discord roles for humans.</LargeTitle>
</div>
<div style={{ color: palette.taupe500 }}>
<LargeText>
Ditch the bot commands. It's&nbsp;{new Date().getFullYear()}.
</LargeText>
</div>
</HeroText>
<Space />
<HalfsiesContainer>
<HalfsiesItem style={{ marginTop: '2em' }}>
<DemoAlignment>
<DemoDiscord />
</DemoAlignment>
<DemoSubtitle>Why are you okay with antiques?</DemoSubtitle>
</HalfsiesItem>
<HalfsiesItem style={{ marginTop: '2em' }}>
<DemoAlignment>
<DemoPicker />
</DemoAlignment>
<DemoSubtitle>Just click or tap.</DemoSubtitle>
</HalfsiesItem>
</HalfsiesContainer>
</HeroCentering>
);

View file

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

View file

@ -0,0 +1,96 @@
import { DynamicLogomark } from '@roleypoly/design-system/atoms/branding';
import { Popover } from '@roleypoly/design-system/atoms/popover';
import { GuildNav } from '@roleypoly/design-system/molecules/guild-nav';
import { NavSlug } from '@roleypoly/design-system/molecules/nav-slug';
import { UserAvatarGroup } from '@roleypoly/design-system/molecules/user-avatar-group';
import { UserPopover } from '@roleypoly/design-system/molecules/user-popover';
import { DiscordUser, GuildSlug } from '@roleypoly/types';
import * as React from 'react';
import { GoOrganization } from 'react-icons/go';
import {
GuildPopoverHead,
InteractionBase,
MastheadA,
MastheadAlignment,
MastheadBase,
MastheadLeft,
MastheadRight,
} from './Masthead.styled';
type Props = {
user?: DiscordUser;
activeGuildId: string | null;
guilds: GuildSlug[];
disableGuildPicker?: boolean;
};
export const Authed = (props: Props) => {
const [userPopoverState, setUserPopoverState] = React.useState(false);
const [serverPopoverState, setServerPopoverState] = React.useState(false);
return (
<MastheadBase>
<MastheadAlignment>
<MastheadLeft>
<MastheadA href="/servers">
<DynamicLogomark height={35} />
</MastheadA>
<InteractionBase
onClick={() => {
if (!props.disableGuildPicker) {
setServerPopoverState(true);
setUserPopoverState(false);
}
}}
hide={!serverPopoverState}
>
<NavSlug
guild={
props.guilds.find(
(guild) => guild.id === props.activeGuildId
) || null
}
/>
</InteractionBase>
<Popover
headContent={
<GuildPopoverHead>
<GoOrganization />
My Guilds
</GuildPopoverHead>
}
canDefocus
position="bottom left"
active={serverPopoverState}
preferredWidth={560}
onExit={() => setServerPopoverState(false)}
>
{() => <GuildNav guilds={props.guilds} />}
</Popover>
</MastheadLeft>
<MastheadRight>
<InteractionBase
onClick={() => {
setUserPopoverState(true);
setServerPopoverState(false);
}}
hide={!userPopoverState}
>
{props.user !== undefined && (
<UserAvatarGroup user={props.user} />
)}
</InteractionBase>
<Popover
headContent={<></>}
canDefocus
position="top right"
active={userPopoverState}
onExit={() => setUserPopoverState(false)}
>
{() => props.user && <UserPopover user={props.user} />}
</Popover>
</MastheadRight>
</MastheadAlignment>
</MastheadBase>
);
};

View file

@ -0,0 +1,34 @@
import { DynamicLogotype } from '@roleypoly/design-system/atoms/branding';
import { Button } from '@roleypoly/design-system/atoms/button';
import * as React from 'react';
import { FaSignInAlt } from 'react-icons/fa';
import {
MastheadA,
MastheadAlignment,
MastheadBase,
MastheadLeft,
MastheadRight,
} from './Masthead.styled';
export const Guest = () => (
<MastheadBase>
<MastheadAlignment>
<MastheadLeft>
<MastheadA href="/">
<DynamicLogotype height={30} />
</MastheadA>
</MastheadLeft>
<MastheadRight>
<MastheadA href="/auth/login">
<Button size="small">
Login{' '}
<FaSignInAlt
size="1em"
style={{ transform: 'translateY(1px)' }}
/>
</Button>
</MastheadA>
</MastheadRight>
</MastheadAlignment>
</MastheadBase>
);

View file

@ -0,0 +1,16 @@
import * as React from 'react';
import { guild, mastheadSlugs, user } from '../../fixtures/storyData';
import { Authed } from './Authed';
import { Guest } from './Guest';
export default {
title: 'Organisms/Masthead',
};
export const HasGuilds = () => (
<Authed guilds={mastheadSlugs} activeGuildId={guild.id} user={user} />
);
export const NoGuilds = () => <Authed guilds={[]} activeGuildId={null} user={user} />;
export const Guest_ = () => <Guest />;

View file

@ -0,0 +1,92 @@
import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
import { palette } from '@roleypoly/design-system/atoms/colors';
import { transitions } from '@roleypoly/design-system/atoms/timings';
import styled, { css } from 'styled-components';
export const MastheadBase = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
height: 50px;
background-color: ${palette.taupe100};
display: flex;
align-items: center;
justify-content: center;
padding: 0 3px;
z-index: 100;
`;
export const MastheadAlignment = styled.div`
max-width: 98vw;
width: 1024px;
display: flex;
align-items: center;
`;
const sideBase = styled.div`
flex: 1;
display: flex;
`;
export const MastheadLeft = styled(sideBase)``;
export const MastheadRight = styled(sideBase)`
flex: 0;
justify-content: flex-end;
`;
export const MastheadCollapse = styled.div`
${onSmallScreen(css`
display: none;
`)}
`;
export const MastheadInner = styled.div`
/* height: 30px; */
display: flex;
align-items: center;
`;
type InteractionBaseProps = {
hide: boolean;
};
export const InteractionBase = styled.div<InteractionBaseProps>`
display: flex;
align-items: center;
height: 50px;
padding: 0 5px;
transition: opacity ${transitions.actionable}s ease-in-out,
background-color ${transitions.actionable}s ease-in-out;
opacity: ${(props) => (props.hide ? 1 : 0)};
:hover {
background-color: rgba(0, 0, 0, 0.15);
cursor: pointer;
}
`;
export const MastheadA = styled.a`
display: flex;
align-items: center;
justify-content: center;
color: unset;
text-decoration: unset;
`;
export const GuildPopoverHead = styled.div`
display: flex;
align-items: center;
svg {
color: ${palette.taupe500};
padding: 5px;
height: 1.4em;
font-size: 2em;
margin-right: 10px;
margin-left: 16px;
${onSmallScreen(css`
margin-left: 0;
`)}
}
`;

View file

@ -0,0 +1,2 @@
export * from './Authed';
export * from './Guest';

View file

@ -0,0 +1,33 @@
import * as React from 'react';
import styled from 'styled-components';
import { guild } from '../../fixtures/storyData';
import { Preauth } from './Preauth';
export default {
title: 'Organisms/Preauth',
component: Preauth,
};
const Center = styled.div`
margin: 0 auto;
`;
export const NoSlug = ({ onSendSecretCode }) => {
return (
<Center>
<Preauth botName="roleypoly#3266" onSendSecretCode={onSendSecretCode} />
</Center>
);
};
export const WithSlug = ({ onSendSecretCode }) => {
return (
<Center>
<Preauth
botName="roleypoly#3266"
guildSlug={guild}
onSendSecretCode={onSendSecretCode}
/>
</Center>
);
};

View file

@ -0,0 +1,50 @@
import { Button } from '@roleypoly/design-system/atoms/button';
import { PreauthGreeting } from '@roleypoly/design-system/molecules/preauth-greeting';
import { GuildSlug } from '@roleypoly/types';
import * as React from 'react';
import { FaDiscord } from 'react-icons/fa';
import styled from 'styled-components';
export type PreauthProps = {
guildSlug?: GuildSlug;
onSendSecretCode: (code: string) => void;
botName?: string;
discordOAuthLink?: string;
};
const Centered = styled.div`
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 90vw;
margin: 0 auto;
`;
const WidthContainer = styled.div`
width: 20em;
max-width: 90vw;
`;
export const Preauth = (props: PreauthProps) => {
return (
<Centered>
{props.guildSlug && <PreauthGreeting guildSlug={props.guildSlug} />}
<WidthContainer>
<a href={props.discordOAuthLink || '#'}>
<Button
color="discord"
icon={
<div style={{ position: 'relative', top: 3 }}>
<FaDiscord />
</div>
}
>
Sign in with Discord
</Button>
</a>
</WidthContainer>
</Centered>
);
};

View file

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

View file

@ -0,0 +1,44 @@
jest.unmock('@roleypoly/design-system/atoms/role')
.unmock('@roleypoly/design-system/atoms/button')
.unmock('@roleypoly/design-system/molecules/picker-category')
.unmock('@roleypoly/design-system/organisms/role-picker');
import { Role } from '@roleypoly/design-system/atoms/role';
import { PickerCategory } from '@roleypoly/design-system/molecules/picker-category';
import { ResetSubmit } from '@roleypoly/design-system/molecules/reset-submit';
import { shallow } from 'enzyme';
import * as React from 'react';
import {
guildData,
mastheadSlugs,
member,
mockCategorySingle,
roleCategory,
roleCategory2,
} from '../../fixtures/storyData';
import { RolePicker, RolePickerProps } from './RolePicker';
it('unselects the rest of a category in single mode', () => {
const props: RolePickerProps = {
guildData: { ...guildData, categories: [mockCategorySingle] },
member: { ...member, roles: [] },
roles: [...roleCategory, ...roleCategory2],
guild: mastheadSlugs[0],
onSubmit: jest.fn(),
editable: false,
};
const view = shallow(<RolePicker {...props} />);
const roles = view.find(PickerCategory).dive().find(Role);
roles.first().props().onClick?.(true);
view.find(ResetSubmit).props().onSubmit();
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.roles[0]]);
roles.at(1).props().onClick?.(true);
view.find(ResetSubmit).props().onSubmit();
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.roles[1]]);
});

View file

@ -0,0 +1,60 @@
import * as React from 'react';
import {
guild,
guildData,
member,
roleCategory,
roleCategory2,
} from '../../fixtures/storyData';
import { RolePicker, RolePickerProps } from './RolePicker';
const props: Partial<RolePickerProps> = {
guildData: guildData,
member: member,
guild: guild,
roles: [...roleCategory, ...roleCategory2],
editable: false,
};
const noMessageArgs: Partial<RolePickerProps> = {
...props,
guildData: {
...guildData,
message: '',
},
};
const noCategoriesArgs: Partial<RolePickerProps> = {
...props,
guildData: {
...guildData,
categoriesList: [],
},
};
const emptyArgs = {
...props,
guildData: {
...guildData,
categoriesList: [],
message: '',
},
};
export default {
title: 'Organisms/Role Picker',
args: props,
component: RolePicker,
};
export const Full = (args) => <RolePicker {...args} />;
export const EditableFull = (args) => <RolePicker {...args} />;
EditableFull.args = {
editable: true,
};
export const NoMessage = (args) => <RolePicker {...args} />;
NoMessage.args = noMessageArgs;
export const NoCategories = (args) => <RolePicker {...args} />;
NoCategories.args = noCategoriesArgs;
export const Empty = (args) => <RolePicker {...args} />;
Empty.args = emptyArgs;

View file

@ -0,0 +1,34 @@
import { palette } from '@roleypoly/design-system/atoms/colors';
import styled from 'styled-components';
export const Container = styled.div``;
export const Box = styled.div`
background-color: ${palette.taupe300};
padding: 5px;
margin: 5px 0;
`;
export const MessageBox = styled(Box)`
padding: 10px;
`;
export const CategoryContainer = styled(Box)``;
export const InfoBox = styled(MessageBox)`
display: flex;
align-items: center;
`;
export const InfoIcon = styled.div`
flex-shrink: 0;
font-size: 1.75em;
margin-right: 10px;
display: flex;
align-items: center;
color: ${palette.taupe500};
`;
export const Buttons = styled.div`
display: flex;
`;

View file

@ -0,0 +1,151 @@
import { FaderOpacity } from '@roleypoly/design-system/atoms/fader';
import { Space } from '@roleypoly/design-system/atoms/space';
import { Link } from '@roleypoly/design-system/atoms/typography';
import { PickerCategory } from '@roleypoly/design-system/molecules/picker-category';
import { ResetSubmit } from '@roleypoly/design-system/molecules/reset-submit';
import { ServerMasthead } from '@roleypoly/design-system/molecules/server-masthead';
import { ReactifyNewlines } from '@roleypoly/misc-utils/ReactifyNewlines';
import { sortBy } from '@roleypoly/misc-utils/sortBy';
import {
Category,
CategoryType,
GuildData,
GuildSlug,
Member,
Role,
} from '@roleypoly/types';
import { isEqual, xor } from 'lodash';
import * as React from 'react';
import { GoInfo } from 'react-icons/go';
import {
CategoryContainer,
Container,
InfoBox,
InfoIcon,
MessageBox,
} from './RolePicker.styled';
export type RolePickerProps = {
guild: GuildSlug;
guildData: GuildData;
member: Member;
roles: Role[];
onSubmit: (selectedRoles: string[]) => void;
editable: boolean;
};
export const RolePicker = (props: RolePickerProps) => {
const [selectedRoles, updateSelectedRoles] = React.useState<string[]>(
props.member.roles
);
React.useEffect(() => {
if (!isEqual(props.member.roles, selectedRoles)) {
updateSelectedRoles(props.member.roles);
}
}, [props.member.roles]);
const handleChange = (category: Category) => (role: Role) => (newState: boolean) => {
if (category.type === CategoryType.Single) {
updateSelectedRoles(
newState === true
? [
...selectedRoles.filter((x) => !category.roles.includes(x)),
role.id,
]
: selectedRoles.filter((x) => x !== role.id)
);
} else {
updateSelectedRoles(
newState === true
? [...selectedRoles, role.id]
: selectedRoles.filter((x) => x !== role.id)
);
}
};
return (
<Container>
<Space />
<ServerMasthead guild={props.guild} editable={props.editable} />
<Space />
{props.guildData.message && (
<>
<MessageBox>
<ReactifyNewlines>{props.guildData.message}</ReactifyNewlines>
</MessageBox>
<Space />
</>
)}
{props.guildData.categories.length !== 0 ? (
<>
<div>
{sortBy(
props.guildData.categories.filter(
(category) => !category.hidden
),
'position'
).map((category, idx) => (
<CategoryContainer key={idx}>
<PickerCategory
key={idx}
category={category}
title={category.name}
selectedRoles={selectedRoles.filter((roleId) =>
category.roles.includes(roleId)
)}
roles={
category.roles
.map((role) =>
props.roles.find((r) => r.id === role)
)
.filter((r) => r !== undefined) as Role[]
}
onChange={handleChange(category)}
wikiMode={false}
type={
category.type === CategoryType.Single
? 'single'
: 'multi'
}
/>
</CategoryContainer>
))}
</div>
<div>
<FaderOpacity
isVisible={
xor(selectedRoles, props.member.roles).length !== 0
}
>
<ResetSubmit
onSubmit={() => props.onSubmit(selectedRoles)}
onReset={() => {
updateSelectedRoles(props.member.roles);
}}
/>
</FaderOpacity>
</div>
</>
) : (
<InfoBox>
<InfoIcon>
<GoInfo />
</InfoIcon>
<div>
There are currently no roles available for you to choose from.
{props.editable && (
<>
{' '}
<a href={`/s/${props.guild.id}/edit`}>
<Link>Add some roles!</Link>
</a>
</>
)}
</div>
</InfoBox>
)}
</Container>
);
};

View file

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

View file

@ -0,0 +1,25 @@
import { UserGuildPermissions } from '@roleypoly/types';
import { mastheadSlugs } from '../../fixtures/storyData';
import { ServerSetup } from './ServerSetup';
export default {
title: 'Organisms/Server Setup',
component: ServerSetup,
};
export const asAdmin = () => (
<ServerSetup
guildSlug={{ ...mastheadSlugs[1], permissionLevel: UserGuildPermissions.Admin }}
/>
);
export const asManager = () => (
<ServerSetup
guildSlug={{
...mastheadSlugs[1],
permissionLevel: UserGuildPermissions.Manager,
}}
/>
);
export const asUser = () => (
<ServerSetup guildSlug={{ ...mastheadSlugs[1], permissionLevel: 0 }} />
);

View file

@ -0,0 +1,14 @@
import styled from 'styled-components';
export const FlexLine = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding: 15px;
`;
export const FlexWrap = styled.div`
display: flex;
flex-direction: column;
flex-wrap: wrap;
`;

View file

@ -0,0 +1,117 @@
import { Avatar, utils } from '@roleypoly/design-system/atoms/avatar';
import { Button } from '@roleypoly/design-system/atoms/button';
import { DotOverlay } from '@roleypoly/design-system/atoms/dot-overlay';
import { Hero } from '@roleypoly/design-system/atoms/hero';
import { AccentTitle, SmallTitle } from '@roleypoly/design-system/atoms/typography';
import { evaluatePermission } from '@roleypoly/misc-utils/hasPermission';
import { GuildSlug, UserGuildPermissions } from '@roleypoly/types';
import * as React from 'react';
import { FaDiscord } from 'react-icons/fa';
import { GoArrowLeft } from 'react-icons/go';
import { FlexLine, FlexWrap } from './ServerSetup.styled';
export type ServerSetupProps = {
guildSlug: GuildSlug;
};
export const ServerSetup = (props: ServerSetupProps) => (
<>
<DotOverlay />
<Hero>
<FlexWrap>
<FlexLine>
<div>
<Avatar
hash={props.guildSlug.icon}
src={utils.avatarHash(
props.guildSlug.id,
props.guildSlug.icon,
'icons'
)}
>
{utils.initialsFromName(props.guildSlug.name)}
</Avatar>
</div>
<div>
<SmallTitle>
&nbsp;&nbsp;Roleypoly isn't in {props.guildSlug.name}
</SmallTitle>
</div>
</FlexLine>
{renderMessage(props.guildSlug)}
</FlexWrap>
</Hero>
</>
);
const renderMessage = ({ id, permissionLevel, name }: GuildSlug) => {
if (evaluatePermission(permissionLevel, UserGuildPermissions.Admin)) {
return adminMessage(id);
} else if (evaluatePermission(permissionLevel, UserGuildPermissions.Manager)) {
return managerMessage(id);
} else {
return userMessage(name);
}
};
const adminMessage = (id: string) => (
<>
<FlexLine>
<AccentTitle>
You're an admin of this server, click the button to get started!
</AccentTitle>
</FlexLine>
<FlexLine>
<div>
<a href={`/machinery/bot-join?id=${id}`}>
<Button color="discord" icon={<FaDiscord />}>
Add Roleypoly
</Button>
</a>
</div>
</FlexLine>
</>
);
const managerMessage = (id: string) => (
<>
<FlexLine>
<AccentTitle>
You might have the permissions to add it to the server.
</AccentTitle>
</FlexLine>
<FlexLine>
<div>
<a href={`/machinery/bot-join?id=${id}`}>
<Button color="discord" icon={<FaDiscord />}>
Add Roleypoly
</Button>
</a>
</div>
</FlexLine>
</>
);
const userMessage = (name: string) => {
return (
<>
<FlexLine>
<AccentTitle>
If you think this is a mistake, please contact staff for {name}.
</AccentTitle>
</FlexLine>
<FlexLine>
<Button
onClick={() => {
// void router.push('/');
}}
color="muted"
size="small"
icon={<GoArrowLeft />}
>
Go back
</Button>
</FlexLine>
</>
);
};

View file

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

View file

@ -0,0 +1,12 @@
import { mastheadSlugs } from '../../fixtures/storyData';
import { ServersListing } from './ServersListing';
export default {
title: 'Organisms/Servers Listing',
component: ServersListing,
args: {
guilds: mastheadSlugs,
},
};
export const serversListing = (args) => <ServersListing {...args} />;

View file

@ -0,0 +1,23 @@
import { onTablet } from '@roleypoly/design-system/atoms/breakpoints';
import styled, { css } from 'styled-components';
export const ContentContainer = styled.div`
display: flex;
flex-direction: column;
max-width: calc(98vw - 15px);
padding-bottom: 25px;
${onTablet(css`
flex-direction: row;
flex-wrap: wrap;
`)}
`;
export const CardContainer = styled.div`
box-sizing: border-box;
margin-bottom: 5px;
${onTablet(css`
margin: 5px;
flex-basis: 30%;
max-width: 30%;
`)}
`;

View file

@ -0,0 +1,27 @@
import { CompletelyStylelessLink } from '@roleypoly/design-system/atoms/typography';
import { ServerListingCard } from '@roleypoly/design-system/molecules/server-listing-card';
import { sortBy } from '@roleypoly/misc-utils/sortBy';
import { GuildSlug } from '@roleypoly/types';
import * as React from 'react';
import { CardContainer, ContentContainer } from './ServersListing.styled';
type ServersListingProps = {
guilds: GuildSlug[];
};
export const ServersListing = (props: ServersListingProps) => (
<ContentContainer>
{props.guilds &&
sortBy(props.guilds, 'name', (a: string, b: string) =>
a.toLowerCase() > b.toLowerCase() ? 1 : -1
).map((guild, idx) => (
<CardContainer key={idx}>
<a href={`/s/${guild.id}`}>
<CompletelyStylelessLink>
<ServerListingCard guild={guild} />
</CompletelyStylelessLink>
</a>
</CardContainer>
))}
</ContentContainer>
);

View file

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