mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-25 03:49:11 +00:00
feat(design-system): pre-port of roleypoly/ui
This commit is contained in:
parent
c41fcabfd0
commit
ea2683da00
98 changed files with 2339 additions and 0 deletions
8
src/design-system/BUILD.bazel
Normal file
8
src/design-system/BUILD.bazel
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||||
|
|
||||||
|
package(default_visibility = ["//visibility:public"])
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "lib",
|
||||||
|
srcs = glob(["*.ts"]),
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { DemoDiscord } from './DemoDiscord';
|
||||||
|
|
||||||
|
const story = moleculeStories('Landing Demos', module);
|
||||||
|
|
||||||
|
story.add('Discord', () => <DemoDiscord />);
|
|
@ -0,0 +1,67 @@
|
||||||
|
import styled, { keyframes } from 'styled-components';
|
||||||
|
import { palette } from '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 'hack/fixtures/demoData';
|
||||||
|
import { Typist } from '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';
|
|
@ -0,0 +1,7 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import { DemoPicker } from './DemoPicker';
|
||||||
|
|
||||||
|
const story = moleculeStories('Landing Demos', module);
|
||||||
|
|
||||||
|
story.add('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 'atoms/role';
|
||||||
|
import { Role as RPCRole } from '@roleypoly/rpc/shared';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { demoData } from 'hack/fixtures/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.AsObject['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';
|
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>
|
||||||
|
);
|
7
src/design-system/molecules/footer/Footer.story.tsx
Normal file
7
src/design-system/molecules/footer/Footer.story.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import { Footer } from './Footer';
|
||||||
|
|
||||||
|
const story = moleculeStories('Footer', module);
|
||||||
|
|
||||||
|
story.add('Basic', () => <Footer />);
|
30
src/design-system/molecules/footer/Footer.styled.ts
Normal file
30
src/design-system/molecules/footer/Footer.styled.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { palette } from 'atoms/colors';
|
||||||
|
import { transitions } from '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 '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';
|
18
src/design-system/molecules/guild-nav/GuildNav.story.tsx
Normal file
18
src/design-system/molecules/guild-nav/GuildNav.story.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { GuildNav } from './GuildNav';
|
||||||
|
import { guildEnum } from 'hack/fixtures/storyData';
|
||||||
|
import { PopoverBase } from 'atoms/popover/Popover.styled';
|
||||||
|
|
||||||
|
const story = moleculeStories('Guild Nav', module);
|
||||||
|
|
||||||
|
story.add('Has Guilds', () => (
|
||||||
|
<PopoverBase active>
|
||||||
|
<GuildNav guildEnumeration={guildEnum} />
|
||||||
|
</PopoverBase>
|
||||||
|
));
|
||||||
|
story.add('No Guilds', () => (
|
||||||
|
<PopoverBase active>
|
||||||
|
<GuildNav guildEnumeration={{ guildsList: [] }} />
|
||||||
|
</PopoverBase>
|
||||||
|
));
|
21
src/design-system/molecules/guild-nav/GuildNav.styled.ts
Normal file
21
src/design-system/molecules/guild-nav/GuildNav.styled.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { transitions } from 'atoms/timings';
|
||||||
|
import { palette } from '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;
|
||||||
|
}
|
||||||
|
`;
|
64
src/design-system/molecules/guild-nav/GuildNav.tsx
Normal file
64
src/design-system/molecules/guild-nav/GuildNav.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { NavSlug } from 'molecules/nav-slug';
|
||||||
|
import { sortBy } from 'utils/sortBy';
|
||||||
|
import { GuildEnumeration, PresentableGuild } from '@roleypoly/rpc/platform';
|
||||||
|
import { hasPermission, permissions } from 'utils/hasPermission';
|
||||||
|
import { GoZap, GoStar } from 'react-icons/go';
|
||||||
|
import { Role } from '@roleypoly/rpc/shared';
|
||||||
|
import { GuildNavItem } from './GuildNav.styled';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
guildEnumeration: GuildEnumeration.AsObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tooltipId = 'guildnav';
|
||||||
|
|
||||||
|
const Badges = (props: { guild: PresentableGuild.AsObject }) => {
|
||||||
|
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.AsObject[];
|
||||||
|
|
||||||
|
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.map((g) => ({
|
||||||
|
...g,
|
||||||
|
nameLower: g.guild?.name.toLowerCase(),
|
||||||
|
})),
|
||||||
|
'nameLower'
|
||||||
|
).map(({ nameLower, ...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';
|
2
src/design-system/molecules/molecules.story.tsx
Normal file
2
src/design-system/molecules/molecules.story.tsx
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import { makeFactory } from '../.storybook/storyHelper';
|
||||||
|
export const moleculeStories = makeFactory('Molecules');
|
9
src/design-system/molecules/nav-slug/NavSlug.story.tsx
Normal file
9
src/design-system/molecules/nav-slug/NavSlug.story.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import { NavSlug } from './NavSlug';
|
||||||
|
import { guild } from 'hack/fixtures/storyData';
|
||||||
|
|
||||||
|
const story = moleculeStories('Server Slug', module);
|
||||||
|
|
||||||
|
story.add('Empty', () => <NavSlug guild={null} />);
|
||||||
|
story.add('Example', () => <NavSlug guild={guild} />);
|
16
src/design-system/molecules/nav-slug/NavSlug.styled.ts
Normal file
16
src/design-system/molecules/nav-slug/NavSlug.styled.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const SlugContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 5px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SlugName = styled.div`
|
||||||
|
padding: 0 10px;
|
||||||
|
position: relative;
|
||||||
|
top: -1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
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/rpc/shared';
|
||||||
|
import { Avatar, utils } from 'atoms/avatar';
|
||||||
|
import { SlugContainer, SlugName } from './NavSlug.styled';
|
||||||
|
import { GoOrganization } from 'react-icons/go';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
guild: Guild.AsObject | 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';
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import { PickerCategory, CategoryProps } from './PickerCategory';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { text, optionsKnob } from '@storybook/addon-knobs';
|
||||||
|
import { roleCategory, roleWikiData, mockCategory } from 'hack/fixtures/storyData';
|
||||||
|
|
||||||
|
const stories = moleculeStories('Picker Category', module);
|
||||||
|
|
||||||
|
const data: (mode?: 'single') => CategoryProps = (mode?: 'single') => ({
|
||||||
|
title: text('Title', 'Pronouns'),
|
||||||
|
type: 'multi',
|
||||||
|
roles: roleCategory,
|
||||||
|
wikiMode: false,
|
||||||
|
category: mockCategory,
|
||||||
|
onChange: () => action('onChange'),
|
||||||
|
selectedRoles: optionsKnob<string[]>(
|
||||||
|
'Selected Roles',
|
||||||
|
roleCategory.reduce((acc, x) => ({ ...acc, [x.name]: x.id }), {}),
|
||||||
|
[roleCategory[0].id],
|
||||||
|
{ display: mode === 'single' ? 'select' : 'multi-select' }
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
stories.add('Multi', () => {
|
||||||
|
const d = data();
|
||||||
|
return <PickerCategory {...d} type="multi" />;
|
||||||
|
});
|
||||||
|
stories.add('Single', () => {
|
||||||
|
const d = data('single');
|
||||||
|
return <PickerCategory {...d} type="single" />;
|
||||||
|
});
|
||||||
|
stories.add('Wiki', () => {
|
||||||
|
const d = data();
|
||||||
|
return <PickerCategory {...d} wikiMode roleWikiData={roleWikiData} />;
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const Head = styled.div`
|
||||||
|
margin: 7px 5px;
|
||||||
|
line-height: 200%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const HeadTitle = styled.div`
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const HeadSub = styled.div`
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: -4px;
|
||||||
|
`;
|
|
@ -0,0 +1,64 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Role as RPCRole } from '@roleypoly/rpc/shared';
|
||||||
|
import { Category as RPCCategory } from '@roleypoly/rpc/platform';
|
||||||
|
import { LargeText, AmbientLarge } from 'atoms/typography';
|
||||||
|
import { Role } from 'atoms/role';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import { Head, HeadTitle, HeadSub } from './PickerCategory.styled';
|
||||||
|
|
||||||
|
export type CategoryProps = {
|
||||||
|
title: string;
|
||||||
|
roles: RPCRole.AsObject[];
|
||||||
|
category: RPCCategory.AsObject;
|
||||||
|
selectedRoles: string[];
|
||||||
|
onChange: (role: RPCRole.AsObject) => (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 !== RPCRole.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';
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { moleculeStories } from '../molecules.story';
|
||||||
|
import { PreauthGreeting } from './PreauthGreeting';
|
||||||
|
import { guild } from 'hack/fixtures/storyData';
|
||||||
|
|
||||||
|
const story = moleculeStories('Preauth', module);
|
||||||
|
|
||||||
|
story.add('Greeting', () => <PreauthGreeting guildSlug={guild} />);
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Avatar, utils as avatarUtils } from 'atoms/avatar';
|
||||||
|
import { Guild } from '@roleypoly/rpc/shared';
|
||||||
|
import { AccentTitle } from 'atoms/typography';
|
||||||
|
import { Space } from 'atoms/space';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
type GreetingProps = {
|
||||||
|
guildSlug: Guild.AsObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Center = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
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';
|
|
@ -0,0 +1,37 @@
|
||||||
|
jest.unmock('atoms/text-input');
|
||||||
|
jest.unmock('./PreauthSecretCode');
|
||||||
|
|
||||||
|
import { Button } from 'atoms/button';
|
||||||
|
import { TextInputWithIcon } from 'atoms/text-input';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { PreauthSecretCode } from './PreauthSecretCode';
|
||||||
|
import { FaderOpacity } from '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,8 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { moleculeStories } from '../molecules.story';
|
||||||
|
import { PreauthSecretCode } from './PreauthSecretCode';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
const story = moleculeStories('Preauth', module);
|
||||||
|
|
||||||
|
story.add('Secret Code', () => <PreauthSecretCode onSubmit={action('onSubmit')} />);
|
|
@ -0,0 +1,46 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { TextInputWithIcon } from 'atoms/text-input';
|
||||||
|
import { FiKey } from 'react-icons/fi';
|
||||||
|
import { FaderOpacity } from 'atoms/fader';
|
||||||
|
import { Button } from 'atoms/button';
|
||||||
|
import { Space } from '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';
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Button } from '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 { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { ResetSubmit } from './ResetSubmit';
|
||||||
|
const story = moleculeStories('Reset & Submit', module);
|
||||||
|
|
||||||
|
story.add('Reset & Submit', () => (
|
||||||
|
<ResetSubmit onSubmit={action('onSubmit')} onReset={action('onReset')} />
|
||||||
|
));
|
|
@ -0,0 +1,19 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { onSmallScreen } from '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 'atoms/breakpoints';
|
||||||
|
import { Button } from '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';
|
|
@ -0,0 +1,19 @@
|
||||||
|
jest.unmock('./ServerMasthead');
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { ServerMasthead } from './ServerMasthead';
|
||||||
|
import { guild } from 'hack/fixtures/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,9 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import { ServerMasthead } from './ServerMasthead';
|
||||||
|
import { guild } from 'hack/fixtures/storyData';
|
||||||
|
|
||||||
|
const story = moleculeStories('Server Masthead', module);
|
||||||
|
|
||||||
|
story.add('Default', () => <ServerMasthead guild={guild} editable={false} />);
|
||||||
|
story.add('Editable', () => <ServerMasthead guild={guild} editable={true} />);
|
|
@ -0,0 +1,36 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { palette } from 'atoms/colors';
|
||||||
|
import { transitions } from 'atoms/timings';
|
||||||
|
|
||||||
|
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/rpc/shared';
|
||||||
|
import { Avatar, utils } from 'atoms/avatar';
|
||||||
|
import { AccentTitle, AmbientLarge } from 'atoms/typography';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { guild } from 'hack/fixtures/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.AsObject;
|
||||||
|
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';
|
|
@ -0,0 +1,13 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import { UserAvatarGroup } from './UserAvatarGroup';
|
||||||
|
import { user } from 'hack/fixtures/storyData';
|
||||||
|
import { Hero } from 'atoms/hero';
|
||||||
|
|
||||||
|
const story = moleculeStories('User Avatar Group', module);
|
||||||
|
|
||||||
|
story.add('Default', () => (
|
||||||
|
<Hero>
|
||||||
|
<UserAvatarGroup user={user} />
|
||||||
|
</Hero>
|
||||||
|
));
|
|
@ -0,0 +1,29 @@
|
||||||
|
import styled, { css } from 'styled-components';
|
||||||
|
import { onSmallScreen } from 'atoms/breakpoints';
|
||||||
|
import { palette } from 'atoms/colors';
|
||||||
|
|
||||||
|
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/rpc/shared';
|
||||||
|
import { utils, Avatar } from 'atoms/avatar';
|
||||||
|
import { Group, Collapse, Discriminator, GroupText } from './UserAvatarGroup.styled';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: DiscordUser.AsObject;
|
||||||
|
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';
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { user } from 'hack/fixtures/storyData';
|
||||||
|
import { moleculeStories } from 'molecules/molecules.story';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { UserPopover } from './UserPopover';
|
||||||
|
import { PopoverBase } from 'atoms/popover/Popover.styled';
|
||||||
|
|
||||||
|
const story = moleculeStories('User Popover', module);
|
||||||
|
|
||||||
|
story.add('User Popover', () => (
|
||||||
|
<PopoverBase active>
|
||||||
|
<UserPopover user={user} />
|
||||||
|
</PopoverBase>
|
||||||
|
));
|
|
@ -0,0 +1,33 @@
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { palette } from 'atoms/colors';
|
||||||
|
import { transitions } from 'atoms/timings';
|
||||||
|
|
||||||
|
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/rpc/shared';
|
||||||
|
import { UserAvatarGroup } from '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.AsObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 'organisms/organisms.story';
|
||||||
|
import { AppShell } from './AppShell';
|
||||||
|
import { rpUser, guildEnum } from 'hack/fixtures/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 '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 'organisms/masthead';
|
||||||
|
import { RoleypolyUser } from '@roleypoly/rpc/shared';
|
||||||
|
import { Footer } from 'molecules/footer';
|
||||||
|
import { Content, GlobalStyles } from './AppShell.styled';
|
||||||
|
import { GlobalStyleColors } from '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 '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 'atoms/breakpoints';
|
||||||
|
import { palette } from 'atoms/colors';
|
||||||
|
import { text300, text500, text700 } from '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 'organisms/organisms.story';
|
||||||
|
import { HelpPageBase } from './HelpPageBase';
|
||||||
|
import { Content } from '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 '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 'organisms/organisms.story';
|
||||||
|
import { HelpStoryWrapper } from '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 '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())}
|
||||||
|
`;
|
83
src/design-system/organisms/help-why-no-roles/WhyNoRoles.tsx
Normal file
83
src/design-system/organisms/help-why-no-roles/WhyNoRoles.tsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { HalfsiesContainer, HalfsiesItem } from 'atoms/halfsies';
|
||||||
|
import { FaCheck, FaTimes } from 'react-icons/fa';
|
||||||
|
import { DiscordBase, DiscordRole } from './WhyNoRoles.styled';
|
||||||
|
import { demoData } from 'hack/fixtures/demoData';
|
||||||
|
import { Role } from '@roleypoly/rpc/shared';
|
||||||
|
import { palette } from 'atoms/colors';
|
||||||
|
import chroma from 'chroma-js';
|
||||||
|
import { SparkleOverlay } from '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 '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 'atoms/breakpoints';
|
||||||
|
import { text400 } from '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;
|
||||||
|
`;
|
38
src/design-system/organisms/landing/Landing.tsx
Normal file
38
src/design-system/organisms/landing/Landing.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { palette } from 'atoms/colors';
|
||||||
|
import { Space } from 'atoms/space';
|
||||||
|
import { LargeText, LargeTitle } from 'atoms/typography';
|
||||||
|
import { DemoDiscord } from 'molecules/demo-discord';
|
||||||
|
import { DemoPicker } from 'molecules/demo-picker';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { DemoAlignment, DemoSubtitle, HeroCentering, HeroText } from './Landing.styled';
|
||||||
|
import { HalfsiesContainer, HalfsiesItem } from '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 'atoms/branding';
|
||||||
|
import { Popover } from 'atoms/popover';
|
||||||
|
import { guildEnum } from 'hack/fixtures/storyData';
|
||||||
|
import { GuildNav } from 'molecules/guild-nav';
|
||||||
|
import { NavSlug } from 'molecules/nav-slug';
|
||||||
|
import { UserAvatarGroup } from 'molecules/user-avatar-group';
|
||||||
|
import { UserPopover } from '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 'atoms/branding';
|
||||||
|
import { Button } from '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>
|
||||||
|
);
|
17
src/design-system/organisms/masthead/Masthead.story.tsx
Normal file
17
src/design-system/organisms/masthead/Masthead.story.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { rpUser, guild, guildEnum } from 'hack/fixtures/storyData';
|
||||||
|
import { organismStories } from '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 'atoms/breakpoints';
|
||||||
|
import { palette } from 'atoms/colors';
|
||||||
|
import { transitions } from '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';
|
2
src/design-system/organisms/organisms.story.tsx
Normal file
2
src/design-system/organisms/organisms.story.tsx
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import { makeFactory } from '../.storybook/storyHelper';
|
||||||
|
export const organismStories = makeFactory('Organisms');
|
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 'organisms/organisms.story';
|
||||||
|
import { guild } from 'hack/fixtures/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 'atoms/button';
|
||||||
|
import { Space } from 'atoms/space';
|
||||||
|
import { PreauthGreeting } from 'molecules/preauth-greeting';
|
||||||
|
import { PreauthSecretCode } from '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 'atoms/role';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import {
|
||||||
|
guild,
|
||||||
|
guildData,
|
||||||
|
guildRoles,
|
||||||
|
member,
|
||||||
|
mockCategorySingle,
|
||||||
|
} from 'hack/fixtures/storyData';
|
||||||
|
import { ResetSubmit } from 'molecules/reset-submit';
|
||||||
|
import { PickerCategory } from '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]]);
|
||||||
|
});
|
45
src/design-system/organisms/role-picker/RolePicker.story.tsx
Normal file
45
src/design-system/organisms/role-picker/RolePicker.story.tsx
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { RolePicker, RolePickerProps } from './RolePicker';
|
||||||
|
import { organismStories } from 'organisms/organisms.story';
|
||||||
|
import { guildData, member, guildRoles, guild } from 'hack/fixtures/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 '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 'atoms/fader';
|
||||||
|
import { Space } from 'atoms/space';
|
||||||
|
import { ResetSubmit } from 'molecules/reset-submit';
|
||||||
|
import { ServerMasthead } from 'molecules/server-masthead';
|
||||||
|
import { PickerCategory } from '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';
|
18
src/design-system/templates/auth-login/AuthLogin.story.tsx
Normal file
18
src/design-system/templates/auth-login/AuthLogin.story.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { templateStories } from 'templates/templates.story';
|
||||||
|
import { AuthLogin } from './AuthLogin';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { guild } from 'hack/fixtures/storyData';
|
||||||
|
|
||||||
|
const story = templateStories('Login', module);
|
||||||
|
|
||||||
|
story.add('No Slug', () => (
|
||||||
|
<AuthLogin botName="roleypoly#3266" onSendSecretCode={action('secret code!')} />
|
||||||
|
));
|
||||||
|
story.add('With Slug', () => (
|
||||||
|
<AuthLogin
|
||||||
|
botName="roleypoly#3266"
|
||||||
|
guildSlug={guild}
|
||||||
|
onSendSecretCode={action('secret code!')}
|
||||||
|
/>
|
||||||
|
));
|
14
src/design-system/templates/auth-login/AuthLogin.tsx
Normal file
14
src/design-system/templates/auth-login/AuthLogin.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { Hero } from 'atoms/hero';
|
||||||
|
import { AppShell } from 'organisms/app-shell';
|
||||||
|
import { Preauth, PreauthProps } from 'organisms/preauth/Preauth';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export type AuthLoginProps = PreauthProps;
|
||||||
|
|
||||||
|
export const AuthLogin = (props: AuthLoginProps) => (
|
||||||
|
<AppShell showFooter user={null}>
|
||||||
|
<Hero topSpacing={100} bottomSpacing={175}>
|
||||||
|
<Preauth {...props} />
|
||||||
|
</Hero>
|
||||||
|
</AppShell>
|
||||||
|
);
|
1
src/design-system/templates/auth-login/index.ts
Normal file
1
src/design-system/templates/auth-login/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './AuthLogin';
|
10
src/design-system/templates/errors/Errors.story.tsx
Normal file
10
src/design-system/templates/errors/Errors.story.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { templateStories } from 'templates/templates.story';
|
||||||
|
import { Error } from './Errors';
|
||||||
|
import { errorMessages } from './errorStrings';
|
||||||
|
|
||||||
|
const messages = templateStories('Errors/Messages', module);
|
||||||
|
|
||||||
|
for (let message in errorMessages) {
|
||||||
|
messages.add(`${message}`, () => <Error code={message} />);
|
||||||
|
}
|
26
src/design-system/templates/errors/Errors.tsx
Normal file
26
src/design-system/templates/errors/Errors.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { DotOverlay } from 'atoms/dot-overlay';
|
||||||
|
import { Hero } from 'atoms/hero';
|
||||||
|
import { AppShell } from 'organisms/app-shell';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ErrorMessage, getMessageFromCode } from './errorStrings';
|
||||||
|
import { ErrorBanner } from 'organisms/error-banner';
|
||||||
|
import { RoleypolyUser } from '@roleypoly/rpc/shared';
|
||||||
|
|
||||||
|
export type ErrorProps = {
|
||||||
|
code: string | number;
|
||||||
|
messageOverride?: ErrorMessage;
|
||||||
|
user?: RoleypolyUser.AsObject | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Error = (props: ErrorProps) => {
|
||||||
|
const messageFromCode = getMessageFromCode(props.code);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppShell user={props.user || null}>
|
||||||
|
<DotOverlay />
|
||||||
|
<Hero topSpacing={100} bottomSpacing={25}>
|
||||||
|
<ErrorBanner message={messageFromCode} />
|
||||||
|
</Hero>
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
};
|
66
src/design-system/templates/errors/errorStrings.ts
Normal file
66
src/design-system/templates/errors/errorStrings.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
export type ErrorMessage = {
|
||||||
|
english: string;
|
||||||
|
japanese?: string;
|
||||||
|
friendlyCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultMessage: Required<ErrorMessage> = {
|
||||||
|
english: `Something went bad. How could this happen?`,
|
||||||
|
japanese: `わかりません...`,
|
||||||
|
friendlyCode: 'Oops.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const errorMessages: { [code: string]: ErrorMessage } = {
|
||||||
|
default: defaultMessage,
|
||||||
|
'400': {
|
||||||
|
english: 'Your client sent me something weird...',
|
||||||
|
japanese: '((((;゜Д゜)))',
|
||||||
|
},
|
||||||
|
'403': {
|
||||||
|
english: `You weren't allowed to access this.`,
|
||||||
|
japanese: 'あなたはこの点に合格しないかもしれません',
|
||||||
|
},
|
||||||
|
'404': {
|
||||||
|
english: `This page is in another castle.`,
|
||||||
|
japanese: 'お探しのページは見つかりませんでした',
|
||||||
|
},
|
||||||
|
'419': {
|
||||||
|
english: 'Something went too slowly...',
|
||||||
|
japanese: 'おやすみなさい〜',
|
||||||
|
},
|
||||||
|
'500': {
|
||||||
|
english: `The server doesn't like you right now. Feed it a cookie.`,
|
||||||
|
japanese: 'クッキーを送ってください〜 クッキーを送ってください〜',
|
||||||
|
},
|
||||||
|
serverFailure: {
|
||||||
|
english: `Server is super unhappy with you today...`,
|
||||||
|
japanese: 'クッキーを送ってください〜',
|
||||||
|
friendlyCode: `Oh no!`,
|
||||||
|
},
|
||||||
|
magicExpired: {
|
||||||
|
english: 'That magic login link was expired.',
|
||||||
|
friendlyCode: 'Woah.',
|
||||||
|
},
|
||||||
|
authFailure: {
|
||||||
|
english: `I tried to tell the server who you were...`,
|
||||||
|
japanese: `...but it didn't believe me. :( ごめんなさい`,
|
||||||
|
friendlyCode: 'Yo.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMessageFromCode = (
|
||||||
|
code: keyof typeof errorMessages
|
||||||
|
): Required<ErrorMessage> => {
|
||||||
|
const codeStr = String(code);
|
||||||
|
const baseMessage = errorMessages[codeStr];
|
||||||
|
|
||||||
|
const message: Required<ErrorMessage> = {
|
||||||
|
english: baseMessage?.english || defaultMessage.english,
|
||||||
|
japanese: baseMessage?.japanese || defaultMessage.japanese,
|
||||||
|
friendlyCode: baseMessage
|
||||||
|
? baseMessage?.friendlyCode || codeStr
|
||||||
|
: defaultMessage.friendlyCode,
|
||||||
|
};
|
||||||
|
|
||||||
|
return message;
|
||||||
|
};
|
1
src/design-system/templates/errors/index.ts
Normal file
1
src/design-system/templates/errors/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './Errors';
|
12
src/design-system/templates/help-page/HelpPage.story.tsx
Normal file
12
src/design-system/templates/help-page/HelpPage.story.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { templateStories } from 'templates/templates.story';
|
||||||
|
import { HelpPageTemplate } from './HelpPage';
|
||||||
|
|
||||||
|
const story = templateStories('Help Page', module);
|
||||||
|
|
||||||
|
story.add('Base', () => (
|
||||||
|
<HelpPageTemplate user={null}>
|
||||||
|
<h1>What is the world but vibrations?</h1>
|
||||||
|
<p>Vibrations that synchronize and tie it together, running free forever.</p>
|
||||||
|
</HelpPageTemplate>
|
||||||
|
));
|
15
src/design-system/templates/help-page/HelpPage.tsx
Normal file
15
src/design-system/templates/help-page/HelpPage.tsx
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { AppShell } from 'organisms/app-shell';
|
||||||
|
import { HelpPageBase } from 'organisms/help-page-base';
|
||||||
|
import { RoleypolyUser } from '@roleypoly/rpc/shared';
|
||||||
|
|
||||||
|
type HelpPageProps = {
|
||||||
|
user: RoleypolyUser.AsObject | null;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HelpPageTemplate = (props: HelpPageProps) => (
|
||||||
|
<AppShell user={props.user || null}>
|
||||||
|
<HelpPageBase>{props.children}</HelpPageBase>
|
||||||
|
</AppShell>
|
||||||
|
);
|
7
src/design-system/templates/landing/Landing.story.tsx
Normal file
7
src/design-system/templates/landing/Landing.story.tsx
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { templateStories } from 'templates/templates.story';
|
||||||
|
import { LandingTemplate } from './Landing';
|
||||||
|
|
||||||
|
const story = templateStories('Landing', module);
|
||||||
|
|
||||||
|
story.add('Landing', () => <LandingTemplate />);
|
9
src/design-system/templates/landing/Landing.tsx
Normal file
9
src/design-system/templates/landing/Landing.tsx
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { AppShell } from 'organisms/app-shell';
|
||||||
|
import { Landing } from 'organisms/landing';
|
||||||
|
|
||||||
|
export const LandingTemplate = () => (
|
||||||
|
<AppShell showFooter user={null}>
|
||||||
|
<Landing />
|
||||||
|
</AppShell>
|
||||||
|
);
|
1
src/design-system/templates/landing/index.ts
Normal file
1
src/design-system/templates/landing/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './Landing';
|
35
src/design-system/templates/role-picker/RolePicker.story.tsx
Normal file
35
src/design-system/templates/role-picker/RolePicker.story.tsx
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { templateStories } from 'templates/templates.story';
|
||||||
|
import { RolePickerTemplate, RolePickerTemplateProps } from './RolePicker';
|
||||||
|
import {
|
||||||
|
guildData,
|
||||||
|
member,
|
||||||
|
guildRoles,
|
||||||
|
guild,
|
||||||
|
rpUser,
|
||||||
|
guildEnum,
|
||||||
|
} from 'hack/fixtures/storyData';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { boolean } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
|
const story = templateStories('Role Picker', module);
|
||||||
|
|
||||||
|
const props: RolePickerTemplateProps = {
|
||||||
|
guildData: {
|
||||||
|
...guildData,
|
||||||
|
message:
|
||||||
|
'Hey, this is kind of a demo setup so features/use cases can be shown off.\n\nThanks for using Roleypoly <3',
|
||||||
|
},
|
||||||
|
member: member,
|
||||||
|
guild: guild,
|
||||||
|
roles: guildRoles,
|
||||||
|
onSubmit: action('onSubmit'),
|
||||||
|
editable: false,
|
||||||
|
user: rpUser,
|
||||||
|
guildEnumeration: guildEnum,
|
||||||
|
activeGuildId: guild.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
story.add('Role Picker', () => {
|
||||||
|
return <RolePickerTemplate {...props} editable={boolean('Editable', false)} />;
|
||||||
|
});
|
25
src/design-system/templates/role-picker/RolePicker.tsx
Normal file
25
src/design-system/templates/role-picker/RolePicker.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { AppShell } from 'organisms/app-shell';
|
||||||
|
import { RolePicker, RolePickerProps } from 'organisms/role-picker';
|
||||||
|
import { RoleypolyUser } from '@roleypoly/rpc/shared';
|
||||||
|
import { GuildEnumeration } from '@roleypoly/rpc/platform';
|
||||||
|
|
||||||
|
export type RolePickerTemplateProps = RolePickerProps & {
|
||||||
|
user: RoleypolyUser.AsObject;
|
||||||
|
guildEnumeration?: GuildEnumeration.AsObject;
|
||||||
|
activeGuildId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RolePickerTemplate = (props: RolePickerTemplateProps) => {
|
||||||
|
const { user, ...pickerProps } = props;
|
||||||
|
return (
|
||||||
|
<AppShell
|
||||||
|
guildEnumeration={props.guildEnumeration}
|
||||||
|
activeGuildId={props.activeGuildId}
|
||||||
|
user={user}
|
||||||
|
small
|
||||||
|
>
|
||||||
|
<RolePicker {...pickerProps} />
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
};
|
1
src/design-system/templates/role-picker/index.ts
Normal file
1
src/design-system/templates/role-picker/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './RolePicker';
|
Loading…
Add table
Reference in a new issue