mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-07-01 16:26:57 +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
|
@ -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';
|
Loading…
Add table
Add a link
Reference in a new issue