mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-06-16 17:49:09 +00:00
add editor wiring and styling updates
This commit is contained in:
parent
1c38ab145c
commit
8cf3b2c78d
20 changed files with 434 additions and 53 deletions
38
packages/api/handlers/update-guild.ts
Normal file
38
packages/api/handlers/update-guild.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
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();
|
||||
}
|
||||
);
|
|
@ -1,13 +1,6 @@
|
|||
import { onTablet } from '@roleypoly/design-system/atoms/breakpoints';
|
||||
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`
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 100%;
|
||||
|
@ -15,3 +8,16 @@ export const HalfsiesItem = styled.div`
|
|||
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;
|
||||
`}
|
||||
`;
|
||||
|
|
|
@ -22,8 +22,9 @@ export const TabTitleRow = styled.div`
|
|||
position: fixed;
|
||||
${onSmallScreen(
|
||||
css`
|
||||
width: fit-content;
|
||||
position: unset;
|
||||
max-width: 100vw;
|
||||
max-width: 98vw;
|
||||
`
|
||||
)}
|
||||
`;
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
export type TabViewProps = {
|
||||
children: React.ReactNode[];
|
||||
initialTab?: number;
|
||||
masthead?: React.ReactNode;
|
||||
};
|
||||
|
||||
type TabProps = {
|
||||
|
@ -39,6 +40,7 @@ export const TabView = (props: TabViewProps) => {
|
|||
return (
|
||||
<TabViewStyled>
|
||||
<TabTitleRow>
|
||||
{props.masthead && props.masthead}
|
||||
<QuickNav
|
||||
currentNavItem={tabNames[currentTab]}
|
||||
navItems={tabNames}
|
||||
|
|
|
@ -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 { ResetSubmit } from './ResetSubmit';
|
||||
import { InlineResetSubmit, ResetSubmit } from './ResetSubmit';
|
||||
|
||||
export default {
|
||||
title: 'Molecules',
|
||||
title: 'Molecules/Reset and Submit',
|
||||
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 * as React from 'react';
|
||||
import { MdRestore } from 'react-icons/md';
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
type Props = {
|
||||
onSubmit: () => void;
|
||||
|
@ -22,8 +22,14 @@ const Left = styled.div`
|
|||
`}
|
||||
`;
|
||||
|
||||
const Right = styled.div`
|
||||
const Right = styled.div<{ inline?: boolean }>`
|
||||
flex: 1;
|
||||
|
||||
${(props) =>
|
||||
props.inline &&
|
||||
css`
|
||||
padding-left: 0.2em;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const ResetSubmit = (props: Props) => {
|
||||
|
@ -40,3 +46,20 @@ export const ResetSubmit = (props: Props) => {
|
|||
</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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
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 = {
|
||||
|
@ -34,23 +37,35 @@ export const EditorShell = (props: EditorShellProps) => {
|
|||
});
|
||||
};
|
||||
|
||||
const hasChanges = React.useMemo(() => !deepEqual(guild.data, props.guild.data), [
|
||||
guild.data,
|
||||
props.guild.data,
|
||||
]);
|
||||
|
||||
return (
|
||||
<TabView initialTab={0}>
|
||||
<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>
|
||||
<Space />
|
||||
<TabView initialTab={0} masthead={<EditorMasthead guild={guild} />}>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"dependencies": {
|
||||
"@roleypoly/types": "*",
|
||||
"chroma-js": "^2.1.2",
|
||||
"deep-equal": "^2.0.5",
|
||||
"isomorphic-unfetch": "^3.1.0",
|
||||
"ksuid": "^2.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -30,6 +31,7 @@
|
|||
"@storybook/react": "^6.3.2",
|
||||
"@storybook/theming": "^6.3.2",
|
||||
"@types/chroma-js": "^2.1.3",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/node": "^15.12.5",
|
||||
"@types/react": "^17.0.11",
|
||||
"@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} />;
|
22
packages/design-system/templates/editor/Editor.tsx
Normal file
22
packages/design-system/templates/editor/Editor.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
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;
|
||||
};
|
||||
|
||||
export type GuildDataUpdate = Omit<Omit<GuildData, 'features'>, 'id'>;
|
||||
|
||||
export type PresentableGuild = {
|
||||
id: string;
|
||||
guild: GuildSlug;
|
||||
|
|
|
@ -7,6 +7,7 @@ import LandingPage from '../pages/landing';
|
|||
import PickerPage from '../pages/picker';
|
||||
|
||||
const ServersPage = React.lazy(() => import('../pages/servers'));
|
||||
const EditorPage = React.lazy(() => import('../pages/editor'));
|
||||
|
||||
const MachineryNewSession = React.lazy(() => import('../pages/machinery/new-session'));
|
||||
const MachineryLogout = React.lazy(() => import('../pages/machinery/logout'));
|
||||
|
@ -32,6 +33,7 @@ export const AppRouter = () => {
|
|||
<RouteWrapper component={LandingPage} path="/" />
|
||||
<RouteWrapper component={ServersPage} path="/servers" />
|
||||
<RouteWrapper component={PickerPage} path="/s/:serverID" />
|
||||
<RouteWrapper component={EditorPage} path="/s/:serverID/edit" />
|
||||
|
||||
<RouteWrapper component={ErrorPage} path="/error" />
|
||||
<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;
|
Loading…
Add table
Add a link
Reference in a new issue