feat(UI): add role picker, auth helpers, refactor for viability

This commit is contained in:
41666 2020-12-18 00:14:30 -05:00
parent 3fe3cfc21f
commit e4e4bb9024
32 changed files with 408 additions and 136 deletions

View file

@ -41,7 +41,8 @@
"react-icons": "^4.1.0", "react-icons": "^4.1.0",
"react-is": "^17.0.1", "react-is": "^17.0.1",
"react-tooltip": "^4.2.11", "react-tooltip": "^4.2.11",
"styled-components": "^5.2.1" "styled-components": "^5.2.1",
"swr": "^0.3.9"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.12.10", "@babel/core": "^7.12.10",

View file

@ -0,0 +1,80 @@
import KSUID from 'ksuid';
import { CategoryType, Features, GuildData as GuildDataT } from 'roleypoly/common/types';
import { respond } from '../utils/api-tools';
import { GuildData } from '../utils/kv';
// Temporary use.
export const CreateRoleypolyData = async (request: Request): Promise<Response> => {
const data: GuildDataT = {
id: '386659935687147521',
message:
'Hey, this is kind of a demo setup so features/use cases can be shown off.\n\nThanks for using Roleypoly <3',
features: Features.Preview,
categories: [
{
id: KSUID.randomSync().string,
name: 'Demo Roles',
type: CategoryType.Multi,
hidden: false,
position: 0,
roles: [
'557825026406088717',
'557824994269200384',
'557824893241131029',
'557812915386843170',
'557812901717737472',
'557812805546541066',
],
},
{
id: KSUID.randomSync().string,
name: 'Colors',
type: CategoryType.Single,
hidden: false,
position: 1,
roles: ['394060232893923349', '394060145799331851', '394060192846839809'],
},
{
id: KSUID.randomSync().string,
name: 'Test Roles',
type: CategoryType.Multi,
hidden: false,
position: 5,
roles: ['558104828216213505', '558103534453653514', '558297233582194728'],
},
{
id: KSUID.randomSync().string,
name: 'Region',
type: CategoryType.Multi,
hidden: false,
position: 3,
roles: [
'397296181803483136',
'397296137066774529',
'397296218809827329',
'397296267283267605',
],
},
{
id: KSUID.randomSync().string,
name: 'Opt-in Channels',
type: CategoryType.Multi,
hidden: false,
position: 4,
roles: ['414514823959674890', '764230661904007219'],
},
{
id: KSUID.randomSync().string,
name: 'Pronouns',
type: CategoryType.Multi,
hidden: false,
position: 2,
roles: ['485916566790340608', '485916566941335583', '485916566311927808'],
},
],
};
await GuildData.put(data.id, data);
return respond({ ok: true });
};

View file

@ -1,4 +1,5 @@
import { BotJoin } from './handlers/bot-join'; import { BotJoin } from './handlers/bot-join';
import { CreateRoleypolyData } from './handlers/create-roleypoly-data';
import { GetPickerData } from './handlers/get-picker-data'; import { GetPickerData } from './handlers/get-picker-data';
import { GetSession } from './handlers/get-session'; import { GetSession } from './handlers/get-session';
import { GetSlug } from './handlers/get-slug'; import { GetSlug } from './handlers/get-slug';
@ -29,6 +30,7 @@ router.add('GET', 'x-headers', (request) => {
return new Response(JSON.stringify(headers)); return new Response(JSON.stringify(headers));
}); });
router.add('GET', 'x-create-roleypoly-data', CreateRoleypolyData);
addEventListener('fetch', (event: FetchEvent) => { addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(router.handle(event.request)); event.respondWith(router.handle(event.request));

View file

@ -1,3 +1,6 @@
import { addCORS } from './utils/api-tools';
import { uiPublicURI } from './utils/config';
export type Handler = (request: Request) => Promise<Response> | Response; export type Handler = (request: Request) => Promise<Response> | Response;
type RoutingTree = { type RoutingTree = {
@ -20,6 +23,8 @@ export class Router {
500: this.serverError, 500: this.serverError,
}; };
private uiURL = new URL(uiPublicURI);
addFallback(which: keyof Fallbacks, handler: Handler) { addFallback(which: keyof Fallbacks, handler: Handler) {
this.fallbacks[which] = handler; this.fallbacks[which] = handler;
} }
@ -46,13 +51,21 @@ export class Router {
if (handler) { if (handler) {
try { try {
return handler(request); const response = await handler(request);
// this.wrapCORS(request, response);
return response;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
return this.fallbacks[500](request); return this.fallbacks[500](request);
} }
} }
if (lowerMethod === 'options') {
return new Response(null, addCORS({}));
}
return this.fallbacks[404](request); return this.fallbacks[404](request);
} }

View file

@ -4,6 +4,7 @@ import {
permissions as Permissions, permissions as Permissions,
} from '../../common/utils/hasPermission'; } from '../../common/utils/hasPermission';
import { Handler } from '../router'; import { Handler } from '../router';
import { uiPublicURI } from './config';
import { Sessions, WrappedKVNamespace } from './kv'; import { Sessions, WrappedKVNamespace } from './kv';
export const formData = (obj: Record<string, any>): string => { export const formData = (obj: Record<string, any>): string => {
@ -12,8 +13,18 @@ export const formData = (obj: Record<string, any>): string => {
.join('&'); .join('&');
}; };
export const respond = (obj: Record<string, any>, init?: ResponseInit) => export const addCORS = (init: ResponseInit = {}) => ({
new Response(JSON.stringify(obj), init); ...init,
headers: {
...(init.headers || {}),
'access-control-allow-origin': uiPublicURI,
'access-control-allow-method': '*',
'access-control-allow-headers': '*',
},
});
export const respond = (obj: Record<string, any>, init: ResponseInit = {}) =>
new Response(JSON.stringify(obj), addCORS(init));
export const resolveFailures = ( export const resolveFailures = (
handleWith: () => Response, handleWith: () => Response,

View file

@ -0,0 +1,56 @@
import { NextPageContext } from 'next';
import getConfig from 'next/config';
import nookies from 'nookies';
import useSWR from 'swr';
export const getPublicURI = (context?: NextPageContext) => {
if (context?.req) {
const { publicRuntimeConfig } = getConfig();
return publicRuntimeConfig.apiPublicURI;
} else {
return typeof localStorage !== 'undefined' && localStorage.getItem('api_uri');
}
};
export const getSessionKey = (context?: NextPageContext) => {
if (context?.req) {
return nookies.get(context)['rp_session_key'];
} else {
return (
typeof sessionStorage !== 'undefined' && sessionStorage.getItem('session_key')
);
}
};
export const apiFetch = async <T>(
path: string,
init?: RequestInit,
context?: NextPageContext
): Promise<T | null> => {
const sessionKey = getSessionKey(context);
if (!sessionKey) {
return null;
}
const authorizedInit: RequestInit = {
...(init || {}),
headers: {
...(init?.headers || {}),
authorization: `Bearer ${sessionKey}`,
},
};
const response = await fetch(`${getPublicURI(context)}${path}`, authorizedInit);
if (response.status >= 400) {
const reason = (await response.json())['error'];
throw new Error(`Fetch failed: ${reason}`);
}
return response.json() as Promise<T>;
};
export const swrFetch = <T>(path: string, context?: NextPageContext) =>
useSWR<T>(path, (url: string): Promise<any> => apiFetch<T>(url, undefined, context), {
revalidateOnFocus: false,
});

View file

@ -1,7 +1,7 @@
export const sortBy = <T>( export const sortBy = <T, Key extends keyof T>(
array: T[], array: T[],
key: keyof T, key: Key,
predicate?: (a: T[keyof T], b: T[keyof T]) => number predicate?: (a: T[typeof key], b: T[typeof key]) => number
) => { ) => {
return array.sort((a, b) => { return array.sort((a, b) => {
if (predicate) { if (predicate) {

View file

@ -9,6 +9,7 @@ export default {
}, },
args: { args: {
initials: 'KR', initials: 'KR',
hash: 'aa',
}, },
}; };

View file

@ -5,13 +5,14 @@ export type AvatarProps = {
src?: string; src?: string;
children?: string | React.ReactNode; children?: string | React.ReactNode;
size?: number; size?: number;
hash?: string;
deliberatelyEmpty?: boolean; deliberatelyEmpty?: boolean;
}; };
/** Chuldren is recommended to not be larger than 2 uppercase letters. */ /** Chuldren is recommended to not be larger than 2 uppercase letters. */
export const Avatar = (props: AvatarProps) => ( export const Avatar = (props: AvatarProps) => (
<Container size={props.size} deliberatelyEmpty={props.deliberatelyEmpty}> <Container size={props.size} deliberatelyEmpty={props.deliberatelyEmpty}>
{props.src && ( {props.src && props.hash && (
<Image <Image
style={{ style={{
backgroundImage: `url(${props.src})`, backgroundImage: `url(${props.src})`,

View file

@ -27,16 +27,18 @@ export const PopoverBase = styled.div<PopoverStyledProps>`
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
`} `}
${onSmallScreen(css` ${onSmallScreen(
position: absolute; css`
top: 0; position: absolute;
left: 0; top: 0;
bottom: 0; left: 0;
right: 0; bottom: 0;
min-width: unset; right: 0;
width: 100vw; min-width: unset;
height: 100vh; width: 100vw;
`)}; height: 100vh;
`
)};
`; `;
export const DefocusHandler = styled.div<PopoverStyledProps>` export const DefocusHandler = styled.div<PopoverStyledProps>`
@ -84,4 +86,5 @@ export const PopoverHeadCloser = styled.div`
export const PopoverContent = styled.div` export const PopoverContent = styled.div`
padding: 5px; padding: 5px;
overflow-y: hidden;
`; `;

View file

@ -16,13 +16,14 @@ type PopoverProps = {
canDefocus?: boolean; canDefocus?: boolean;
onExit?: (type: 'escape' | 'defocus' | 'explicit') => void; onExit?: (type: 'escape' | 'defocus' | 'explicit') => void;
headContent: React.ReactNode; headContent: React.ReactNode;
preferredWidth?: number;
}; };
export const Popover = (props: PopoverProps) => { export const Popover = (props: PopoverProps) => {
globalOnKeyUp(['Escape'], () => props.onExit?.('escape'), props.active); globalOnKeyUp(['Escape'], () => props.onExit?.('escape'), props.active);
return ( return (
<> <>
<PopoverBase active={props.active}> <PopoverBase active={props.active} preferredWidth={props.preferredWidth}>
<PopoverHead> <PopoverHead>
<PopoverHeadCloser onClick={() => props.onExit?.('explicit')}> <PopoverHeadCloser onClick={() => props.onExit?.('explicit')}>
<IoMdClose /> <IoMdClose />

View file

@ -32,8 +32,7 @@ export const Circle = styled.div<StyledProps>`
svg { svg {
width: 10px; width: 10px;
height: 10px; height: 10px;
fill-opacity: ${(props) => fill-opacity: ${(props) => (props.selected || props.disabled ? 1 : 0)};
props.selected || props.disabled || props.type !== 'delete' ? 1 : 0};
transition: fill-opacity ${transitions.in2in}s ease-in-out; transition: fill-opacity ${transitions.in2in}s ease-in-out;
fill: ${(props) => fill: ${(props) =>
props.disabled && props.defaultColor props.disabled && props.defaultColor

View file

@ -1,5 +1,6 @@
import Link from 'next/link'; import Link from 'next/link';
import * as React from 'react'; import * as React from 'react';
import Scrollbars from 'react-custom-scrollbars';
import { GoStar, GoZap } from 'react-icons/go'; import { GoStar, GoZap } from 'react-icons/go';
import ReactTooltip from 'react-tooltip'; import ReactTooltip from 'react-tooltip';
import { GuildSlug, UserGuildPermissions } from 'roleypoly/common/types'; import { GuildSlug, UserGuildPermissions } from 'roleypoly/common/types';
@ -29,14 +30,23 @@ const Badges = (props: { guild: GuildSlug }) => {
export const GuildNav = (props: Props) => ( export const GuildNav = (props: Props) => (
<div> <div>
{sortBy(props.guilds, 'id').map((guild) => ( <Scrollbars
<Link href={`/s/${guild.id}`} passHref> universal
<GuildNavItem> autoHide
<NavSlug guild={guild || null} key={guild.id} /> // autoHeight
<Badges guild={guild} /> style={{ height: 'calc(100vh - 45px - 1.4em)', overflowX: 'hidden' }}
</GuildNavItem> >
</Link> {sortBy(props.guilds, 'name', (a: string, b: string) =>
))} a.toLowerCase() > b.toLowerCase() ? 1 : -1
<ReactTooltip id={tooltipId} /> ).map((guild) => (
<Link href={`/s/${guild.id}`} passHref key={guild.id}>
<GuildNavItem>
<NavSlug guild={guild || null} key={guild.id} />
<Badges guild={guild} />
</GuildNavItem>
</Link>
))}
<ReactTooltip id={tooltipId} />
</Scrollbars>
</div> </div>
); );

View file

@ -11,9 +11,11 @@ type Props = {
export const NavSlug = (props: Props) => ( export const NavSlug = (props: Props) => (
<SlugContainer> <SlugContainer>
<Avatar <Avatar
hash={props.guild ? props.guild.icon : undefined}
src={ src={
(props.guild && utils.avatarHash(props.guild.id, props.guild.icon)) || props.guild
undefined ? utils.avatarHash(props.guild.id, props.guild.icon)
: undefined
} }
deliberatelyEmpty={!props.guild} deliberatelyEmpty={!props.guild}
size={35} size={35}

View file

@ -27,6 +27,7 @@ export const PreauthGreeting = (props: GreetingProps) => (
'icons', 'icons',
512 512
)} )}
hash={props.guildSlug.icon}
> >
{avatarUtils.initialsFromName(props.guildSlug.name)} {avatarUtils.initialsFromName(props.guildSlug.name)}
</Avatar> </Avatar>

View file

@ -1,14 +1,13 @@
import Link from 'next/link'; import Link from 'next/link';
import * as React from 'react'; import * as React from 'react';
import { GoPencil } from 'react-icons/go'; import { GoPencil } from 'react-icons/go';
import { Guild } from 'roleypoly/common/types'; import { GuildSlug } from 'roleypoly/common/types';
import { guild } from 'roleypoly/common/types/storyData';
import { Avatar, utils } from 'roleypoly/design-system/atoms/avatar'; import { Avatar, utils } from 'roleypoly/design-system/atoms/avatar';
import { AccentTitle, AmbientLarge } from 'roleypoly/design-system/atoms/typography'; import { AccentTitle, AmbientLarge } from 'roleypoly/design-system/atoms/typography';
import { Editable, Icon, Name, Wrapper } from './ServerMasthead.styled'; import { Editable, Icon, Name, Wrapper } from './ServerMasthead.styled';
export type ServerMastheadProps = { export type ServerMastheadProps = {
guild: Guild; guild: GuildSlug;
editable: boolean; editable: boolean;
}; };
@ -17,8 +16,9 @@ export const ServerMasthead = (props: ServerMastheadProps) => {
<Wrapper> <Wrapper>
<Icon> <Icon>
<Avatar <Avatar
hash={props.guild.icon}
size={props.editable ? 60 : 48} size={props.editable ? 60 : 48}
src={utils.avatarHash(guild.id, guild.icon, 'icons', 512)} src={utils.avatarHash(props.guild.id, props.guild.icon, 'icons', 512)}
> >
{utils.initialsFromName(props.guild.name)} {utils.initialsFromName(props.guild.name)}
</Avatar> </Avatar>

View file

@ -19,6 +19,7 @@ export const UserAvatarGroup = (props: Props) => (
</Collapse> </Collapse>
<Avatar <Avatar
size={34} size={34}
hash={props.user.avatar}
src={utils.avatarHash(props.user.id, props.user.avatar, 'avatars')} src={utils.avatarHash(props.user.id, props.user.avatar, 'avatars')}
> >
{utils.initialsFromName(props.user.username)} {utils.initialsFromName(props.user.username)}

View file

@ -62,6 +62,7 @@ export const Authed = (props: Props) => {
canDefocus canDefocus
position="bottom left" position="bottom left"
active={serverPopoverState} active={serverPopoverState}
preferredWidth={560}
onExit={() => setServerPopoverState(false)} onExit={() => setServerPopoverState(false)}
> >
{() => <GuildNav guilds={props.guilds} />} {() => <GuildNav guilds={props.guilds} />}

View file

@ -1,16 +1,19 @@
import NextLink from 'next/link';
import * as React from 'react'; import * as React from 'react';
import { GoInfo } from 'react-icons/go'; import { GoInfo } from 'react-icons/go';
import { import {
Category, Category,
CategoryType, CategoryType,
Guild,
GuildData, GuildData,
GuildSlug,
Member, Member,
Role, Role,
} from 'roleypoly/common/types'; } from 'roleypoly/common/types';
import { ReactifyNewlines } from 'roleypoly/common/utils/ReactifyNewlines'; import { ReactifyNewlines } from 'roleypoly/common/utils/ReactifyNewlines';
import { sortBy } from 'roleypoly/common/utils/sortBy';
import { FaderOpacity } from 'roleypoly/design-system/atoms/fader'; import { FaderOpacity } from 'roleypoly/design-system/atoms/fader';
import { Space } from 'roleypoly/design-system/atoms/space'; import { Space } from 'roleypoly/design-system/atoms/space';
import { Link } from 'roleypoly/design-system/atoms/typography';
import { PickerCategory } from 'roleypoly/design-system/molecules/picker-category'; import { PickerCategory } from 'roleypoly/design-system/molecules/picker-category';
import { ResetSubmit } from 'roleypoly/design-system/molecules/reset-submit'; import { ResetSubmit } from 'roleypoly/design-system/molecules/reset-submit';
import { ServerMasthead } from 'roleypoly/design-system/molecules/server-masthead'; import { ServerMasthead } from 'roleypoly/design-system/molecules/server-masthead';
@ -23,7 +26,7 @@ import {
} from './RolePicker.styled'; } from './RolePicker.styled';
export type RolePickerProps = { export type RolePickerProps = {
guild: Guild; guild: GuildSlug;
guildData: GuildData; guildData: GuildData;
member: Member; member: Member;
roles: Role[]; roles: Role[];
@ -81,32 +84,34 @@ export const RolePicker = (props: RolePickerProps) => {
{props.guildData.categories.length !== 0 ? ( {props.guildData.categories.length !== 0 ? (
<> <>
<div> <div>
{props.guildData.categories.map((category, idx) => ( {sortBy(props.guildData.categories, 'position').map(
<CategoryContainer key={idx}> (category, idx) => (
<PickerCategory <CategoryContainer key={idx}>
key={idx} <PickerCategory
category={category} key={idx}
title={category.name} category={category}
selectedRoles={selectedRoles.filter((roleId) => title={category.name}
category.roles.includes(roleId) selectedRoles={selectedRoles.filter((roleId) =>
)} category.roles.includes(roleId)
roles={ )}
category.roles roles={
.map((role) => category.roles
props.roles.find((r) => r.id === role) .map((role) =>
) props.roles.find((r) => r.id === role)
.filter((r) => r !== undefined) as Role[] )
} .filter((r) => r !== undefined) as Role[]
onChange={handleChange(category)} }
wikiMode={false} onChange={handleChange(category)}
type={ wikiMode={false}
category.type === CategoryType.Single type={
? 'single' category.type === CategoryType.Single
: 'multi' ? 'single'
} : 'multi'
/> }
</CategoryContainer> />
))} </CategoryContainer>
)
)}
</div> </div>
<FaderOpacity <FaderOpacity
isVisible={!arrayMatches(selectedRoles, props.member.roles)} isVisible={!arrayMatches(selectedRoles, props.member.roles)}
@ -126,6 +131,18 @@ export const RolePicker = (props: RolePickerProps) => {
</InfoIcon> </InfoIcon>
<div> <div>
There are currently no roles available for you to choose from. There are currently no roles available for you to choose from.
{props.editable && (
<>
{' '}
<NextLink
passHref
href={`/s/[id]/edit`}
as={`/s/${props.guild.id}/edit`}
>
<Link>Add some roles!</Link>
</NextLink>
</>
)}
</div> </div>
</InfoBox> </InfoBox>
)} )}

View file

@ -1,9 +1,10 @@
import * as React from 'react'; import * as React from 'react';
import { AppShell } from 'roleypoly/design-system/organisms/app-shell'; import { AppShell } from 'roleypoly/design-system/organisms/app-shell';
import { Landing } from 'roleypoly/design-system/organisms/landing'; import { Landing } from 'roleypoly/design-system/organisms/landing';
import { ProvidableAppShellProps } from 'roleypoly/providers/appShellData';
export const LandingTemplate = () => ( export const LandingTemplate = (props: ProvidableAppShellProps) => (
<AppShell showFooter> <AppShell showFooter {...props}>
<Landing /> <Landing />
</AppShell> </AppShell>
); );

View file

@ -5,7 +5,7 @@ import {
RolePickerProps, RolePickerProps,
} from 'roleypoly/design-system/organisms/role-picker'; } from 'roleypoly/design-system/organisms/role-picker';
export type RolePickerTemplateProps = RolePickerProps & AppShellProps; export type RolePickerTemplateProps = RolePickerProps & Omit<AppShellProps, 'children'>;
export const RolePickerTemplate = (props: RolePickerTemplateProps) => { export const RolePickerTemplate = (props: RolePickerTemplateProps) => {
const { user, guilds, activeGuildId, ...pickerProps } = props; const { user, guilds, activeGuildId, ...pickerProps } = props;

View file

@ -1,8 +1,6 @@
import NextApp, { AppContext, AppProps } from 'next/app'; import { AppProps } from 'next/app';
import nookies from 'nookies';
import * as React from 'react'; import * as React from 'react';
import { InjectTypekitFont } from 'roleypoly/design-system/atoms/fonts'; import { InjectTypekitFont } from 'roleypoly/design-system/atoms/fonts';
import { AuthProvider } from 'roleypoly/providers/auth/AuthContext';
type Props = AppProps & { type Props = AppProps & {
sessionKey: string | null; sessionKey: string | null;
@ -11,26 +9,7 @@ type Props = AppProps & {
const App = (props: Props) => ( const App = (props: Props) => (
<> <>
<InjectTypekitFont /> <InjectTypekitFont />
<AuthProvider sessionKey={props.sessionKey}> <props.Component {...props.pageProps} />
<props.Component {...props.pageProps} />
</AuthProvider>
</> </>
); );
export default App; export default App;
export const getInitialProps = async (context: AppContext) => {
let sessionKey: string | null = null;
if (context.ctx.req) {
const key = nookies.get(context.ctx)['rp_session_key'];
if (key) {
sessionKey = key;
}
} else {
sessionKey = sessionStorage.getItem('session_key');
}
const pageProps = await NextApp.getInitialProps(context);
return { ...pageProps, sessionKey };
};

View file

@ -1,5 +1,20 @@
import { useRouter } from 'next/router';
import * as React from 'react'; import * as React from 'react';
import { LandingTemplate } from 'roleypoly/design-system/templates/landing'; import { LandingTemplate } from 'roleypoly/design-system/templates/landing';
import { useAppShellProps } from 'roleypoly/providers/appShellData';
const Index = () => <LandingTemplate />; const Index = () => {
const {
appShellProps: { guilds, user },
} = useAppShellProps();
const router = useRouter();
React.useEffect(() => {
if (user || guilds) {
router.replace('/servers');
}
}, [guilds, user]);
return <LandingTemplate user={user} guilds={guilds} />;
};
export default Index; export default Index;

View file

@ -14,6 +14,7 @@ type Props = {
const Logout = (props: Props) => { const Logout = (props: Props) => {
React.useEffect(() => { React.useEffect(() => {
sessionStorage.removeItem('session_key'); sessionStorage.removeItem('session_key');
sessionStorage.removeItem('session_data');
location.href = '/'; location.href = '/';
}, []); }, []);

View file

@ -1,4 +1,5 @@
import { NextPageContext } from 'next'; import { NextPageContext } from 'next';
import getConfig from 'next/config';
import nookies from 'nookies'; import nookies from 'nookies';
import * as React from 'react'; import * as React from 'react';
import { Hero } from 'roleypoly/design-system/atoms/hero'; import { Hero } from 'roleypoly/design-system/atoms/hero';
@ -7,15 +8,18 @@ import { AppShell } from 'roleypoly/design-system/organisms/app-shell';
type Props = { type Props = {
sessionID: string; sessionID: string;
apiURI: string;
}; };
const NewSession = (props: Props) => { const NewSession = (props: Props) => {
const { sessionID } = props; const { sessionID, apiURI } = props;
React.useEffect(() => { React.useEffect(() => {
sessionStorage.setItem('session_key', sessionID); sessionStorage.setItem('session_key', sessionID);
localStorage.setItem('api_uri', apiURI); // TODO: set better
location.href = '/'; location.href = '/';
}, [sessionID]); }, [sessionID, apiURI]);
return ( return (
<AppShell> <AppShell>
@ -26,7 +30,11 @@ const NewSession = (props: Props) => {
); );
}; };
export const getServerSideProps = (context: NextPageContext): { props: Props } => { export const getServerSideProps = async (
context: NextPageContext
): Promise<{ props: Props }> => {
const { publicRuntimeConfig } = getConfig();
const apiURI = publicRuntimeConfig.apiPublicURI;
const sessionID = context.query.session_id as string; const sessionID = context.query.session_id as string;
if (!sessionID) { if (!sessionID) {
throw new Error("I shouldn't be here today."); throw new Error("I shouldn't be here today.");
@ -39,7 +47,7 @@ export const getServerSideProps = (context: NextPageContext): { props: Props } =
sameSite: 'strict', sameSite: 'strict',
}); });
return { props: { sessionID } }; return { props: { sessionID, apiURI } };
}; };
export default NewSession; export default NewSession;

59
src/pages/s/[id].tsx Normal file
View file

@ -0,0 +1,59 @@
import { NextPage, NextPageContext } from 'next';
import Head from 'next/head';
import * as React from 'react';
import { PresentableGuild, UserGuildPermissions } from 'roleypoly/common/types';
import { apiFetch } from 'roleypoly/common/utils/isomorphicFetch';
import { RolePickerTemplate } from 'roleypoly/design-system/templates/role-picker';
import { useAppShellProps } from 'roleypoly/providers/appShellData';
type Props = {
data: PresentableGuild;
};
const RolePickerPage: NextPage<Props> = (props) => {
const { appShellProps } = useAppShellProps();
return (
<>
<Head>
<title>Picking roles on {props.data.guild.name} - Roleypoly</title>
</Head>
<RolePickerTemplate
user={appShellProps.user}
guilds={appShellProps.guilds}
guild={props.data.guild}
roles={props.data.roles}
guildData={props.data.data}
member={props.data.member}
editable={props.data.guild.permissionLevel !== UserGuildPermissions.User}
activeGuildId={props.data.id}
onSubmit={(i) => {
console.log(i);
}}
/>
</>
);
};
RolePickerPage.getInitialProps = async (context: NextPageContext): Promise<Props> => {
const serverID = context.query.id;
if (!serverID) {
throw new Error('serverID missing');
}
const pickerData = await apiFetch<PresentableGuild>(
`/get-picker-data/${serverID}`,
undefined,
context
);
if (!pickerData) {
throw new Error('TODO: picker fetch failed');
}
return {
data: pickerData,
};
};
export default RolePickerPage;

10
src/pages/s/[id]/edit.tsx Normal file
View file

@ -0,0 +1,10 @@
import Link from 'next/link';
import * as React from 'react';
const ServerEditor = () => (
<div>
<Link href="/">Go back</Link>
</div>
);
export default ServerEditor;

1
src/pages/servers.tsx Normal file
View file

@ -0,0 +1 @@
export default () => <div></div>;

View file

@ -0,0 +1,27 @@
import { NextPageContext } from 'next';
import { SessionData } from 'roleypoly/common/types';
import { swrFetch } from 'roleypoly/common/utils/isomorphicFetch';
import { AppShellProps } from 'roleypoly/design-system/organisms/app-shell';
export type ProvidableAppShellProps = {
user: AppShellProps['user'];
guilds: AppShellProps['guilds'];
};
export const useAppShellProps = (context?: NextPageContext) => {
const { data, error } = swrFetch<Omit<SessionData, 'tokens'>>(
'/get-session',
context
);
const props: ProvidableAppShellProps = {
user: data?.user,
guilds: data?.guilds,
};
return {
appShellProps: props,
isLoading: !error && !data,
isError: error,
};
};

View file

@ -1,40 +0,0 @@
import * as React from 'react';
type AuthContextType = {
sessionKey: string | null;
setSessionKey: (value: string | null) => void;
};
type Props = {
sessionKey: string | null;
children: React.ReactNode;
};
const AuthContext = React.createContext<AuthContextType>({
sessionKey: null,
setSessionKey: () => {},
});
export const AuthProvider = (props: Props) => {
const [sessionKey, setSessionKey] = React.useState(props.sessionKey);
return (
<AuthContext.Provider value={{ sessionKey, setSessionKey }}>
{props.children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const authCtx = React.useContext(AuthContext);
if (!authCtx) {
throw new Error('useAuth used without AuthProvider');
}
return authCtx;
};
export const isAuthenticated = () => {
const authCtx = useAuth();
return authCtx.sessionKey !== null;
};

View file

@ -1 +0,0 @@
export * from './AuthContext';

View file

@ -5603,6 +5603,11 @@ depd@~1.1.2:
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
dequal@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
des.js@^1.0.0: des.js@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843"
@ -13282,6 +13287,13 @@ svg-tags@^1.0.0:
resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q= integrity sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=
swr@^0.3.9:
version "0.3.9"
resolved "https://registry.yarnpkg.com/swr/-/swr-0.3.9.tgz#a179a795244c7b68684af6a632f1ad579e6a69e0"
integrity sha512-lyN4SjBzpoW4+v3ebT7JUtpzf9XyzrFwXIFv+E8ZblvMa5enSNaUBs4EPkL8gGA/GDMLngEmB53o5LaNboAPfg==
dependencies:
dequal "2.0.2"
symbol-tree@^3.2.2, symbol-tree@^3.2.4: symbol-tree@^3.2.2, symbol-tree@^3.2.4:
version "3.2.4" version "3.2.4"
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"