mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 11:29:12 +00:00
Feat/editor category pass2 (#290)
* feat(design-system): add editor skeletons * use css media queries rather than JS media queries * init remake * feat: add basis of toggle atom * finish toggle * use pointer cursor with toggle * sync * feat: add server message in editor * cleanup storybook * add short editor item and data model for categories * chore: fix build by moving jest version downward * chore: remove old category editor * chore: fix EditorCategoryShort index * add editor wiring and styling updates * fix linting issues
This commit is contained in:
parent
a37d481b18
commit
7d681d69d6
48 changed files with 927 additions and 1100 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -10,5 +10,6 @@
|
||||||
"**/.yarn": true,
|
"**/.yarn": true,
|
||||||
"**/.pnp.*": true
|
"**/.pnp.*": true
|
||||||
},
|
},
|
||||||
"typescript.enablePromptUseWorkspaceTsdk": true
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
|
"typescript.preferences.importModuleSpecifier": "non-relative"
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@
|
||||||
"enzyme-adapter-react-16": "^1.15.6",
|
"enzyme-adapter-react-16": "^1.15.6",
|
||||||
"husky": "^6.0.0",
|
"husky": "^6.0.0",
|
||||||
"is-ci": "^3.0.0",
|
"is-ci": "^3.0.0",
|
||||||
"jest": "^27.0.6",
|
"jest": "26.6.0",
|
||||||
"jest-enzyme": "^7.1.2",
|
"jest-enzyme": "^7.1.2",
|
||||||
"jest-react-hooks-shallow": "^1.5.1",
|
"jest-react-hooks-shallow": "^1.5.1",
|
||||||
"jest-styled-components": "^7.0.4",
|
"jest-styled-components": "^7.0.4",
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
"stylelint-config-standard": "^22.0.0",
|
"stylelint-config-standard": "^22.0.0",
|
||||||
"stylelint-config-styled-components": "^0.1.1",
|
"stylelint-config-styled-components": "^0.1.1",
|
||||||
"stylelint-prettier": "^1.2.0",
|
"stylelint-prettier": "^1.2.0",
|
||||||
"ts-jest": "^27.0.3",
|
"ts-jest": "^26.0.0",
|
||||||
"typescript": "^4.3.4"
|
"typescript": "^4.3.4"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
39
packages/api/handlers/update-guild.ts
Normal file
39
packages/api/handlers/update-guild.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { GuildDataUpdate, SessionData, UserGuildPermissions } from '@roleypoly/types';
|
||||||
|
import { withSession } from '../utils/api-tools';
|
||||||
|
import { getGuildData } from '../utils/guild';
|
||||||
|
import { GuildData } from '../utils/kv';
|
||||||
|
import { lowPermissions, missingParameters, notFound, ok } from '../utils/responses';
|
||||||
|
|
||||||
|
export const UpdateGuild = withSession(
|
||||||
|
(session: SessionData) =>
|
||||||
|
async (request: Request): Promise<Response> => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const [, , guildID] = url.pathname.split('/');
|
||||||
|
if (!guildID) {
|
||||||
|
return missingParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildUpdate = (await request.json()) as GuildDataUpdate;
|
||||||
|
|
||||||
|
const guild = session.guilds.find((guild) => guild.id === guildID);
|
||||||
|
if (!guild) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
guild?.permissionLevel !== UserGuildPermissions.Manager &&
|
||||||
|
guild?.permissionLevel !== UserGuildPermissions.Admin
|
||||||
|
) {
|
||||||
|
return lowPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGuildData = {
|
||||||
|
...(await getGuildData(guildID)),
|
||||||
|
...guildUpdate,
|
||||||
|
};
|
||||||
|
|
||||||
|
await GuildData.put(guildID, newGuildData);
|
||||||
|
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
|
);
|
|
@ -15,6 +15,6 @@
|
||||||
"level": "^7.0.0",
|
"level": "^7.0.0",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"webpack": "^4.x"
|
"webpack": "4.44.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
stories: ['../**/*.stories.mdx', '../**/*.stories.@(js|jsx|ts|tsx)'],
|
stories: [
|
||||||
|
'../*.stories.mdx',
|
||||||
|
...['atoms', 'molecules', 'organisms', 'templates'].map(
|
||||||
|
(dir) => `../${dir}/**/*.stories.@(tsx|mdx)`
|
||||||
|
),
|
||||||
|
],
|
||||||
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
|
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
|
||||||
|
reactOptions: {
|
||||||
|
fastRefresh: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
};
|
|
||||||
const Link = (props: Props) => <>{props.children}</>;
|
|
||||||
|
|
||||||
export default Link;
|
|
|
@ -31,6 +31,7 @@ export const BreakpointsProvider = (props: { children: React.ReactNode }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
updateScreenSize();
|
updateScreenSize();
|
||||||
|
setImmediate(() => updateScreenSize());
|
||||||
|
|
||||||
mediaQueries.onDesktop.addEventListener('change', updateScreenSize);
|
mediaQueries.onDesktop.addEventListener('change', updateScreenSize);
|
||||||
mediaQueries.onTablet.addEventListener('change', updateScreenSize);
|
mediaQueries.onTablet.addEventListener('change', updateScreenSize);
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import { onTablet } from '@roleypoly/design-system/atoms/breakpoints';
|
import { onTablet } from '@roleypoly/design-system/atoms/breakpoints';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
export const HalfsiesContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const HalfsiesItem = styled.div`
|
export const HalfsiesItem = styled.div`
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
|
@ -15,3 +8,16 @@ export const HalfsiesItem = styled.div`
|
||||||
flex: 1 2 50%;
|
flex: 1 2 50%;
|
||||||
`)}
|
`)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const HalfsiesContainer = styled.div<{ center?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
${({ center }) =>
|
||||||
|
center &&
|
||||||
|
css`
|
||||||
|
align-content: center;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
|
import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
|
||||||
|
import { palette } from '@roleypoly/design-system/atoms/colors';
|
||||||
|
import { transitions } from '@roleypoly/design-system/atoms/timings';
|
||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import { palette } from '../colors';
|
|
||||||
import { transitions } from '../timings';
|
export const HideIfSmall = styled.div`
|
||||||
|
display: initial;
|
||||||
|
|
||||||
|
${onSmallScreen(css`
|
||||||
|
display: none;
|
||||||
|
`)}
|
||||||
|
`;
|
||||||
|
export const HideIfNotSmall = styled.div`
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
${onSmallScreen(css`
|
||||||
|
display: initial;
|
||||||
|
`)}
|
||||||
|
`;
|
||||||
|
|
||||||
export const NavItem = styled.div<{ selected: boolean }>`
|
export const NavItem = styled.div<{ selected: boolean }>`
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
|
@ -35,6 +51,7 @@ export const DropdownNavOpener = styled.div`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: background-color ${transitions.actionable}s ease-in-out;
|
transition: background-color ${transitions.actionable}s ease-in-out;
|
||||||
|
width: 98vw;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${palette.taupe300};
|
background-color: ${palette.taupe300};
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// thing should be everything visible on desktop/tablet and a popover when small
|
// thing should be everything visible on desktop/tablet and a popover when small
|
||||||
|
|
||||||
import { useBreakpointContext } from '@roleypoly/design-system/atoms/breakpoints';
|
|
||||||
import { Popover } from '@roleypoly/design-system/atoms/popover';
|
import { Popover } from '@roleypoly/design-system/atoms/popover';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { GoChevronDown } from 'react-icons/go';
|
import { GoChevronDown } from 'react-icons/go';
|
||||||
|
@ -8,6 +7,8 @@ import {
|
||||||
DropdownNavCurrent,
|
DropdownNavCurrent,
|
||||||
DropdownNavIcon,
|
DropdownNavIcon,
|
||||||
DropdownNavOpener,
|
DropdownNavOpener,
|
||||||
|
HideIfNotSmall,
|
||||||
|
HideIfSmall,
|
||||||
NavItem,
|
NavItem,
|
||||||
} from './QuickNav.styled';
|
} from './QuickNav.styled';
|
||||||
|
|
||||||
|
@ -18,18 +19,17 @@ export type QuickNavProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QuickNav = (props: QuickNavProps) => {
|
export const QuickNav = (props: QuickNavProps) => {
|
||||||
const breakpoints = useBreakpointContext();
|
return (
|
||||||
|
<>
|
||||||
if (breakpoints.screenSize.onSmallScreen) {
|
<QuickNavExpanded {...props} />
|
||||||
return <QuickNavCollapsed {...props} />;
|
<QuickNavCollapsed {...props} />
|
||||||
}
|
</>
|
||||||
|
);
|
||||||
return <QuickNavExpanded {...props} />;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const QuickNavExpanded = (props: QuickNavProps) => {
|
export const QuickNavExpanded = (props: QuickNavProps) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<HideIfSmall>
|
||||||
{props.navItems.map((navItem) => (
|
{props.navItems.map((navItem) => (
|
||||||
<NavItem
|
<NavItem
|
||||||
onClick={() => props.onNavChange?.(navItem)}
|
onClick={() => props.onNavChange?.(navItem)}
|
||||||
|
@ -39,7 +39,7 @@ export const QuickNavExpanded = (props: QuickNavProps) => {
|
||||||
{navItem}
|
{navItem}
|
||||||
</NavItem>
|
</NavItem>
|
||||||
))}
|
))}
|
||||||
</div>
|
</HideIfSmall>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ export const QuickNavCollapsed = (props: QuickNavProps) => {
|
||||||
const [popoverState, setPopoverState] = useState(false);
|
const [popoverState, setPopoverState] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<HideIfNotSmall>
|
||||||
{popoverState ? (
|
{popoverState ? (
|
||||||
<Popover
|
<Popover
|
||||||
headContent={<>Server Editor</>}
|
headContent={<>Server Editor</>}
|
||||||
|
@ -80,6 +80,6 @@ export const QuickNavCollapsed = (props: QuickNavProps) => {
|
||||||
<DropdownNavCurrent>{props.currentNavItem}</DropdownNavCurrent>
|
<DropdownNavCurrent>{props.currentNavItem}</DropdownNavCurrent>
|
||||||
</DropdownNavOpener>
|
</DropdownNavOpener>
|
||||||
)}
|
)}
|
||||||
</div>
|
</HideIfNotSmall>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -22,8 +22,9 @@ export const TabTitleRow = styled.div`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
${onSmallScreen(
|
${onSmallScreen(
|
||||||
css`
|
css`
|
||||||
|
width: fit-content;
|
||||||
position: unset;
|
position: unset;
|
||||||
max-width: 100vw;
|
max-width: 98vw;
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
`;
|
`;
|
||||||
|
@ -65,5 +66,9 @@ export const TabContentTitle = styled.div`
|
||||||
${text500}
|
${text500}
|
||||||
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
padding: 10px;
|
padding: 0.5em 7px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TabDepth = styled.div`
|
||||||
|
margin-left: 7px;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
export type TabViewProps = {
|
export type TabViewProps = {
|
||||||
children: React.ReactNode[];
|
children: React.ReactNode[];
|
||||||
initialTab?: number;
|
initialTab?: number;
|
||||||
|
masthead?: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TabProps = {
|
type TabProps = {
|
||||||
|
@ -39,6 +40,7 @@ export const TabView = (props: TabViewProps) => {
|
||||||
return (
|
return (
|
||||||
<TabViewStyled>
|
<TabViewStyled>
|
||||||
<TabTitleRow>
|
<TabTitleRow>
|
||||||
|
{props.masthead && props.masthead}
|
||||||
<QuickNav
|
<QuickNav
|
||||||
currentNavItem={tabNames[currentTab]}
|
currentNavItem={tabNames[currentTab]}
|
||||||
navItems={tabNames}
|
navItems={tabNames}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { palette } from '@roleypoly/design-system/atoms/colors';
|
import { palette } from '@roleypoly/design-system/atoms/colors';
|
||||||
|
import { fontCSS } from '@roleypoly/design-system/atoms/fonts';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import styled from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
const StyledTextInput = styled.input`
|
const common = css`
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: 1px solid ${palette.taupe200};
|
border: 1px solid ${palette.taupe200};
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -39,11 +40,14 @@ const StyledTextInput = styled.input`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type TextInputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
const StyledTextInput = styled.input`
|
||||||
_override?: React.Component;
|
${common};
|
||||||
};
|
`;
|
||||||
|
|
||||||
export const TextInput = (props: TextInputProps) => {
|
type TextInputProps<T extends HTMLInputElement | HTMLTextAreaElement> =
|
||||||
|
React.InputHTMLAttributes<T>;
|
||||||
|
|
||||||
|
export const TextInput = (props: TextInputProps<HTMLInputElement>) => {
|
||||||
const { ...rest } = props;
|
const { ...rest } = props;
|
||||||
return <StyledTextInput {...rest} />;
|
return <StyledTextInput {...rest} />;
|
||||||
};
|
};
|
||||||
|
@ -68,7 +72,7 @@ const IconInputContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type TextInputWithIconProps = TextInputProps & {
|
type TextInputWithIconProps = TextInputProps<HTMLInputElement> & {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,3 +85,27 @@ export const TextInputWithIcon = (props: TextInputWithIconProps) => {
|
||||||
</IconInputContainer>
|
</IconInputContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const StyledTextarea = styled.textarea`
|
||||||
|
${common};
|
||||||
|
${fontCSS};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MultilineTextInput = (
|
||||||
|
props: TextInputProps<HTMLTextAreaElement> & { rows?: number }
|
||||||
|
) => {
|
||||||
|
const { ...rest } = props;
|
||||||
|
const [value, setValue] = React.useState(String(props.value));
|
||||||
|
const rows = Math.min(10, Math.max(props.rows || 2, value.split(/\r?\n/).length));
|
||||||
|
return (
|
||||||
|
<StyledTextarea
|
||||||
|
{...rest}
|
||||||
|
rows={rows}
|
||||||
|
value={value}
|
||||||
|
onChange={(eventData) => {
|
||||||
|
setValue(eventData.target.value);
|
||||||
|
props.onChange?.(eventData);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
23
packages/design-system/atoms/toggle/Toggle.stories.tsx
Normal file
23
packages/design-system/atoms/toggle/Toggle.stories.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Toggle } from './Toggle';
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/Toggle',
|
||||||
|
component: Toggle,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggle = (args) => <Toggle {...args}>Turn a cool thing on</Toggle>;
|
||||||
|
export const interactive = (args) => {
|
||||||
|
const [state, setState] = React.useState(true);
|
||||||
|
return (
|
||||||
|
<Toggle
|
||||||
|
{...args}
|
||||||
|
state={state}
|
||||||
|
onChange={(val) => {
|
||||||
|
setState(val);
|
||||||
|
args.onChange(val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Turn a cool thing on
|
||||||
|
</Toggle>
|
||||||
|
);
|
||||||
|
};
|
40
packages/design-system/atoms/toggle/Toggle.styled.tsx
Normal file
40
packages/design-system/atoms/toggle/Toggle.styled.tsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { palette } from '@roleypoly/design-system/atoms/colors';
|
||||||
|
import styled, { css } from 'styled-components';
|
||||||
|
import { transitions } from '../timings';
|
||||||
|
|
||||||
|
export const ToggleState = styled.div`
|
||||||
|
height: 1em;
|
||||||
|
width: 1em;
|
||||||
|
border-radius: 1em;
|
||||||
|
background-color: ${palette.grey600};
|
||||||
|
position: absolute;
|
||||||
|
top: 0.15em;
|
||||||
|
left: 0.15em;
|
||||||
|
transform: translateX(0);
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
transition: transform ${transitions.actionable}s ease-in-out;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ToggleSwitch = styled.div<{ state: boolean }>`
|
||||||
|
display: inline-block;
|
||||||
|
background-color: ${(props) => (props.state ? palette.green200 : 'rgba(0,0,0,0.45)')};
|
||||||
|
height: 1.3em;
|
||||||
|
width: 2.6em;
|
||||||
|
border-radius: 1.3em;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
top: 0.23em;
|
||||||
|
transition: background-color ${transitions.in2in}s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
|
||||||
|
${ToggleState} {
|
||||||
|
${(props) =>
|
||||||
|
props.state === true &&
|
||||||
|
css`
|
||||||
|
transform: translateX(1.3em);
|
||||||
|
`}
|
||||||
|
}
|
||||||
|
`;
|
20
packages/design-system/atoms/toggle/Toggle.tsx
Normal file
20
packages/design-system/atoms/toggle/Toggle.tsx
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { ToggleState, ToggleSwitch } from './Toggle.styled';
|
||||||
|
|
||||||
|
type ToggleProps = {
|
||||||
|
onChange?: (newState: boolean) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
state: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Toggle = (props: ToggleProps) => (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
props.onChange?.(!props.state);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ToggleSwitch state={props.state}>
|
||||||
|
<ToggleState />
|
||||||
|
</ToggleSwitch>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
1
packages/design-system/atoms/toggle/index.ts
Normal file
1
packages/design-system/atoms/toggle/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './Toggle';
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { mockCategory } from '@roleypoly/design-system/fixtures/storyData';
|
||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { EditorCategoryShort } from './EditorCategoryShort';
|
||||||
|
|
||||||
|
it('triggers onOpen when clicked', async () => {
|
||||||
|
const onOpen = jest.fn();
|
||||||
|
const view = render(<EditorCategoryShort category={mockCategory} onOpen={onOpen} />);
|
||||||
|
|
||||||
|
view.getByRole('menuitem')?.click();
|
||||||
|
|
||||||
|
expect(onOpen).toHaveBeenCalled();
|
||||||
|
});
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { mockCategory } from '@roleypoly/design-system/fixtures/storyData';
|
||||||
|
import ReactTooltip from 'react-tooltip';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { EditorCategoryShort } from './EditorCategoryShort';
|
||||||
|
|
||||||
|
const decorator = (story) => (
|
||||||
|
<Wrapper>
|
||||||
|
{story()}
|
||||||
|
<ReactTooltip />
|
||||||
|
</Wrapper>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Molecules/Short Category',
|
||||||
|
component: EditorCategoryShort,
|
||||||
|
args: {
|
||||||
|
category: mockCategory,
|
||||||
|
},
|
||||||
|
decorators: [decorator],
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
box-shadow: 0 0 10px #000;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const shortEditor = (args) => <EditorCategoryShort {...args} />;
|
||||||
|
export const shortEditorHidden = (args) => <EditorCategoryShort {...args} />;
|
||||||
|
shortEditorHidden.args = {
|
||||||
|
category: { ...mockCategory, hidden: true },
|
||||||
|
};
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { transitions } from '@roleypoly/design-system/atoms/timings';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const GrabberBox = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 1em;
|
||||||
|
flex: 0;
|
||||||
|
cursor: grab;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -5px;
|
||||||
|
left: -8px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
|
opacity: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: opacity ${transitions.actionable}s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Opener = styled.div`
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity ${transitions.actionable}s ease-in-out;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
padding: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
${Opener} {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Name = styled.div`
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
margin-right: 1em;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Flags = styled.div``;
|
||||||
|
|
||||||
|
export const Void = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
`;
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Category } from '@roleypoly/types';
|
||||||
|
import { DragEventHandler, MouseEventHandler } from 'react';
|
||||||
|
import { GoEyeClosed, GoGrabber, GoKebabHorizontal } from 'react-icons/go';
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Flags,
|
||||||
|
GrabberBox,
|
||||||
|
Name,
|
||||||
|
Opener,
|
||||||
|
Void,
|
||||||
|
} from './EditorCategoryShort.styled';
|
||||||
|
|
||||||
|
type ShortProps = {
|
||||||
|
category: Category;
|
||||||
|
onDrag?: DragEventHandler;
|
||||||
|
onOpen?: MouseEventHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorCategoryShort = (props: ShortProps) => (
|
||||||
|
<Container onClick={props.onOpen} role="menuitem">
|
||||||
|
<GrabberBox onDrag={props.onDrag} role="button">
|
||||||
|
<GoGrabber />
|
||||||
|
</GrabberBox>
|
||||||
|
<Name>{props.category.name}</Name>
|
||||||
|
<Flags>{props.category.hidden && <GoEyeClosed data-tip="Hidden to Members" />}</Flags>
|
||||||
|
<Void />
|
||||||
|
<Opener role="button">
|
||||||
|
<GoKebabHorizontal />
|
||||||
|
</Opener>
|
||||||
|
</Container>
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './EditorCategoryShort';
|
|
@ -3,4 +3,11 @@ import styled from 'styled-components';
|
||||||
export const RoleContainer = styled.div`
|
export const RoleContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
/* This should be a Role element */
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { FaderOpacity } from '@roleypoly/design-system/atoms/fader';
|
import { FaderOpacity } from '@roleypoly/design-system/atoms/fader';
|
||||||
import { HorizontalSwitch } from '@roleypoly/design-system/atoms/horizontal-switch';
|
|
||||||
import { Popover } from '@roleypoly/design-system/atoms/popover';
|
import { Popover } from '@roleypoly/design-system/atoms/popover';
|
||||||
import { Role } from '@roleypoly/design-system/atoms/role';
|
import { Role } from '@roleypoly/design-system/atoms/role';
|
||||||
import { Space } from '@roleypoly/design-system/atoms/space';
|
import { Space } from '@roleypoly/design-system/atoms/space';
|
||||||
import { TextInput, TextInputWithIcon } from '@roleypoly/design-system/atoms/text-input';
|
import { TextInput, TextInputWithIcon } from '@roleypoly/design-system/atoms/text-input';
|
||||||
|
import { Toggle } from '@roleypoly/design-system/atoms/toggle';
|
||||||
import { Text } from '@roleypoly/design-system/atoms/typography';
|
import { Text } from '@roleypoly/design-system/atoms/typography';
|
||||||
import { RoleSearch } from '@roleypoly/design-system/molecules/role-search';
|
import { RoleSearch } from '@roleypoly/design-system/molecules/role-search';
|
||||||
import { Category, CategoryType, Role as RoleType } from '@roleypoly/types';
|
import { Category, CategoryType, Role as RoleType } from '@roleypoly/types';
|
||||||
|
@ -74,24 +74,22 @@ export const EditorCategory = (props: Props) => {
|
||||||
|
|
||||||
<Space />
|
<Space />
|
||||||
|
|
||||||
<Text>Selection Type</Text>
|
|
||||||
<div>
|
<div>
|
||||||
<HorizontalSwitch
|
<Toggle
|
||||||
items={['Multiple', 'Single']}
|
state={props.category.type === CategoryType.Multi}
|
||||||
value={typeEnumToSwitch(props.category.type)}
|
onChange={onUpdate('type', (x) =>
|
||||||
onChange={onUpdate('type', switchToTypeEnum)}
|
x ? CategoryType.Multi : CategoryType.Single
|
||||||
/>
|
)}
|
||||||
|
>
|
||||||
|
Allow users to pick multiple roles
|
||||||
|
</Toggle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Space />
|
<Space />
|
||||||
|
|
||||||
<Text>Visiblity</Text>
|
|
||||||
<div>
|
<div>
|
||||||
<HorizontalSwitch
|
<Toggle state={props.category.hidden} onChange={onUpdate('hidden')}>
|
||||||
items={['Visible', 'Hidden']}
|
Hide category from users
|
||||||
value={props.category.hidden ? 'Hidden' : 'Visible'}
|
</Toggle>
|
||||||
onChange={onUpdate('hidden', (a) => a === 'Hidden')}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Space />
|
<Space />
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Avatar, utils as avatarUtils } from '@roleypoly/design-system/atoms/avatar';
|
||||||
|
import { Text } from '@roleypoly/design-system/atoms/typography';
|
||||||
|
import { PresentableGuild } from '@roleypoly/types';
|
||||||
|
import styled, { css } from 'styled-components';
|
||||||
|
import { onSmallScreen } from '../../atoms/breakpoints';
|
||||||
|
|
||||||
|
type EditorMastheadProps = {
|
||||||
|
guild: PresentableGuild;
|
||||||
|
onSubmit: () => void;
|
||||||
|
onReset: () => void;
|
||||||
|
showSaveReset: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MastheadContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: start;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
${Text} {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
${onSmallScreen(css`
|
||||||
|
display: none;
|
||||||
|
`)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const EditorMasthead = (props: EditorMastheadProps) => (
|
||||||
|
<MastheadContainer>
|
||||||
|
<Avatar
|
||||||
|
size={34}
|
||||||
|
hash={props.guild.guild.icon}
|
||||||
|
src={avatarUtils.avatarHash(props.guild.id, props.guild.guild.icon, 'icons')}
|
||||||
|
>
|
||||||
|
{avatarUtils.initialsFromName(props.guild.guild.name)}
|
||||||
|
</Avatar>
|
||||||
|
<Text>Server Editor</Text>
|
||||||
|
</MastheadContainer>
|
||||||
|
);
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './EditorMasthead';
|
|
@ -1,9 +1,10 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ResetSubmit } from './ResetSubmit';
|
import { InlineResetSubmit, ResetSubmit } from './ResetSubmit';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: 'Molecules',
|
title: 'Molecules/Reset and Submit',
|
||||||
component: ResetSubmit,
|
component: ResetSubmit,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ResetAndSubmit = (args) => <ResetSubmit {...args} />;
|
export const normal = (args) => <ResetSubmit {...args} />;
|
||||||
|
export const inline = (args) => <InlineResetSubmit {...args} />;
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const Buttons = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Left = styled.div`
|
|
||||||
flex: 0;
|
|
||||||
${onSmallScreen`
|
|
||||||
flex: 1 1 100%;
|
|
||||||
order: 2;
|
|
||||||
`}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Right = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
|
@ -2,7 +2,7 @@ import { onSmallScreen } from '@roleypoly/design-system/atoms/breakpoints';
|
||||||
import { Button } from '@roleypoly/design-system/atoms/button';
|
import { Button } from '@roleypoly/design-system/atoms/button';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { MdRestore } from 'react-icons/md';
|
import { MdRestore } from 'react-icons/md';
|
||||||
import styled from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
|
@ -22,8 +22,14 @@ const Left = styled.div`
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Right = styled.div`
|
const Right = styled.div<{ inline?: boolean }>`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.inline &&
|
||||||
|
css`
|
||||||
|
padding-left: 0.2em;
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ResetSubmit = (props: Props) => {
|
export const ResetSubmit = (props: Props) => {
|
||||||
|
@ -40,3 +46,20 @@ export const ResetSubmit = (props: Props) => {
|
||||||
</Buttons>
|
</Buttons>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const InlineResetSubmit = (props: Props) => {
|
||||||
|
return (
|
||||||
|
<Buttons>
|
||||||
|
<Left>
|
||||||
|
<Button color="muted" size="small" icon={<MdRestore />} onClick={props.onReset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
</Left>
|
||||||
|
<Right inline>
|
||||||
|
<Button onClick={props.onSubmit} size="small">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Right>
|
||||||
|
</Buttons>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { guildEnum } from '@roleypoly/design-system/fixtures/storyData';
|
||||||
|
import { EditorCategoriesTab } from './EditorCategoriesTab';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Organisms/Editor/Categories Tab',
|
||||||
|
component: EditorCategoriesTab,
|
||||||
|
args: {
|
||||||
|
guild: guildEnum.guilds[0],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const categoriesTab = (args) => <EditorCategoriesTab {...args} />;
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { TabDepth } from '@roleypoly/design-system/atoms/tab-view/TabView.styled';
|
||||||
|
import { EditorCategory } from '@roleypoly/design-system/molecules/editor-category';
|
||||||
|
import { EditorCategoryShort } from '@roleypoly/design-system/molecules/editor-category-short/EditorCategoryShort';
|
||||||
|
import { EditorShellProps } from '@roleypoly/design-system/organisms/editor-shell';
|
||||||
|
import { Category } from '@roleypoly/types';
|
||||||
|
import { sortBy } from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { CategoryContainer } from './EditorCategoriesTab.styled';
|
||||||
|
|
||||||
|
export const EditorCategoriesTab = (props: EditorShellProps) => {
|
||||||
|
const [openStates, setOpenStates] = React.useState<Category['id'][]>([]);
|
||||||
|
|
||||||
|
const onCategoryOpen = (id: Category['id']) => () => {
|
||||||
|
setOpenStates([...new Set(openStates).add(id)]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabDepth>
|
||||||
|
{sortBy(props.guild.data.categories, ['position', 'id']).map((category, idx) =>
|
||||||
|
openStates.includes(category.id) ? (
|
||||||
|
<CategoryContainer key={idx}>
|
||||||
|
<EditorCategory
|
||||||
|
category={category}
|
||||||
|
uncategorizedRoles={[]}
|
||||||
|
guildRoles={props.guild.roles}
|
||||||
|
onChange={(category) => props.onCategoryChange?.(category)}
|
||||||
|
/>
|
||||||
|
</CategoryContainer>
|
||||||
|
) : (
|
||||||
|
<EditorCategoryShort
|
||||||
|
key={idx}
|
||||||
|
category={category}
|
||||||
|
onOpen={onCategoryOpen(category.id)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</TabDepth>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './EditorCategoriesTab';
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Space } from '@roleypoly/design-system/atoms/space';
|
||||||
|
import { TabDepth } from '@roleypoly/design-system/atoms/tab-view/TabView.styled';
|
||||||
|
import { MultilineTextInput } from '@roleypoly/design-system/atoms/text-input';
|
||||||
|
import { Text } from '@roleypoly/design-system/atoms/typography';
|
||||||
|
import { EditorShellProps } from '@roleypoly/design-system/organisms/editor-shell';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export const EditorDetailsTab = (props: EditorShellProps) => {
|
||||||
|
return (
|
||||||
|
<TabDepth>
|
||||||
|
<Space />
|
||||||
|
<Text>Server Message</Text>
|
||||||
|
<MultilineTextInput
|
||||||
|
value={props.guild.data.message}
|
||||||
|
onChange={(eventData) => props.onMessageChange?.(eventData.target.value)}
|
||||||
|
/>
|
||||||
|
<Space />
|
||||||
|
</TabDepth>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './EditorDetailsTab';
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { Space } from '@roleypoly/design-system/atoms/space';
|
||||||
|
import { Tab, TabView } from '@roleypoly/design-system/atoms/tab-view';
|
||||||
|
import { EditorMasthead } from '@roleypoly/design-system/molecules/editor-masthead';
|
||||||
|
import { EditorCategoriesTab } from '@roleypoly/design-system/organisms/editor-categories-tab';
|
||||||
|
import { EditorDetailsTab } from '@roleypoly/design-system/organisms/editor-details-tab';
|
||||||
|
import { Category, PresentableGuild } from '@roleypoly/types';
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export type EditorShellProps = {
|
||||||
|
guild: PresentableGuild;
|
||||||
|
onGuildChange?: (guild: PresentableGuild) => void;
|
||||||
|
onCategoryChange?: (category: Category) => void;
|
||||||
|
onMessageChange?: (message: PresentableGuild['data']['message']) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorShell = (props: EditorShellProps) => {
|
||||||
|
const [guild, setGuild] = React.useState<PresentableGuild>(props.guild);
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
setGuild(props.guild);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCategoryChange = (category: Category) => {
|
||||||
|
setGuild((currentGuild) => {
|
||||||
|
const categories = [
|
||||||
|
...currentGuild.data.categories.filter((x) => x.id !== category.id),
|
||||||
|
category,
|
||||||
|
];
|
||||||
|
return { ...currentGuild, data: { ...currentGuild.data, categories } };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMessageChange = (message: PresentableGuild['data']['message']) => {
|
||||||
|
setGuild((currentGuild) => {
|
||||||
|
return { ...currentGuild, data: { ...guild.data, message } };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasChanges = React.useMemo(
|
||||||
|
() => !deepEqual(guild.data, props.guild.data),
|
||||||
|
[guild.data, props.guild.data]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Space />
|
||||||
|
<TabView
|
||||||
|
initialTab={0}
|
||||||
|
masthead={
|
||||||
|
<EditorMasthead
|
||||||
|
guild={guild}
|
||||||
|
onReset={reset}
|
||||||
|
onSubmit={() => props.onGuildChange?.(guild)}
|
||||||
|
showSaveReset={hasChanges}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Tab title="Guild Details">
|
||||||
|
{() => (
|
||||||
|
<EditorDetailsTab
|
||||||
|
{...props}
|
||||||
|
guild={guild}
|
||||||
|
onMessageChange={onMessageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Categories & Roles">
|
||||||
|
{() => (
|
||||||
|
<EditorCategoriesTab
|
||||||
|
{...props}
|
||||||
|
guild={guild}
|
||||||
|
onCategoryChange={onCategoryChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
<Tab title="Utilities">{() => <div>hi2!</div>}</Tab>
|
||||||
|
</TabView>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,30 +0,0 @@
|
||||||
import { Tab, TabView } from '@roleypoly/design-system/atoms/tab-view';
|
|
||||||
import { EditorCategory } from '@roleypoly/design-system/molecules/editor-category';
|
|
||||||
import { PresentableGuild } from '@roleypoly/types';
|
|
||||||
import { CategoryContainer } from './EditorShell.styled';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
guild: PresentableGuild;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const EditorShell = (props: Props) => (
|
|
||||||
<TabView initialTab={0}>
|
|
||||||
<Tab title="Roles">{() => <RolesTab {...props} />}</Tab>
|
|
||||||
<Tab title="Server Details">{() => <div>hi2!</div>}</Tab>
|
|
||||||
</TabView>
|
|
||||||
);
|
|
||||||
|
|
||||||
const RolesTab = (props: Props) => (
|
|
||||||
<div>
|
|
||||||
{props.guild.data.categories.map((category, idx) => (
|
|
||||||
<CategoryContainer key={idx}>
|
|
||||||
<EditorCategory
|
|
||||||
category={category}
|
|
||||||
uncategorizedRoles={[]}
|
|
||||||
guildRoles={props.guild.roles}
|
|
||||||
onChange={(x) => console.log(x)}
|
|
||||||
/>
|
|
||||||
</CategoryContainer>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
|
@ -8,6 +8,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@roleypoly/types": "*",
|
"@roleypoly/types": "*",
|
||||||
"chroma-js": "^2.1.2",
|
"chroma-js": "^2.1.2",
|
||||||
|
"deep-equal": "^2.0.5",
|
||||||
"isomorphic-unfetch": "^3.1.0",
|
"isomorphic-unfetch": "^3.1.0",
|
||||||
"ksuid": "^2.0.0",
|
"ksuid": "^2.0.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
"@storybook/react": "^6.3.2",
|
"@storybook/react": "^6.3.2",
|
||||||
"@storybook/theming": "^6.3.2",
|
"@storybook/theming": "^6.3.2",
|
||||||
"@types/chroma-js": "^2.1.3",
|
"@types/chroma-js": "^2.1.3",
|
||||||
|
"@types/deep-equal": "^1.0.1",
|
||||||
"@types/node": "^15.12.5",
|
"@types/node": "^15.12.5",
|
||||||
"@types/react": "^17.0.11",
|
"@types/react": "^17.0.11",
|
||||||
"@types/react-custom-scrollbars": "^4.0.7",
|
"@types/react-custom-scrollbars": "^4.0.7",
|
||||||
|
|
13
packages/design-system/templates/editor/Editor.stories.tsx
Normal file
13
packages/design-system/templates/editor/Editor.stories.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { guildEnum, mastheadSlugs, user } from '../../fixtures/storyData';
|
||||||
|
import { EditorTemplate } from './Editor';
|
||||||
|
export default {
|
||||||
|
title: 'Templates/Server Editor',
|
||||||
|
component: EditorTemplate,
|
||||||
|
args: {
|
||||||
|
guilds: mastheadSlugs,
|
||||||
|
user: user,
|
||||||
|
guild: guildEnum.guilds[0],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serverEditor = (args) => <EditorTemplate {...args} />;
|
17
packages/design-system/templates/editor/Editor.tsx
Normal file
17
packages/design-system/templates/editor/Editor.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { AppShell, AppShellProps } from '@roleypoly/design-system/organisms/app-shell';
|
||||||
|
import {
|
||||||
|
EditorShell,
|
||||||
|
EditorShellProps,
|
||||||
|
} from '@roleypoly/design-system/organisms/editor-shell';
|
||||||
|
|
||||||
|
export const EditorTemplate = (
|
||||||
|
props: EditorShellProps & Omit<AppShellProps, 'children'>
|
||||||
|
) => {
|
||||||
|
const { guild, onCategoryChange, onMessageChange, onGuildChange, ...appShellProps } =
|
||||||
|
props;
|
||||||
|
return (
|
||||||
|
<AppShell {...appShellProps} activeGuildId={guild.id}>
|
||||||
|
<EditorShell guild={guild} onGuildChange={onGuildChange} />
|
||||||
|
</AppShell>
|
||||||
|
);
|
||||||
|
};
|
1
packages/design-system/templates/editor/index.ts
Normal file
1
packages/design-system/templates/editor/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './Editor';
|
|
@ -22,6 +22,8 @@ export type GuildData = {
|
||||||
features: Features;
|
features: Features;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GuildDataUpdate = Omit<Omit<GuildData, 'features'>, 'id'>;
|
||||||
|
|
||||||
export type PresentableGuild = {
|
export type PresentableGuild = {
|
||||||
id: string;
|
id: string;
|
||||||
guild: GuildSlug;
|
guild: GuildSlug;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import LandingPage from '../pages/landing';
|
||||||
import PickerPage from '../pages/picker';
|
import PickerPage from '../pages/picker';
|
||||||
|
|
||||||
const ServersPage = React.lazy(() => import('../pages/servers'));
|
const ServersPage = React.lazy(() => import('../pages/servers'));
|
||||||
|
const EditorPage = React.lazy(() => import('../pages/editor'));
|
||||||
|
|
||||||
const MachineryNewSession = React.lazy(() => import('../pages/machinery/new-session'));
|
const MachineryNewSession = React.lazy(() => import('../pages/machinery/new-session'));
|
||||||
const MachineryLogout = React.lazy(() => import('../pages/machinery/logout'));
|
const MachineryLogout = React.lazy(() => import('../pages/machinery/logout'));
|
||||||
|
@ -32,6 +33,7 @@ export const AppRouter = () => {
|
||||||
<RouteWrapper component={LandingPage} path="/" />
|
<RouteWrapper component={LandingPage} path="/" />
|
||||||
<RouteWrapper component={ServersPage} path="/servers" />
|
<RouteWrapper component={ServersPage} path="/servers" />
|
||||||
<RouteWrapper component={PickerPage} path="/s/:serverID" />
|
<RouteWrapper component={PickerPage} path="/s/:serverID" />
|
||||||
|
<RouteWrapper component={EditorPage} path="/s/:serverID/edit" />
|
||||||
|
|
||||||
<RouteWrapper component={ErrorPage} path="/error" />
|
<RouteWrapper component={ErrorPage} path="/error" />
|
||||||
<RouteWrapper component={ErrorPage} path="/error/:identity" />
|
<RouteWrapper component={ErrorPage} path="/error/:identity" />
|
||||||
|
|
100
packages/web/src/pages/editor.tsx
Normal file
100
packages/web/src/pages/editor.tsx
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { Redirect } from '@reach/router';
|
||||||
|
import { EditorTemplate } from '@roleypoly/design-system/templates/editor';
|
||||||
|
import { GenericLoadingTemplate } from '@roleypoly/design-system/templates/generic-loading';
|
||||||
|
import {
|
||||||
|
GuildDataUpdate,
|
||||||
|
PresentableGuild,
|
||||||
|
UserGuildPermissions,
|
||||||
|
} from '@roleypoly/types';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useAppShellProps } from '../contexts/app-shell/AppShellContext';
|
||||||
|
import { useRecentGuilds } from '../contexts/recent-guilds/RecentGuildsContext';
|
||||||
|
import { useSessionContext } from '../contexts/session/SessionContext';
|
||||||
|
import { Title } from '../utils/metaTitle';
|
||||||
|
|
||||||
|
type EditorProps = {
|
||||||
|
serverID: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Editor = (props: EditorProps) => {
|
||||||
|
const { serverID } = props;
|
||||||
|
const { session, authedFetch, isAuthenticated } = useSessionContext();
|
||||||
|
const { pushRecentGuild } = useRecentGuilds();
|
||||||
|
const appShellProps = useAppShellProps();
|
||||||
|
|
||||||
|
const [guild, setGuild] = React.useState<PresentableGuild | null | false>(null);
|
||||||
|
const [pending, setPending] = React.useState(false);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchGuild = async () => {
|
||||||
|
const response = await authedFetch(`/get-picker-data/${serverID}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.status !== 200) {
|
||||||
|
setGuild(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setGuild(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchGuild();
|
||||||
|
}, [serverID, authedFetch]);
|
||||||
|
|
||||||
|
React.useCallback((serverID) => pushRecentGuild(serverID), [pushRecentGuild])(serverID);
|
||||||
|
|
||||||
|
// If the user is not authenticated, redirect to the login page.
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Redirect to={`/auth/login?r=${props.serverID}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is not an admin, they can't edit the guild
|
||||||
|
// so we redirect them to the picker
|
||||||
|
const guildSlug = session?.guilds?.find((guild) => guild.id === serverID);
|
||||||
|
if (guildSlug && guildSlug?.permissionLevel === UserGuildPermissions.User) {
|
||||||
|
return <Redirect to={`/s/${props.serverID}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the guild isn't loaded, render a loading placeholder
|
||||||
|
if (guild === null) {
|
||||||
|
return <GenericLoadingTemplate />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the guild is not found, redirect to the picker page
|
||||||
|
if (guild === false) {
|
||||||
|
return <Redirect to={`/s/${props.serverID}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGuildChange = async (guild: PresentableGuild) => {
|
||||||
|
if (pending) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPending(true);
|
||||||
|
|
||||||
|
const updatePayload: GuildDataUpdate = {
|
||||||
|
message: guild.data.message,
|
||||||
|
categories: guild.data.categories,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await authedFetch(`/update-guild/${serverID}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(updatePayload),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
setGuild(guild);
|
||||||
|
setPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Title title={`Editing ${guild.guild.name} - Roleypoly`} />
|
||||||
|
<EditorTemplate {...appShellProps} guild={guild} onGuildChange={onGuildChange} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Editor;
|
|
@ -22,7 +22,10 @@
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true
|
"isolatedModules": true,
|
||||||
|
"paths": {
|
||||||
|
"@roleypoly/*": ["./packages/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"exclude": ["node_modules", "**/*.stories.tsx", "packages/api"]
|
"exclude": ["node_modules", "**/*.stories.tsx", "packages/api"]
|
||||||
|
|
Loading…
Add table
Reference in a new issue