mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-16 17:49:09 +00:00
feat: Add majority of design system components, build system fixes (#11)
* feat(design-system): pre-port of roleypoly/ui * feat(design-system): port molecules * chore(design-system): prettier * feat(design-system): add intro card and MDX components * fix(common/utils): hack fixtures test data moved to design-system, update accordingly * chore: document protoReflection.ts * fix(design-system): some molecules missed the magic fuckery * ci: keep going on bazel test failures * fix(design-system): server masthead molecule missed the magic fuckery * chore: fix ts paths * chore: fix docker publisher * chore: fix docker publisher names * chore(discord-bot): fix publisher * chore(discord-bot): fix publisher
This commit is contained in:
parent
c41fcabfd0
commit
89f237cf22
133 changed files with 2795 additions and 278 deletions
24
src/design-system/Intro.stories.mdx
Normal file
24
src/design-system/Intro.stories.mdx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Meta } from '@storybook/addon-docs/blocks';
|
||||
import { Logotype } from 'roleypoly/src/design-system/atoms/branding';
|
||||
import { Space } from 'roleypoly/src/design-system/atoms/space';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
|
||||
<Meta title="Roleypoly Design System" />
|
||||
|
||||
<Logotype height="4em" circleFill={palette.taupe100} />
|
||||
<Space />
|
||||
|
||||
# Rapid UI
|
||||
|
||||
#### Roleypoly Design System
|
||||
|
||||
This is a tool for Roleypoly developers to design and show off UI
|
||||
components as they build them.
|
||||
|
||||
If you're interested in helping build Roleypoly, [please visit the GitHub project.][roleypoly]
|
||||
|
||||
All components here follow the [Atomic Design System][atomic], and might be used in any number of Roleypoly UI systems, not limited to
|
||||
just the end user web application.
|
||||
|
||||
[roleypoly]: https://github.com/roleypoly/roleypoly
|
||||
[atomic]: https://bradfrost.com/blog/post/atomic-web-design/
|
|
@ -8,7 +8,7 @@ The Roleypoly Design System (rapid) is an atomic design system to help rapidly a
|
|||
|
||||
**Please follow hermeticity considerations.**
|
||||
|
||||
This package cannot reference RPC types, as they do not exist in the outside world. Storybook is the core component of this, and Storybook doesn't know how to find RPC types at CI build time, as Bazel is also not present.
|
||||
This package cannot reference RPC types, as they do not exist in the outside world. Storybook is the core component of this, and Storybook doesn't know how to find RPC types at CI build time, as Bazel is also not present. If you are worried about RPC types being compatible, please write a unit test and include the RPC types then.
|
||||
|
||||
You need:
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
load("//:hack/jest.bzl", "jest_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
@ -14,3 +15,7 @@ react_library(
|
|||
"@types/styled-components",
|
||||
],
|
||||
)
|
||||
|
||||
jest_test(
|
||||
src = ":tab-view",
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ load("//:hack/jest.bzl", "jest_test")
|
|||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "timings",
|
||||
name = "typist",
|
||||
deps = [
|
||||
"react",
|
||||
"@types/react",
|
||||
|
@ -12,5 +12,5 @@ react_library(
|
|||
)
|
||||
|
||||
jest_test(
|
||||
src = ":timings",
|
||||
src = ":typist",
|
||||
)
|
||||
|
|
|
@ -6,6 +6,8 @@ react_library(
|
|||
name = "typography",
|
||||
deps = [
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/timings",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
||||
|
|
33
src/design-system/atoms/typography/mdx.tsx
Normal file
33
src/design-system/atoms/typography/mdx.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import styled from 'styled-components';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
import {
|
||||
AccentTitle,
|
||||
LargeTitle,
|
||||
Link,
|
||||
MediumTitle,
|
||||
SmallTitle,
|
||||
Text,
|
||||
text600,
|
||||
text700,
|
||||
text800,
|
||||
text900,
|
||||
} from './typography';
|
||||
|
||||
export const mdxComponents = {
|
||||
h1: styled.h1`
|
||||
${text900}
|
||||
`,
|
||||
h2: styled.h2`
|
||||
${text800}
|
||||
`,
|
||||
h3: styled.h3`
|
||||
${text700}
|
||||
`,
|
||||
h4: styled.h4`
|
||||
${text600}
|
||||
`,
|
||||
p: styled.p`
|
||||
${Text}
|
||||
`,
|
||||
a: Link,
|
||||
};
|
|
@ -107,3 +107,12 @@ export const Spacing = () => (
|
|||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Link = () => (
|
||||
<typography.Link
|
||||
target="_blank"
|
||||
href="https://images.boredomfiles.com/wp-content/uploads/sites/5/2016/03/fox-door-5.png"
|
||||
>
|
||||
Here is a link <3
|
||||
</typography.Link>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
const reset = css`
|
||||
|
@ -86,3 +88,12 @@ export const AmbientLarge = styled.span`
|
|||
export const AmbientSmall = styled.span`
|
||||
${text100}
|
||||
`;
|
||||
|
||||
export const Link = styled.a`
|
||||
color: ${palette.taupe500};
|
||||
text-decoration: none;
|
||||
transition: color ${transitions.actionable}s ease-in-out;
|
||||
&:hover {
|
||||
color: ${palette.taupe600};
|
||||
}
|
||||
`;
|
||||
|
|
16
src/design-system/molecules/demo-discord/BUILD.bazel
Normal file
16
src/design-system/molecules/demo-discord/BUILD.bazel
Normal file
|
@ -0,0 +1,16 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "demo-discord",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/typist",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
import * as React from 'react';
|
||||
import { DemoDiscord } from './DemoDiscord';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Role Demos',
|
||||
};
|
||||
|
||||
export const Discord = () => <DemoDiscord />;
|
|
@ -0,0 +1,68 @@
|
|||
import styled, { keyframes } from 'styled-components';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
|
||||
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;
|
||||
`;
|
53
src/design-system/molecules/demo-discord/DemoDiscord.tsx
Normal file
53
src/design-system/molecules/demo-discord/DemoDiscord.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Base,
|
||||
Timestamp,
|
||||
TextParts,
|
||||
Username,
|
||||
InputBox,
|
||||
Line,
|
||||
InputTextAlignment,
|
||||
} from './DemoDiscord.styled';
|
||||
import { demoData } from 'roleypoly/src/design-system/shared-types/demoData';
|
||||
import { Typist } from 'roleypoly/src/design-system/atoms/typist';
|
||||
|
||||
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)}
|
||||
{time.getHours() <= 12 ? 'AM' : 'PM'}
|
||||
</Timestamp>
|
||||
<Username onClick={() => setEasterEggCount(easterEggCount + 1)}>
|
||||
okano 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>
|
||||
|
||||
<Typist
|
||||
resetTimeout={2000}
|
||||
charTimeout={75}
|
||||
lines={demoData.map((role) => `.iam ${role.name}`)}
|
||||
/>
|
||||
<Line />
|
||||
</InputTextAlignment>
|
||||
</InputBox>
|
||||
</Base>
|
||||
);
|
||||
};
|
1
src/design-system/molecules/demo-discord/index.ts
Normal file
1
src/design-system/molecules/demo-discord/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './DemoDiscord';
|
15
src/design-system/molecules/demo-picker/BUILD.bazel
Normal file
15
src/design-system/molecules/demo-picker/BUILD.bazel
Normal file
|
@ -0,0 +1,15 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "demo-picker",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/role",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
import * as React from 'react';
|
||||
import { DemoPicker } from './DemoPicker';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Role Demos',
|
||||
};
|
||||
|
||||
export const Picker = () => <DemoPicker />;
|
46
src/design-system/molecules/demo-picker/DemoPicker.tsx
Normal file
46
src/design-system/molecules/demo-picker/DemoPicker.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as React from 'react';
|
||||
import { Role } from 'roleypoly/src/design-system/atoms/role';
|
||||
import { Role as RPCRole } from 'roleypoly/src/design-system/shared-types';
|
||||
import styled from 'styled-components';
|
||||
import { demoData } from 'roleypoly/src/design-system/shared-types/demoData';
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
1
src/design-system/molecules/demo-picker/index.ts
Normal file
1
src/design-system/molecules/demo-picker/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './DemoPicker';
|
17
src/design-system/molecules/footer/BUILD.bazel
Normal file
17
src/design-system/molecules/footer/BUILD.bazel
Normal file
|
@ -0,0 +1,17 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "footer",
|
||||
deps = [
|
||||
"react",
|
||||
"react-icons",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/timings",
|
||||
"//src/design-system/atoms/typography",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
94
src/design-system/molecules/footer/Flags.tsx
Normal file
94
src/design-system/molecules/footer/Flags.tsx
Normal 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-5"></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>
|
||||
);
|
9
src/design-system/molecules/footer/Footer.stories.tsx
Normal file
9
src/design-system/molecules/footer/Footer.stories.tsx
Normal 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} />;
|
31
src/design-system/molecules/footer/Footer.styled.ts
Normal file
31
src/design-system/molecules/footer/Footer.styled.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import styled from 'styled-components';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
27
src/design-system/molecules/footer/Footer.tsx
Normal file
27
src/design-system/molecules/footer/Footer.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import * as React from 'react';
|
||||
import { FooterWrapper, HoverColor } from './Footer.styled';
|
||||
import { AmbientLarge } from 'roleypoly/src/design-system/atoms/typography';
|
||||
import { FaHeart } from 'react-icons/fa';
|
||||
import { Flags } from './Flags';
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
export const Footer = () => (
|
||||
<FooterWrapper>
|
||||
<AmbientLarge>
|
||||
<div>
|
||||
© {year} Roleypoly – Made with{' '}
|
||||
<FaHeart size={'0.8em'} color={'#fe4365'} />
|
||||
in Raleigh, NC
|
||||
</div>
|
||||
<div>
|
||||
<a href="https://discord.gg/Xj6rK3E">Discord/Support</a> –
|
||||
<a href="https://patreon.com/kata">Patreon</a> –
|
||||
<a href="https://github.com/roleypoly">GitHub</a>
|
||||
</div>
|
||||
<HoverColor>
|
||||
<Flags height={'1em'} />
|
||||
</HoverColor>
|
||||
</AmbientLarge>
|
||||
</FooterWrapper>
|
||||
);
|
1
src/design-system/molecules/footer/index.ts
Normal file
1
src/design-system/molecules/footer/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Footer';
|
21
src/design-system/molecules/guild-nav/BUILD.bazel
Normal file
21
src/design-system/molecules/guild-nav/BUILD.bazel
Normal file
|
@ -0,0 +1,21 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "guild-nav",
|
||||
deps = [
|
||||
"next",
|
||||
"react",
|
||||
"react-icons",
|
||||
"react-tooltip",
|
||||
"styled-components",
|
||||
"//src/common/utils",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/timings",
|
||||
"//src/design-system/molecules/nav-slug",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
21
src/design-system/molecules/guild-nav/GuildNav.stories.tsx
Normal file
21
src/design-system/molecules/guild-nav/GuildNav.stories.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react';
|
||||
import { GuildNav } from './GuildNav';
|
||||
import { guildEnum } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import { PopoverBase } from 'roleypoly/src/design-system/atoms/popover/Popover.styled';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Guild Nav',
|
||||
component: GuildNav,
|
||||
};
|
||||
|
||||
export const HasGuilds = () => (
|
||||
<PopoverBase active>
|
||||
<GuildNav guildEnumeration={guildEnum} />
|
||||
</PopoverBase>
|
||||
);
|
||||
|
||||
export const NoGuilds = () => (
|
||||
<PopoverBase active>
|
||||
<GuildNav guildEnumeration={{ guildsList: [] }} />
|
||||
</PopoverBase>
|
||||
);
|
22
src/design-system/molecules/guild-nav/GuildNav.styled.ts
Normal file
22
src/design-system/molecules/guild-nav/GuildNav.styled.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import styled from 'styled-components';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
62
src/design-system/molecules/guild-nav/GuildNav.tsx
Normal file
62
src/design-system/molecules/guild-nav/GuildNav.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import Link from 'next/link';
|
||||
import * as React from 'react';
|
||||
import { GoStar, GoZap } from 'react-icons/go';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { hasPermission, permissions } from 'roleypoly/src/common/utils/hasPermission';
|
||||
import { sortBy } from 'roleypoly/src/common/utils/sortBy';
|
||||
import {
|
||||
GuildEnumeration,
|
||||
PresentableGuild,
|
||||
Role,
|
||||
Guild,
|
||||
} from 'roleypoly/src/design-system/shared-types';
|
||||
import { NavSlug } from 'roleypoly/src/design-system/molecules/nav-slug';
|
||||
import { GuildNavItem } from './GuildNav.styled';
|
||||
|
||||
type Props = {
|
||||
guildEnumeration: GuildEnumeration;
|
||||
};
|
||||
|
||||
const tooltipId = 'guildnav';
|
||||
|
||||
const Badges = (props: { guild: PresentableGuild }) => {
|
||||
return React.useMemo(() => {
|
||||
if (!props.guild.member) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const roles = props.guild.member.rolesList
|
||||
.map((id) => {
|
||||
if (!props.guild.roles) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return props.guild.roles.rolesList.find((role) => role.id === id);
|
||||
})
|
||||
.filter((x) => !!x) as Role[];
|
||||
|
||||
if (hasPermission(roles, permissions.ADMINISTRATOR)) {
|
||||
return <GoStar data-tip="Administrator" data-for={tooltipId} />;
|
||||
}
|
||||
|
||||
if (hasPermission(roles, permissions.MANAGE_ROLES)) {
|
||||
return <GoZap data-tip="Role Editor" data-for={tooltipId} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [props.guild]);
|
||||
};
|
||||
|
||||
export const GuildNav = (props: Props) => (
|
||||
<div>
|
||||
{sortBy(props.guildEnumeration.guildsList, 'id').map((guild) => (
|
||||
<Link href={`/s/${guild.id}`} passHref>
|
||||
<GuildNavItem>
|
||||
<NavSlug guild={guild.guild || null} key={guild.id} />
|
||||
<Badges guild={guild} />
|
||||
</GuildNavItem>
|
||||
</Link>
|
||||
))}
|
||||
<ReactTooltip id={tooltipId} />
|
||||
</div>
|
||||
);
|
1
src/design-system/molecules/guild-nav/index.ts
Normal file
1
src/design-system/molecules/guild-nav/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './GuildNav';
|
16
src/design-system/molecules/nav-slug/BUILD.bazel
Normal file
16
src/design-system/molecules/nav-slug/BUILD.bazel
Normal file
|
@ -0,0 +1,16 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "nav-slug",
|
||||
deps = [
|
||||
"react",
|
||||
"react-icons",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/avatar",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
11
src/design-system/molecules/nav-slug/NavSlug.stories.tsx
Normal file
11
src/design-system/molecules/nav-slug/NavSlug.stories.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import { NavSlug } from './NavSlug';
|
||||
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Server Slug',
|
||||
component: NavSlug,
|
||||
};
|
||||
|
||||
export const Empty = () => <NavSlug guild={null} />;
|
||||
export const Example = () => <NavSlug guild={guild} />;
|
17
src/design-system/molecules/nav-slug/NavSlug.styled.ts
Normal file
17
src/design-system/molecules/nav-slug/NavSlug.styled.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import styled from 'styled-components';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
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;
|
||||
`;
|
18
src/design-system/molecules/nav-slug/NavSlug.tsx
Normal file
18
src/design-system/molecules/nav-slug/NavSlug.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react';
|
||||
import { Guild } from 'roleypoly/src/design-system/shared-types';
|
||||
import { Avatar, utils } from 'roleypoly/src/design-system/atoms/avatar';
|
||||
import { SlugContainer, SlugName } from './NavSlug.styled';
|
||||
import { GoOrganization } from 'react-icons/go';
|
||||
|
||||
type Props = {
|
||||
guild: Guild | null;
|
||||
};
|
||||
|
||||
export const NavSlug = (props: Props) => (
|
||||
<SlugContainer>
|
||||
<Avatar src={props.guild?.icon} deliberatelyEmpty={!props.guild} size={35}>
|
||||
{props.guild ? utils.initialsFromName(props.guild.name) : <GoOrganization />}
|
||||
</Avatar>
|
||||
<SlugName>{props.guild?.name || <>Your Guilds</>}</SlugName>
|
||||
</SlugContainer>
|
||||
);
|
1
src/design-system/molecules/nav-slug/index.ts
Normal file
1
src/design-system/molecules/nav-slug/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './NavSlug';
|
17
src/design-system/molecules/picker-category/BUILD.bazel
Normal file
17
src/design-system/molecules/picker-category/BUILD.bazel
Normal file
|
@ -0,0 +1,17 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "picker-category",
|
||||
deps = [
|
||||
"react",
|
||||
"react-tooltip",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/role",
|
||||
"//src/design-system/atoms/typography",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,38 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
roleWikiData,
|
||||
roleCategory,
|
||||
mockCategory,
|
||||
} from 'roleypoly/src/design-system/shared-types/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} />;
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
import styled from 'styled-components';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
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;
|
||||
`;
|
|
@ -0,0 +1,67 @@
|
|||
import * as React from 'react';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { Role } from 'roleypoly/src/design-system/atoms/role';
|
||||
import { AmbientLarge, LargeText } from 'roleypoly/src/design-system/atoms/typography';
|
||||
import {
|
||||
Category as RPCCategory,
|
||||
Role as RPCRole,
|
||||
RoleSafety,
|
||||
} from 'roleypoly/src/design-system/shared-types';
|
||||
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>
|
||||
{props.roles.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>
|
||||
);
|
1
src/design-system/molecules/picker-category/index.ts
Normal file
1
src/design-system/molecules/picker-category/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { PickerCategory } from './PickerCategory';
|
17
src/design-system/molecules/preauth-greeting/BUILD.bazel
Normal file
17
src/design-system/molecules/preauth-greeting/BUILD.bazel
Normal file
|
@ -0,0 +1,17 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "preauth-greeting",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/avatar",
|
||||
"//src/design-system/atoms/space",
|
||||
"//src/design-system/atoms/typography",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
import * as React from 'react';
|
||||
import { PreauthGreeting } from './PreauthGreeting';
|
||||
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Preauth/Greeting',
|
||||
component: PreauthGreeting,
|
||||
args: {
|
||||
guildSlug: guild,
|
||||
},
|
||||
};
|
||||
|
||||
export const Greeting = (args) => <PreauthGreeting {...args} />;
|
|
@ -0,0 +1,32 @@
|
|||
import * as React from 'react';
|
||||
import { Avatar, utils as avatarUtils } from 'roleypoly/src/design-system/atoms/avatar';
|
||||
import { Guild } from 'roleypoly/src/design-system/shared-types';
|
||||
import { AccentTitle } from 'roleypoly/src/design-system/atoms/typography';
|
||||
import { Space } from 'roleypoly/src/design-system/atoms/space';
|
||||
import styled from 'styled-components';
|
||||
|
||||
type GreetingProps = {
|
||||
guildSlug: Guild;
|
||||
};
|
||||
|
||||
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={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>
|
||||
);
|
1
src/design-system/molecules/preauth-greeting/index.ts
Normal file
1
src/design-system/molecules/preauth-greeting/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './PreauthGreeting';
|
29
src/design-system/molecules/preauth-secret-code/BUILD.bazel
Normal file
29
src/design-system/molecules/preauth-secret-code/BUILD.bazel
Normal file
|
@ -0,0 +1,29 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
load("//:hack/jest.bzl", "jest_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "preauth-secret-code",
|
||||
deps = [
|
||||
"react",
|
||||
"react-icons",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/button",
|
||||
"//src/design-system/atoms/fader",
|
||||
"//src/design-system/atoms/space",
|
||||
"//src/design-system/atoms/text-input",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
||||
|
||||
jest_test(
|
||||
src = ":preauth-secret-code",
|
||||
deps = [
|
||||
"//src/design-system/atoms/button",
|
||||
"//src/design-system/atoms/fader",
|
||||
"//src/design-system/atoms/text-input",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,37 @@
|
|||
jest.unmock('roleypoly/src/design-system/atoms/text-input');
|
||||
jest.unmock('./PreauthSecretCode');
|
||||
|
||||
import { Button } from 'roleypoly/src/design-system/atoms/button';
|
||||
import { TextInputWithIcon } from 'roleypoly/src/design-system/atoms/text-input';
|
||||
import { shallow } from 'enzyme';
|
||||
import * as React from 'react';
|
||||
import { PreauthSecretCode } from './PreauthSecretCode';
|
||||
import { FaderOpacity } from 'roleypoly/src/design-system/atoms/fader';
|
||||
|
||||
const value = 'unfathomable fishy sticks';
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
it('sends the secret code when submitted', () => {
|
||||
const view = shallow(<PreauthSecretCode onSubmit={onSubmit} />);
|
||||
|
||||
view.find(TextInputWithIcon).simulate('change', { target: { value } });
|
||||
|
||||
view.find(Button).simulate('click');
|
||||
expect(onSubmit).toBeCalledWith(value);
|
||||
});
|
||||
|
||||
it('shows the submit button when secret code is not empty', () => {
|
||||
const view = shallow(<PreauthSecretCode onSubmit={onSubmit} />);
|
||||
|
||||
view.find(TextInputWithIcon).simulate('change', { target: { value } });
|
||||
|
||||
expect(view.find(FaderOpacity).props().isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('hides the submit button when secret code is empty', () => {
|
||||
const view = shallow(<PreauthSecretCode onSubmit={onSubmit} />);
|
||||
|
||||
view.find(TextInputWithIcon).simulate('change', { target: { value: '' } });
|
||||
|
||||
expect(view.find(FaderOpacity).props().isVisible).toBe(false);
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import * as React from 'react';
|
||||
import { PreauthSecretCode } from './PreauthSecretCode';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/Preauth/Secret Code',
|
||||
component: PreauthSecretCode,
|
||||
};
|
||||
|
||||
export const SecretCode = (args) => <PreauthSecretCode {...args} />;
|
|
@ -0,0 +1,46 @@
|
|||
import * as React from 'react';
|
||||
import { TextInputWithIcon } from 'roleypoly/src/design-system/atoms/text-input';
|
||||
import { FiKey } from 'react-icons/fi';
|
||||
import { FaderOpacity } from 'roleypoly/src/design-system/atoms/fader';
|
||||
import { Button } from 'roleypoly/src/design-system/atoms/button';
|
||||
import { Space } from 'roleypoly/src/design-system/atoms/space';
|
||||
|
||||
type PreauthProps = {
|
||||
onSubmit: (code: string) => void;
|
||||
};
|
||||
|
||||
export const PreauthSecretCode = (props: PreauthProps) => {
|
||||
const [secretCode, setSecretCode] = React.useState('');
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSecretCode(event.target.value);
|
||||
};
|
||||
|
||||
const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
props.onSubmit(secretCode);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
props.onSubmit(secretCode);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextInputWithIcon
|
||||
icon={<FiKey />}
|
||||
value={secretCode}
|
||||
placeholder="Super secret code..."
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyPress}
|
||||
/>
|
||||
<Space />
|
||||
<FaderOpacity isVisible={secretCode.length > 0}>
|
||||
<Button color="muted" onClick={handleSubmit}>
|
||||
Submit Code →
|
||||
</Button>
|
||||
</FaderOpacity>
|
||||
</div>
|
||||
);
|
||||
};
|
1
src/design-system/molecules/preauth-secret-code/index.ts
Normal file
1
src/design-system/molecules/preauth-secret-code/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './PreauthSecretCode';
|
24
src/design-system/molecules/reset-submit/BUILD.bazel
Normal file
24
src/design-system/molecules/reset-submit/BUILD.bazel
Normal file
|
@ -0,0 +1,24 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
load("//:hack/jest.bzl", "jest_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "reset-submit",
|
||||
deps = [
|
||||
"react",
|
||||
"react-icons",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/breakpoints",
|
||||
"//src/design-system/atoms/button",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
||||
|
||||
jest_test(
|
||||
src = ":reset-submit",
|
||||
deps = [
|
||||
"//src/design-system/atoms/button",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
import { Button } from 'roleypoly/src/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();
|
||||
});
|
|
@ -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} />;
|
|
@ -0,0 +1,20 @@
|
|||
import styled from 'styled-components';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
|
||||
|
||||
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;
|
||||
`;
|
42
src/design-system/molecules/reset-submit/ResetSubmit.tsx
Normal file
42
src/design-system/molecules/reset-submit/ResetSubmit.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
|
||||
import { Button } from 'roleypoly/src/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>
|
||||
);
|
||||
};
|
1
src/design-system/molecules/reset-submit/index.ts
Normal file
1
src/design-system/molecules/reset-submit/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ResetSubmit';
|
28
src/design-system/molecules/server-masthead/BUILD.bazel
Normal file
28
src/design-system/molecules/server-masthead/BUILD.bazel
Normal file
|
@ -0,0 +1,28 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
load("//:hack/jest.bzl", "jest_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "server-masthead",
|
||||
deps = [
|
||||
"next",
|
||||
"react",
|
||||
"react-icons",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/avatar",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/timings",
|
||||
"//src/design-system/atoms/typography",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
||||
|
||||
jest_test(
|
||||
src = ":server-masthead",
|
||||
deps = [
|
||||
"//src/design-system/shared-types",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,19 @@
|
|||
jest.unmock('./ServerMasthead');
|
||||
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { ServerMasthead } from './ServerMasthead';
|
||||
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import { Editable } from './ServerMasthead.styled';
|
||||
|
||||
it('shows Edit Server when editable is true', () => {
|
||||
const view = shallow(<ServerMasthead editable={true} guild={guild} />);
|
||||
|
||||
expect(view.find(Editable).length).not.toBe(0);
|
||||
});
|
||||
|
||||
it('hides Edit Server when editable is true', () => {
|
||||
const view = shallow(<ServerMasthead editable={false} guild={guild} />);
|
||||
|
||||
expect(view.find(Editable).length).toBe(0);
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import * as React from 'react';
|
||||
import { ServerMasthead } from './ServerMasthead';
|
||||
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
|
||||
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,
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
import styled from 'styled-components';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
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};
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,36 @@
|
|||
import { Guild } from 'roleypoly/src/design-system/shared-types';
|
||||
import { Avatar, utils } from 'roleypoly/src/design-system/atoms/avatar';
|
||||
import { AccentTitle, AmbientLarge } from 'roleypoly/src/design-system/atoms/typography';
|
||||
import Link from 'next/link';
|
||||
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import * as React from 'react';
|
||||
import { GoPencil } from 'react-icons/go';
|
||||
import { Editable, Icon, Name, Wrapper } from './ServerMasthead.styled';
|
||||
|
||||
export type ServerMastheadProps = {
|
||||
guild: Guild;
|
||||
editable: boolean;
|
||||
};
|
||||
|
||||
export const ServerMasthead = (props: ServerMastheadProps) => {
|
||||
return (
|
||||
<Wrapper>
|
||||
<Icon>
|
||||
<Avatar size={props.editable ? 60 : 48} src={guild.icon}>
|
||||
{utils.initialsFromName(props.guild.name)}
|
||||
</Avatar>
|
||||
</Icon>
|
||||
<Name>
|
||||
<AccentTitle>{props.guild.name}</AccentTitle>
|
||||
{props.editable && (
|
||||
<Link href="/s/[id]/edit" as={`/s/${props.guild.id}/edit`}>
|
||||
<Editable role="button">
|
||||
<GoPencil />
|
||||
<AmbientLarge>Edit Server</AmbientLarge>
|
||||
</Editable>
|
||||
</Link>
|
||||
)}
|
||||
</Name>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
1
src/design-system/molecules/server-masthead/index.ts
Normal file
1
src/design-system/molecules/server-masthead/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ServerMasthead';
|
17
src/design-system/molecules/user-avatar-group/BUILD.bazel
Normal file
17
src/design-system/molecules/user-avatar-group/BUILD.bazel
Normal file
|
@ -0,0 +1,17 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "user-avatar-group",
|
||||
deps = [
|
||||
"react",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/avatar",
|
||||
"//src/design-system/atoms/breakpoints",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from 'react';
|
||||
import { UserAvatarGroup } from './UserAvatarGroup';
|
||||
import { user } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import { Hero } from 'roleypoly/src/design-system/atoms/hero';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/User Avatar Group',
|
||||
component: UserAvatarGroup,
|
||||
args: {
|
||||
user,
|
||||
preventCollapse: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = (args) => (
|
||||
<Hero>
|
||||
<UserAvatarGroup {...args} />
|
||||
</Hero>
|
||||
);
|
|
@ -0,0 +1,30 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
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;
|
||||
`;
|
|
@ -0,0 +1,24 @@
|
|||
import * as React from 'react';
|
||||
import { DiscordUser } from 'roleypoly/src/design-system/shared-types';
|
||||
import { utils, Avatar } from 'roleypoly/src/design-system/atoms/avatar';
|
||||
import { Group, Collapse, Discriminator, 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>
|
||||
|
||||
</Collapse>
|
||||
<Avatar size={34} src={props.user.avatar}>
|
||||
{utils.initialsFromName(props.user.username)}
|
||||
</Avatar>
|
||||
</Group>
|
||||
);
|
1
src/design-system/molecules/user-avatar-group/index.ts
Normal file
1
src/design-system/molecules/user-avatar-group/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './UserAvatarGroup';
|
20
src/design-system/molecules/user-popover/BUILD.bazel
Normal file
20
src/design-system/molecules/user-popover/BUILD.bazel
Normal file
|
@ -0,0 +1,20 @@
|
|||
load("//:hack/react.bzl", "react_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
react_library(
|
||||
name = "user-popover",
|
||||
deps = [
|
||||
"next",
|
||||
"react",
|
||||
"react-icons",
|
||||
"styled-components",
|
||||
"//src/design-system/atoms/breakpoints",
|
||||
"//src/design-system/atoms/colors",
|
||||
"//src/design-system/atoms/timings",
|
||||
"//src/design-system/molecules/user-avatar-group",
|
||||
"//src/design-system/shared-types",
|
||||
"@types/react",
|
||||
"@types/styled-components",
|
||||
],
|
||||
)
|
|
@ -0,0 +1,18 @@
|
|||
import { user } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import * as React from 'react';
|
||||
import { UserPopover as UserPopoverComponent } from './UserPopover';
|
||||
import { PopoverBase } from 'roleypoly/src/design-system/atoms/popover/Popover.styled';
|
||||
|
||||
export default {
|
||||
title: 'Molecules/User Popover',
|
||||
component: UserPopoverComponent,
|
||||
args: {
|
||||
user,
|
||||
},
|
||||
};
|
||||
|
||||
export const UserPopover = (args) => (
|
||||
<PopoverBase active>
|
||||
<UserPopoverComponent {...args} />
|
||||
</PopoverBase>
|
||||
);
|
|
@ -0,0 +1,34 @@
|
|||
import styled from 'styled-components';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { transitions } from 'roleypoly/src/design-system/atoms/timings';
|
||||
import * as _ from 'styled-components'; // tslint:disable-line:no-duplicate-imports
|
||||
|
||||
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;
|
||||
}
|
||||
`;
|
30
src/design-system/molecules/user-popover/UserPopover.tsx
Normal file
30
src/design-system/molecules/user-popover/UserPopover.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import * as React from 'react';
|
||||
import { DiscordUser } from 'roleypoly/src/design-system/shared-types';
|
||||
import { UserAvatarGroup } from 'roleypoly/src/design-system/molecules/user-avatar-group';
|
||||
import { Base, NavAction } from './UserPopover.styled';
|
||||
import { GoGear, GoSignOut } from 'react-icons/go';
|
||||
import Link from 'next/link';
|
||||
|
||||
type UserPopoverProps = {
|
||||
user: DiscordUser;
|
||||
};
|
||||
|
||||
export const UserPopover = (props: UserPopoverProps) => (
|
||||
<Base>
|
||||
<UserAvatarGroup user={props.user} preventCollapse={true} />
|
||||
<NavAction>
|
||||
<Link href="/user/settings">
|
||||
<>
|
||||
Settings <GoGear />
|
||||
</>
|
||||
</Link>
|
||||
</NavAction>
|
||||
<NavAction>
|
||||
<Link href="/auth/machinery/logout">
|
||||
<>
|
||||
Log Out <GoSignOut />
|
||||
</>
|
||||
</Link>
|
||||
</NavAction>
|
||||
</Base>
|
||||
);
|
1
src/design-system/molecules/user-popover/index.ts
Normal file
1
src/design-system/molecules/user-popover/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './UserPopover';
|
18
src/design-system/organisms/app-shell/AppShell.story.tsx
Normal file
18
src/design-system/organisms/app-shell/AppShell.story.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react';
|
||||
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
|
||||
import { AppShell } from './AppShell';
|
||||
import { rpUser, guildEnum } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
|
||||
const story = organismStories('App Shell', module);
|
||||
|
||||
story.add('Guest', () => (
|
||||
<AppShell showFooter user={null}>
|
||||
<h1>Hello World</h1>
|
||||
</AppShell>
|
||||
));
|
||||
|
||||
story.add('Logged In', () => (
|
||||
<AppShell user={rpUser} guildEnumeration={guildEnum}>
|
||||
<h1>Hello World</h1>
|
||||
</AppShell>
|
||||
));
|
22
src/design-system/organisms/app-shell/AppShell.styled.tsx
Normal file
22
src/design-system/organisms/app-shell/AppShell.styled.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
|
||||
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;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
41
src/design-system/organisms/app-shell/AppShell.tsx
Normal file
41
src/design-system/organisms/app-shell/AppShell.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import * as React from 'react';
|
||||
import * as Masthead from 'roleypoly/src/design-system/organisms/masthead';
|
||||
import { RoleypolyUser } from '@roleypoly/rpc/shared';
|
||||
import { Footer } from 'roleypoly/src/design-system/molecules/footer';
|
||||
import { Content, GlobalStyles } from './AppShell.styled';
|
||||
import { GlobalStyleColors } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { GuildEnumeration } from '@roleypoly/rpc/platform';
|
||||
import { Scrollbars } from 'react-custom-scrollbars';
|
||||
|
||||
type AppShellProps = {
|
||||
children: React.ReactNode;
|
||||
user: RoleypolyUser.AsObject | null;
|
||||
showFooter?: boolean;
|
||||
small?: boolean;
|
||||
activeGuildId?: string | null;
|
||||
guildEnumeration?: GuildEnumeration.AsObject;
|
||||
};
|
||||
|
||||
export const AppShell = (props: AppShellProps) => (
|
||||
<>
|
||||
<GlobalStyles />
|
||||
<GlobalStyleColors />
|
||||
{props.user !== null ? (
|
||||
<Masthead.Authed
|
||||
guildEnumeration={props.guildEnumeration || { guildsList: [] }}
|
||||
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>
|
||||
</>
|
||||
);
|
1
src/design-system/organisms/app-shell/index.ts
Normal file
1
src/design-system/organisms/app-shell/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './AppShell';
|
|
@ -0,0 +1,16 @@
|
|||
import * as React from 'react';
|
||||
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
|
||||
import { ErrorBanner } from './ErrorBanner';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
|
||||
const story = organismStories('Error Banner', module);
|
||||
|
||||
story.add('Error Banner', () => (
|
||||
<ErrorBanner
|
||||
message={{
|
||||
english: text('English', 'Primary Text'),
|
||||
japanese: text('Japanese (Subtext)', 'Subtext'),
|
||||
friendlyCode: text('Friendly Code', 'Oops!'),
|
||||
}}
|
||||
/>
|
||||
));
|
|
@ -0,0 +1,40 @@
|
|||
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { text300, text500, text700 } from 'roleypoly/src/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};
|
||||
`;
|
24
src/design-system/organisms/error-banner/ErrorBanner.tsx
Normal file
24
src/design-system/organisms/error-banner/ErrorBanner.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
ErrorWrapper,
|
||||
ErrorDivider,
|
||||
ErrorSideCode,
|
||||
ErrorText,
|
||||
ErrorTextLower,
|
||||
} from './ErrorBanner.styled';
|
||||
import { ErrorMessage } from 'templates/errors/errorStrings';
|
||||
|
||||
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>
|
||||
);
|
1
src/design-system/organisms/error-banner/index.ts
Normal file
1
src/design-system/organisms/error-banner/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ErrorBanner';
|
|
@ -0,0 +1,19 @@
|
|||
import * as React from 'react';
|
||||
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
|
||||
import { HelpPageBase } from './HelpPageBase';
|
||||
import { Content } from 'roleypoly/src/design-system/organisms/app-shell/AppShell.styled';
|
||||
|
||||
const baseStory = organismStories('Help Pages', module);
|
||||
|
||||
export const HelpStoryWrapper = (props: { children: React.ReactNode }) => (
|
||||
<Content>
|
||||
<HelpPageBase>{props.children}</HelpPageBase>
|
||||
</Content>
|
||||
);
|
||||
|
||||
baseStory.add('Base', () => (
|
||||
<HelpStoryWrapper>
|
||||
<h1>What is the world but vibrations?</h1>
|
||||
<p>Vibrations that synchronize and tie it together, running free forever.</p>
|
||||
</HelpStoryWrapper>
|
||||
));
|
21
src/design-system/organisms/help-page-base/HelpPageBase.tsx
Normal file
21
src/design-system/organisms/help-page-base/HelpPageBase.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
|
||||
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>
|
||||
);
|
1
src/design-system/organisms/help-page-base/index.ts
Normal file
1
src/design-system/organisms/help-page-base/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './HelpPageBase';
|
|
@ -0,0 +1,10 @@
|
|||
import { WhyNoRoles } from './WhyNoRoles';
|
||||
import * as React from 'react';
|
||||
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
|
||||
import { HelpStoryWrapper } from 'roleypoly/src/design-system/organisms/help-page-base/HelpPageBase.story';
|
||||
|
||||
organismStories('Help Pages/Pages', module).add('Why No Roles', () => (
|
||||
<HelpStoryWrapper>
|
||||
<WhyNoRoles />
|
||||
</HelpStoryWrapper>
|
||||
));
|
|
@ -0,0 +1,35 @@
|
|||
import styled, { css } from 'styled-components';
|
||||
import { palette, numberToChroma } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { Role } from '@roleypoly/rpc/shared';
|
||||
|
||||
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: 175px;
|
||||
`;
|
||||
|
||||
const hover = (roleColor: string) => css`
|
||||
color: #efefef;
|
||||
background-color: ${roleColor};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const DiscordRole = styled.div<{
|
||||
discordRole: Role.AsObject;
|
||||
isRoleypoly: boolean;
|
||||
}>`
|
||||
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())}
|
||||
`;
|
86
src/design-system/organisms/help-why-no-roles/WhyNoRoles.tsx
Normal file
86
src/design-system/organisms/help-why-no-roles/WhyNoRoles.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
HalfsiesContainer,
|
||||
HalfsiesItem,
|
||||
} from 'roleypoly/src/design-system/atoms/halfsies';
|
||||
import { FaCheck, FaTimes } from 'react-icons/fa';
|
||||
import { DiscordBase, DiscordRole } from './WhyNoRoles.styled';
|
||||
import { demoData } from 'roleypoly/src/design-system/shared-types/demoData';
|
||||
import { Role } from '@roleypoly/rpc/shared';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import chroma from 'chroma-js';
|
||||
import { SparkleOverlay } from 'roleypoly/src/design-system/atoms/sparkle';
|
||||
|
||||
const adminRoles: Role.AsObject[] = [
|
||||
{
|
||||
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.AsObject = {
|
||||
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.AsObject[]; 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>
|
||||
);
|
1
src/design-system/organisms/help-why-no-roles/index.ts
Normal file
1
src/design-system/organisms/help-why-no-roles/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './WhyNoRoles';
|
7
src/design-system/organisms/landing/Landing.story.tsx
Normal file
7
src/design-system/organisms/landing/Landing.story.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
|
||||
import { Landing } from './Landing';
|
||||
|
||||
const story = organismStories('Landing', module);
|
||||
|
||||
story.add('Landing', () => <Landing />);
|
31
src/design-system/organisms/landing/Landing.styled.ts
Normal file
31
src/design-system/organisms/landing/Landing.styled.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { onTablet } from 'roleypoly/src/design-system/atoms/breakpoints';
|
||||
import { text400 } from 'roleypoly/src/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;
|
||||
`;
|
41
src/design-system/organisms/landing/Landing.tsx
Normal file
41
src/design-system/organisms/landing/Landing.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { Space } from 'roleypoly/src/design-system/atoms/space';
|
||||
import { LargeText, LargeTitle } from 'roleypoly/src/design-system/atoms/typography';
|
||||
import { DemoDiscord } from 'roleypoly/src/design-system/molecules/demo-discord';
|
||||
import { DemoPicker } from 'roleypoly/src/design-system/molecules/demo-picker';
|
||||
import * as React from 'react';
|
||||
import { DemoAlignment, DemoSubtitle, HeroCentering, HeroText } from './Landing.styled';
|
||||
import {
|
||||
HalfsiesContainer,
|
||||
HalfsiesItem,
|
||||
} from 'roleypoly/src/design-system/atoms/halfsies';
|
||||
|
||||
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 {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>
|
||||
);
|
1
src/design-system/organisms/landing/index.ts
Normal file
1
src/design-system/organisms/landing/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Landing';
|
99
src/design-system/organisms/masthead/Authed.tsx
Normal file
99
src/design-system/organisms/masthead/Authed.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { GuildEnumeration } from '@roleypoly/rpc/platform';
|
||||
import { RoleypolyUser } from '@roleypoly/rpc/shared';
|
||||
import { Logomark } from 'roleypoly/src/design-system/atoms/branding';
|
||||
import { Popover } from 'roleypoly/src/design-system/atoms/popover';
|
||||
import { guildEnum } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import { GuildNav } from 'roleypoly/src/design-system/molecules/guild-nav';
|
||||
import { NavSlug } from 'roleypoly/src/design-system/molecules/nav-slug';
|
||||
import { UserAvatarGroup } from 'roleypoly/src/design-system/molecules/user-avatar-group';
|
||||
import { UserPopover } from 'roleypoly/src/design-system/molecules/user-popover';
|
||||
import Link from 'next/link';
|
||||
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: RoleypolyUser.AsObject;
|
||||
activeGuildId: string | null;
|
||||
guildEnumeration: GuildEnumeration.AsObject;
|
||||
};
|
||||
|
||||
export const Authed = (props: Props) => {
|
||||
const [userPopoverState, setUserPopoverState] = React.useState(false);
|
||||
const [serverPopoverState, setServerPopoverState] = React.useState(false);
|
||||
|
||||
return (
|
||||
<MastheadBase>
|
||||
<MastheadAlignment>
|
||||
<MastheadLeft>
|
||||
<Link href="/dashboard" passHref>
|
||||
<MastheadA>
|
||||
<Logomark height={40} />
|
||||
</MastheadA>
|
||||
</Link>
|
||||
<InteractionBase
|
||||
onClick={() => {
|
||||
setServerPopoverState(true);
|
||||
setUserPopoverState(false);
|
||||
}}
|
||||
hide={!serverPopoverState}
|
||||
>
|
||||
<NavSlug
|
||||
guild={
|
||||
guildEnum.guildsList.find(
|
||||
(g) => g.id === props.activeGuildId
|
||||
)?.guild || null
|
||||
}
|
||||
/>
|
||||
</InteractionBase>
|
||||
<Popover
|
||||
headContent={
|
||||
<GuildPopoverHead>
|
||||
<GoOrganization />
|
||||
My Guilds
|
||||
</GuildPopoverHead>
|
||||
}
|
||||
canDefocus
|
||||
position="bottom left"
|
||||
active={serverPopoverState}
|
||||
onExit={() => setServerPopoverState(false)}
|
||||
>
|
||||
<GuildNav guildEnumeration={props.guildEnumeration} />
|
||||
</Popover>
|
||||
</MastheadLeft>
|
||||
<MastheadRight>
|
||||
<InteractionBase
|
||||
onClick={() => {
|
||||
setUserPopoverState(true);
|
||||
setServerPopoverState(false);
|
||||
}}
|
||||
hide={!userPopoverState}
|
||||
>
|
||||
{props.user.discorduser && (
|
||||
<UserAvatarGroup user={props.user.discorduser} />
|
||||
)}
|
||||
</InteractionBase>
|
||||
<Popover
|
||||
headContent={<></>}
|
||||
canDefocus
|
||||
position="top right"
|
||||
active={userPopoverState}
|
||||
onExit={() => setUserPopoverState(false)}
|
||||
>
|
||||
{props.user.discorduser && (
|
||||
<UserPopover user={props.user.discorduser} />
|
||||
)}
|
||||
</Popover>
|
||||
</MastheadRight>
|
||||
</MastheadAlignment>
|
||||
</MastheadBase>
|
||||
);
|
||||
};
|
39
src/design-system/organisms/masthead/Guest.tsx
Normal file
39
src/design-system/organisms/masthead/Guest.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { Logotype } from 'roleypoly/src/design-system/atoms/branding';
|
||||
import { Button } from 'roleypoly/src/design-system/atoms/button';
|
||||
import Link from 'next/link';
|
||||
import * as React from 'react';
|
||||
import { FaSignInAlt } from 'react-icons/fa';
|
||||
import {
|
||||
MastheadBase,
|
||||
MastheadLeft,
|
||||
MastheadRight,
|
||||
MastheadAlignment,
|
||||
MastheadA,
|
||||
} from './Masthead.styled';
|
||||
|
||||
export const Guest = () => (
|
||||
<MastheadBase>
|
||||
<MastheadAlignment>
|
||||
<MastheadLeft>
|
||||
<Link href="/" passHref>
|
||||
<MastheadA>
|
||||
<Logotype height={30} />
|
||||
</MastheadA>
|
||||
</Link>
|
||||
</MastheadLeft>
|
||||
<MastheadRight>
|
||||
<Link href="/auth/login" passHref>
|
||||
<MastheadA>
|
||||
<Button size="small">
|
||||
Login{' '}
|
||||
<FaSignInAlt
|
||||
size="1em"
|
||||
style={{ transform: 'translateY(1px)' }}
|
||||
/>
|
||||
</Button>
|
||||
</MastheadA>
|
||||
</Link>
|
||||
</MastheadRight>
|
||||
</MastheadAlignment>
|
||||
</MastheadBase>
|
||||
);
|
21
src/design-system/organisms/masthead/Masthead.story.tsx
Normal file
21
src/design-system/organisms/masthead/Masthead.story.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
rpUser,
|
||||
guild,
|
||||
guildEnum,
|
||||
} from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
|
||||
import * as React from 'react';
|
||||
import { Authed } from './Authed';
|
||||
import { Guest } from './Guest';
|
||||
|
||||
const rootStory = organismStories('Masthead', module);
|
||||
const userStory = organismStories('Masthead/User', module);
|
||||
|
||||
userStory.add('Has Guilds', () => (
|
||||
<Authed guildEnumeration={guildEnum} activeGuildId={guild.id} user={rpUser} />
|
||||
));
|
||||
userStory.add('No Guilds (New User)', () => (
|
||||
<Authed guildEnumeration={{ guildsList: [] }} activeGuildId={null} user={rpUser} />
|
||||
));
|
||||
|
||||
rootStory.add('Guest', () => <Guest />);
|
91
src/design-system/organisms/masthead/Masthead.styled.tsx
Normal file
91
src/design-system/organisms/masthead/Masthead.styled.tsx
Normal file
|
@ -0,0 +1,91 @@
|
|||
import { onSmallScreen } from 'roleypoly/src/design-system/atoms/breakpoints';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
import { transitions } from 'roleypoly/src/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;
|
||||
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;
|
||||
`)}
|
||||
}
|
||||
`;
|
2
src/design-system/organisms/masthead/index.ts
Normal file
2
src/design-system/organisms/masthead/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export * from './Guest';
|
||||
export * from './Authed';
|
32
src/design-system/organisms/preauth/Preauth.story.tsx
Normal file
32
src/design-system/organisms/preauth/Preauth.story.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import * as React from 'react';
|
||||
import { Preauth } from './Preauth';
|
||||
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
|
||||
import { guild } from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const story = organismStories('Preauth', module);
|
||||
|
||||
const Center = styled.div`
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
story.add('No Slug', () => {
|
||||
return (
|
||||
<Center>
|
||||
<Preauth botName="roleypoly#3266" onSendSecretCode={action('secret code!')} />
|
||||
</Center>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('With Slug', () => {
|
||||
return (
|
||||
<Center>
|
||||
<Preauth
|
||||
botName="roleypoly#3266"
|
||||
guildSlug={guild}
|
||||
onSendSecretCode={action('secret code!')}
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
});
|
57
src/design-system/organisms/preauth/Preauth.tsx
Normal file
57
src/design-system/organisms/preauth/Preauth.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { Guild } from '@roleypoly/rpc/shared';
|
||||
import { Button } from 'roleypoly/src/design-system/atoms/button';
|
||||
import { Space } from 'roleypoly/src/design-system/atoms/space';
|
||||
import { PreauthGreeting } from 'roleypoly/src/design-system/molecules/preauth-greeting';
|
||||
import { PreauthSecretCode } from 'roleypoly/src/design-system/molecules/preauth-secret-code';
|
||||
import * as React from 'react';
|
||||
import { FaDiscord } from 'react-icons/fa';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export type PreauthProps = {
|
||||
guildSlug?: Guild.AsObject;
|
||||
onSendSecretCode: (code: string) => void;
|
||||
botName?: 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>
|
||||
<Button
|
||||
color="discord"
|
||||
icon={
|
||||
<div style={{ position: 'relative', top: 3 }}>
|
||||
<FaDiscord />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Sign in with Discord
|
||||
</Button>
|
||||
</WidthContainer>
|
||||
<Space />
|
||||
<WidthContainer>
|
||||
<p>
|
||||
Or, send a message saying "login" to{' '}
|
||||
<b>{props.botName || 'roleypoly'}</b>
|
||||
</p>
|
||||
<PreauthSecretCode onSubmit={props.onSendSecretCode} />
|
||||
</WidthContainer>
|
||||
</Centered>
|
||||
);
|
||||
};
|
43
src/design-system/organisms/role-picker/RolePicker.spec.tsx
Normal file
43
src/design-system/organisms/role-picker/RolePicker.spec.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
jest.unmock('atoms/role')
|
||||
.unmock('atoms/button')
|
||||
.unmock('molecules/picker-category')
|
||||
.unmock('organisms/role-picker');
|
||||
|
||||
import { Role } from 'roleypoly/src/design-system/atoms/role';
|
||||
import { shallow } from 'enzyme';
|
||||
import {
|
||||
guild,
|
||||
guildData,
|
||||
guildRoles,
|
||||
member,
|
||||
mockCategorySingle,
|
||||
} from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import { ResetSubmit } from 'roleypoly/src/design-system/molecules/reset-submit';
|
||||
import { PickerCategory } from 'roleypoly/src/design-system/molecules/picker-category';
|
||||
import * as React from 'react';
|
||||
import { RolePicker, RolePickerProps } from './RolePicker';
|
||||
|
||||
it('unselects the rest of a category in single mode', () => {
|
||||
const props: RolePickerProps = {
|
||||
guildData: { ...guildData, categoriesList: [mockCategorySingle] },
|
||||
member: { ...member, rolesList: [] },
|
||||
roles: guildRoles,
|
||||
guild: guild,
|
||||
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.rolesList[0]]);
|
||||
|
||||
roles.last().props().onClick?.(true);
|
||||
|
||||
view.find(ResetSubmit).props().onSubmit();
|
||||
expect(props.onSubmit).toBeCalledWith([mockCategorySingle.rolesList[1]]);
|
||||
});
|
50
src/design-system/organisms/role-picker/RolePicker.story.tsx
Normal file
50
src/design-system/organisms/role-picker/RolePicker.story.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import * as React from 'react';
|
||||
import { RolePicker, RolePickerProps } from './RolePicker';
|
||||
import { organismStories } from 'roleypoly/src/design-system/organisms/organisms.story';
|
||||
import {
|
||||
guildData,
|
||||
member,
|
||||
guildRoles,
|
||||
guild,
|
||||
} from 'roleypoly/src/design-system/shared-types/storyData';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
const storyPublic = organismStories('Role Picker/Public', module);
|
||||
const storyEditable = organismStories('Role Picker/Editable', module);
|
||||
|
||||
const props: RolePickerProps = {
|
||||
guildData: guildData,
|
||||
member: member,
|
||||
guild: guild,
|
||||
roles: guildRoles,
|
||||
onSubmit: action('onSubmit'),
|
||||
editable: false,
|
||||
};
|
||||
|
||||
const storyBuilder = (
|
||||
story: typeof storyPublic,
|
||||
mixinProps: Partial<RolePickerProps>
|
||||
) => {
|
||||
story.add('Full', () => <RolePicker {...{ ...props, ...mixinProps }} />);
|
||||
story.add('No Message', () => (
|
||||
<RolePicker
|
||||
{...{
|
||||
...props,
|
||||
guildData: { ...props.guildData, message: '' },
|
||||
...mixinProps,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
story.add('No Categories', () => (
|
||||
<RolePicker
|
||||
{...{
|
||||
...props,
|
||||
guildData: { ...props.guildData, message: '', categoriesList: [] },
|
||||
...mixinProps,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
storyBuilder(storyPublic, {});
|
||||
storyBuilder(storyEditable, { editable: true });
|
|
@ -0,0 +1,34 @@
|
|||
import styled from 'styled-components';
|
||||
import { palette } from 'roleypoly/src/design-system/atoms/colors';
|
||||
|
||||
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;
|
||||
`;
|
135
src/design-system/organisms/role-picker/RolePicker.tsx
Normal file
135
src/design-system/organisms/role-picker/RolePicker.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
import { Member } from '@roleypoly/rpc/discord';
|
||||
import { Category, GuildData } from '@roleypoly/rpc/platform';
|
||||
import { Guild, GuildRoles, Role } from '@roleypoly/rpc/shared';
|
||||
import { FaderOpacity } from 'roleypoly/src/design-system/atoms/fader';
|
||||
import { Space } from 'roleypoly/src/design-system/atoms/space';
|
||||
import { ResetSubmit } from 'roleypoly/src/design-system/molecules/reset-submit';
|
||||
import { ServerMasthead } from 'roleypoly/src/design-system/molecules/server-masthead';
|
||||
import { PickerCategory } from 'roleypoly/src/design-system/molecules/picker-category';
|
||||
import * as React from 'react';
|
||||
import { GoInfo } from 'react-icons/go';
|
||||
import {
|
||||
CategoryContainer,
|
||||
Container,
|
||||
InfoBox,
|
||||
InfoIcon,
|
||||
MessageBox,
|
||||
} from './RolePicker.styled';
|
||||
import { ReactifyNewlines } from 'utils/ReactifyNewlines';
|
||||
|
||||
export type RolePickerProps = {
|
||||
guild: Guild.AsObject;
|
||||
guildData: GuildData.AsObject;
|
||||
member: Member.AsObject;
|
||||
roles: GuildRoles.AsObject;
|
||||
onSubmit: (selectedRoles: string[]) => void;
|
||||
editable: boolean;
|
||||
};
|
||||
|
||||
const arrayMatches = (a: any[], b: any[]) => {
|
||||
return (
|
||||
a === b ||
|
||||
(a.length === b.length &&
|
||||
a.every((x) => b.includes(x)) &&
|
||||
b.every((x) => a.includes(x)))
|
||||
);
|
||||
};
|
||||
|
||||
export const RolePicker = (props: RolePickerProps) => {
|
||||
const [selectedRoles, updateSelectedRoles] = React.useState<string[]>(
|
||||
props.member.rolesList
|
||||
);
|
||||
|
||||
const handleChange = (category: Category.AsObject) => (role: Role.AsObject) => (
|
||||
newState: boolean
|
||||
) => {
|
||||
if (category.type === Category.CategoryType.SINGLE) {
|
||||
updateSelectedRoles(
|
||||
newState === true
|
||||
? [
|
||||
...selectedRoles.filter((x) => !category.rolesList.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.categoriesList.length !== 0 ? (
|
||||
<>
|
||||
<div>
|
||||
{props.guildData.categoriesList.map((category, idx) => (
|
||||
<CategoryContainer key={idx}>
|
||||
<PickerCategory
|
||||
key={idx}
|
||||
category={category}
|
||||
title={category.name}
|
||||
selectedRoles={selectedRoles.filter((roleId) =>
|
||||
category.rolesList.includes(roleId)
|
||||
)}
|
||||
roles={
|
||||
category.rolesList
|
||||
.map((role) =>
|
||||
props.roles.rolesList.find(
|
||||
(r) => r.id === role
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
(r) => r !== undefined
|
||||
) as Role.AsObject[]
|
||||
}
|
||||
onChange={handleChange(category)}
|
||||
wikiMode={false}
|
||||
type={
|
||||
category.type === Category.CategoryType.SINGLE
|
||||
? 'single'
|
||||
: 'multi'
|
||||
}
|
||||
/>
|
||||
</CategoryContainer>
|
||||
))}
|
||||
</div>
|
||||
<FaderOpacity
|
||||
isVisible={!arrayMatches(selectedRoles, props.member.rolesList)}
|
||||
>
|
||||
<ResetSubmit
|
||||
onSubmit={() => props.onSubmit(selectedRoles)}
|
||||
onReset={() => {
|
||||
updateSelectedRoles(props.member.rolesList);
|
||||
}}
|
||||
/>
|
||||
</FaderOpacity>
|
||||
</>
|
||||
) : (
|
||||
<InfoBox>
|
||||
<InfoIcon>
|
||||
<GoInfo />
|
||||
</InfoIcon>
|
||||
<div>
|
||||
There are currently no roles available for you to choose from.
|
||||
</div>
|
||||
</InfoBox>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
};
|
1
src/design-system/organisms/role-picker/index.ts
Normal file
1
src/design-system/organisms/role-picker/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './RolePicker';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue