mirror of
https://github.com/roleypoly/roleypoly.git
synced 2025-04-24 19:39:11 +00:00
feat(UI): add role picker, auth helpers, refactor for viability
This commit is contained in:
parent
3fe3cfc21f
commit
e4e4bb9024
32 changed files with 408 additions and 136 deletions
|
@ -41,7 +41,8 @@
|
|||
"react-icons": "^4.1.0",
|
||||
"react-is": "^17.0.1",
|
||||
"react-tooltip": "^4.2.11",
|
||||
"styled-components": "^5.2.1"
|
||||
"styled-components": "^5.2.1",
|
||||
"swr": "^0.3.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.10",
|
||||
|
|
80
src/backend-worker/handlers/create-roleypoly-data.ts
Normal file
80
src/backend-worker/handlers/create-roleypoly-data.ts
Normal 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 });
|
||||
};
|
|
@ -1,4 +1,5 @@
|
|||
import { BotJoin } from './handlers/bot-join';
|
||||
import { CreateRoleypolyData } from './handlers/create-roleypoly-data';
|
||||
import { GetPickerData } from './handlers/get-picker-data';
|
||||
import { GetSession } from './handlers/get-session';
|
||||
import { GetSlug } from './handlers/get-slug';
|
||||
|
@ -29,6 +30,7 @@ router.add('GET', 'x-headers', (request) => {
|
|||
|
||||
return new Response(JSON.stringify(headers));
|
||||
});
|
||||
router.add('GET', 'x-create-roleypoly-data', CreateRoleypolyData);
|
||||
|
||||
addEventListener('fetch', (event: FetchEvent) => {
|
||||
event.respondWith(router.handle(event.request));
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import { addCORS } from './utils/api-tools';
|
||||
import { uiPublicURI } from './utils/config';
|
||||
|
||||
export type Handler = (request: Request) => Promise<Response> | Response;
|
||||
|
||||
type RoutingTree = {
|
||||
|
@ -20,6 +23,8 @@ export class Router {
|
|||
500: this.serverError,
|
||||
};
|
||||
|
||||
private uiURL = new URL(uiPublicURI);
|
||||
|
||||
addFallback(which: keyof Fallbacks, handler: Handler) {
|
||||
this.fallbacks[which] = handler;
|
||||
}
|
||||
|
@ -46,13 +51,21 @@ export class Router {
|
|||
|
||||
if (handler) {
|
||||
try {
|
||||
return handler(request);
|
||||
const response = await handler(request);
|
||||
|
||||
// this.wrapCORS(request, response);
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return this.fallbacks[500](request);
|
||||
}
|
||||
}
|
||||
|
||||
if (lowerMethod === 'options') {
|
||||
return new Response(null, addCORS({}));
|
||||
}
|
||||
|
||||
return this.fallbacks[404](request);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
permissions as Permissions,
|
||||
} from '../../common/utils/hasPermission';
|
||||
import { Handler } from '../router';
|
||||
import { uiPublicURI } from './config';
|
||||
import { Sessions, WrappedKVNamespace } from './kv';
|
||||
|
||||
export const formData = (obj: Record<string, any>): string => {
|
||||
|
@ -12,8 +13,18 @@ export const formData = (obj: Record<string, any>): string => {
|
|||
.join('&');
|
||||
};
|
||||
|
||||
export const respond = (obj: Record<string, any>, init?: ResponseInit) =>
|
||||
new Response(JSON.stringify(obj), init);
|
||||
export const addCORS = (init: ResponseInit = {}) => ({
|
||||
...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 = (
|
||||
handleWith: () => Response,
|
||||
|
|
56
src/common/utils/isomorphicFetch.ts
Normal file
56
src/common/utils/isomorphicFetch.ts
Normal 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,
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
export const sortBy = <T>(
|
||||
export const sortBy = <T, Key extends keyof T>(
|
||||
array: T[],
|
||||
key: keyof T,
|
||||
predicate?: (a: T[keyof T], b: T[keyof T]) => number
|
||||
key: Key,
|
||||
predicate?: (a: T[typeof key], b: T[typeof key]) => number
|
||||
) => {
|
||||
return array.sort((a, b) => {
|
||||
if (predicate) {
|
||||
|
|
|
@ -9,6 +9,7 @@ export default {
|
|||
},
|
||||
args: {
|
||||
initials: 'KR',
|
||||
hash: 'aa',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -5,13 +5,14 @@ export type AvatarProps = {
|
|||
src?: string;
|
||||
children?: string | React.ReactNode;
|
||||
size?: number;
|
||||
hash?: string;
|
||||
deliberatelyEmpty?: boolean;
|
||||
};
|
||||
|
||||
/** Chuldren is recommended to not be larger than 2 uppercase letters. */
|
||||
export const Avatar = (props: AvatarProps) => (
|
||||
<Container size={props.size} deliberatelyEmpty={props.deliberatelyEmpty}>
|
||||
{props.src && (
|
||||
{props.src && props.hash && (
|
||||
<Image
|
||||
style={{
|
||||
backgroundImage: `url(${props.src})`,
|
||||
|
|
|
@ -27,16 +27,18 @@ export const PopoverBase = styled.div<PopoverStyledProps>`
|
|||
opacity: 0;
|
||||
pointer-events: none;
|
||||
`}
|
||||
${onSmallScreen(css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
min-width: unset;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
`)};
|
||||
${onSmallScreen(
|
||||
css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
min-width: unset;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
`
|
||||
)};
|
||||
`;
|
||||
|
||||
export const DefocusHandler = styled.div<PopoverStyledProps>`
|
||||
|
@ -84,4 +86,5 @@ export const PopoverHeadCloser = styled.div`
|
|||
|
||||
export const PopoverContent = styled.div`
|
||||
padding: 5px;
|
||||
overflow-y: hidden;
|
||||
`;
|
||||
|
|
|
@ -16,13 +16,14 @@ type PopoverProps = {
|
|||
canDefocus?: boolean;
|
||||
onExit?: (type: 'escape' | 'defocus' | 'explicit') => void;
|
||||
headContent: React.ReactNode;
|
||||
preferredWidth?: number;
|
||||
};
|
||||
|
||||
export const Popover = (props: PopoverProps) => {
|
||||
globalOnKeyUp(['Escape'], () => props.onExit?.('escape'), props.active);
|
||||
return (
|
||||
<>
|
||||
<PopoverBase active={props.active}>
|
||||
<PopoverBase active={props.active} preferredWidth={props.preferredWidth}>
|
||||
<PopoverHead>
|
||||
<PopoverHeadCloser onClick={() => props.onExit?.('explicit')}>
|
||||
<IoMdClose />
|
||||
|
|
|
@ -32,8 +32,7 @@ export const Circle = styled.div<StyledProps>`
|
|||
svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
fill-opacity: ${(props) =>
|
||||
props.selected || props.disabled || props.type !== 'delete' ? 1 : 0};
|
||||
fill-opacity: ${(props) => (props.selected || props.disabled ? 1 : 0)};
|
||||
transition: fill-opacity ${transitions.in2in}s ease-in-out;
|
||||
fill: ${(props) =>
|
||||
props.disabled && props.defaultColor
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Link from 'next/link';
|
||||
import * as React from 'react';
|
||||
import Scrollbars from 'react-custom-scrollbars';
|
||||
import { GoStar, GoZap } from 'react-icons/go';
|
||||
import ReactTooltip from 'react-tooltip';
|
||||
import { GuildSlug, UserGuildPermissions } from 'roleypoly/common/types';
|
||||
|
@ -29,14 +30,23 @@ const Badges = (props: { guild: GuildSlug }) => {
|
|||
|
||||
export const GuildNav = (props: Props) => (
|
||||
<div>
|
||||
{sortBy(props.guilds, 'id').map((guild) => (
|
||||
<Link href={`/s/${guild.id}`} passHref>
|
||||
<GuildNavItem>
|
||||
<NavSlug guild={guild || null} key={guild.id} />
|
||||
<Badges guild={guild} />
|
||||
</GuildNavItem>
|
||||
</Link>
|
||||
))}
|
||||
<ReactTooltip id={tooltipId} />
|
||||
<Scrollbars
|
||||
universal
|
||||
autoHide
|
||||
// autoHeight
|
||||
style={{ height: 'calc(100vh - 45px - 1.4em)', overflowX: 'hidden' }}
|
||||
>
|
||||
{sortBy(props.guilds, 'name', (a: string, b: string) =>
|
||||
a.toLowerCase() > b.toLowerCase() ? 1 : -1
|
||||
).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>
|
||||
);
|
||||
|
|
|
@ -11,9 +11,11 @@ type Props = {
|
|||
export const NavSlug = (props: Props) => (
|
||||
<SlugContainer>
|
||||
<Avatar
|
||||
hash={props.guild ? props.guild.icon : undefined}
|
||||
src={
|
||||
(props.guild && utils.avatarHash(props.guild.id, props.guild.icon)) ||
|
||||
undefined
|
||||
props.guild
|
||||
? utils.avatarHash(props.guild.id, props.guild.icon)
|
||||
: undefined
|
||||
}
|
||||
deliberatelyEmpty={!props.guild}
|
||||
size={35}
|
||||
|
|
|
@ -27,6 +27,7 @@ export const PreauthGreeting = (props: GreetingProps) => (
|
|||
'icons',
|
||||
512
|
||||
)}
|
||||
hash={props.guildSlug.icon}
|
||||
>
|
||||
{avatarUtils.initialsFromName(props.guildSlug.name)}
|
||||
</Avatar>
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import Link from 'next/link';
|
||||
import * as React from 'react';
|
||||
import { GoPencil } from 'react-icons/go';
|
||||
import { Guild } from 'roleypoly/common/types';
|
||||
import { guild } from 'roleypoly/common/types/storyData';
|
||||
import { GuildSlug } from 'roleypoly/common/types';
|
||||
import { Avatar, utils } from 'roleypoly/design-system/atoms/avatar';
|
||||
import { AccentTitle, AmbientLarge } from 'roleypoly/design-system/atoms/typography';
|
||||
import { Editable, Icon, Name, Wrapper } from './ServerMasthead.styled';
|
||||
|
||||
export type ServerMastheadProps = {
|
||||
guild: Guild;
|
||||
guild: GuildSlug;
|
||||
editable: boolean;
|
||||
};
|
||||
|
||||
|
@ -17,8 +16,9 @@ export const ServerMasthead = (props: ServerMastheadProps) => {
|
|||
<Wrapper>
|
||||
<Icon>
|
||||
<Avatar
|
||||
hash={props.guild.icon}
|
||||
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)}
|
||||
</Avatar>
|
||||
|
|
|
@ -19,6 +19,7 @@ export const UserAvatarGroup = (props: Props) => (
|
|||
</Collapse>
|
||||
<Avatar
|
||||
size={34}
|
||||
hash={props.user.avatar}
|
||||
src={utils.avatarHash(props.user.id, props.user.avatar, 'avatars')}
|
||||
>
|
||||
{utils.initialsFromName(props.user.username)}
|
||||
|
|
|
@ -62,6 +62,7 @@ export const Authed = (props: Props) => {
|
|||
canDefocus
|
||||
position="bottom left"
|
||||
active={serverPopoverState}
|
||||
preferredWidth={560}
|
||||
onExit={() => setServerPopoverState(false)}
|
||||
>
|
||||
{() => <GuildNav guilds={props.guilds} />}
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
import NextLink from 'next/link';
|
||||
import * as React from 'react';
|
||||
import { GoInfo } from 'react-icons/go';
|
||||
import {
|
||||
Category,
|
||||
CategoryType,
|
||||
Guild,
|
||||
GuildData,
|
||||
GuildSlug,
|
||||
Member,
|
||||
Role,
|
||||
} from 'roleypoly/common/types';
|
||||
import { ReactifyNewlines } from 'roleypoly/common/utils/ReactifyNewlines';
|
||||
import { sortBy } from 'roleypoly/common/utils/sortBy';
|
||||
import { FaderOpacity } from 'roleypoly/design-system/atoms/fader';
|
||||
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 { ResetSubmit } from 'roleypoly/design-system/molecules/reset-submit';
|
||||
import { ServerMasthead } from 'roleypoly/design-system/molecules/server-masthead';
|
||||
|
@ -23,7 +26,7 @@ import {
|
|||
} from './RolePicker.styled';
|
||||
|
||||
export type RolePickerProps = {
|
||||
guild: Guild;
|
||||
guild: GuildSlug;
|
||||
guildData: GuildData;
|
||||
member: Member;
|
||||
roles: Role[];
|
||||
|
@ -81,32 +84,34 @@ export const RolePicker = (props: RolePickerProps) => {
|
|||
{props.guildData.categories.length !== 0 ? (
|
||||
<>
|
||||
<div>
|
||||
{props.guildData.categories.map((category, idx) => (
|
||||
<CategoryContainer key={idx}>
|
||||
<PickerCategory
|
||||
key={idx}
|
||||
category={category}
|
||||
title={category.name}
|
||||
selectedRoles={selectedRoles.filter((roleId) =>
|
||||
category.roles.includes(roleId)
|
||||
)}
|
||||
roles={
|
||||
category.roles
|
||||
.map((role) =>
|
||||
props.roles.find((r) => r.id === role)
|
||||
)
|
||||
.filter((r) => r !== undefined) as Role[]
|
||||
}
|
||||
onChange={handleChange(category)}
|
||||
wikiMode={false}
|
||||
type={
|
||||
category.type === CategoryType.Single
|
||||
? 'single'
|
||||
: 'multi'
|
||||
}
|
||||
/>
|
||||
</CategoryContainer>
|
||||
))}
|
||||
{sortBy(props.guildData.categories, 'position').map(
|
||||
(category, idx) => (
|
||||
<CategoryContainer key={idx}>
|
||||
<PickerCategory
|
||||
key={idx}
|
||||
category={category}
|
||||
title={category.name}
|
||||
selectedRoles={selectedRoles.filter((roleId) =>
|
||||
category.roles.includes(roleId)
|
||||
)}
|
||||
roles={
|
||||
category.roles
|
||||
.map((role) =>
|
||||
props.roles.find((r) => r.id === role)
|
||||
)
|
||||
.filter((r) => r !== undefined) as Role[]
|
||||
}
|
||||
onChange={handleChange(category)}
|
||||
wikiMode={false}
|
||||
type={
|
||||
category.type === CategoryType.Single
|
||||
? 'single'
|
||||
: 'multi'
|
||||
}
|
||||
/>
|
||||
</CategoryContainer>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<FaderOpacity
|
||||
isVisible={!arrayMatches(selectedRoles, props.member.roles)}
|
||||
|
@ -126,6 +131,18 @@ export const RolePicker = (props: RolePickerProps) => {
|
|||
</InfoIcon>
|
||||
<div>
|
||||
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>
|
||||
</InfoBox>
|
||||
)}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import * as React from 'react';
|
||||
import { AppShell } from 'roleypoly/design-system/organisms/app-shell';
|
||||
import { Landing } from 'roleypoly/design-system/organisms/landing';
|
||||
import { ProvidableAppShellProps } from 'roleypoly/providers/appShellData';
|
||||
|
||||
export const LandingTemplate = () => (
|
||||
<AppShell showFooter>
|
||||
export const LandingTemplate = (props: ProvidableAppShellProps) => (
|
||||
<AppShell showFooter {...props}>
|
||||
<Landing />
|
||||
</AppShell>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
RolePickerProps,
|
||||
} from 'roleypoly/design-system/organisms/role-picker';
|
||||
|
||||
export type RolePickerTemplateProps = RolePickerProps & AppShellProps;
|
||||
export type RolePickerTemplateProps = RolePickerProps & Omit<AppShellProps, 'children'>;
|
||||
|
||||
export const RolePickerTemplate = (props: RolePickerTemplateProps) => {
|
||||
const { user, guilds, activeGuildId, ...pickerProps } = props;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import NextApp, { AppContext, AppProps } from 'next/app';
|
||||
import nookies from 'nookies';
|
||||
import { AppProps } from 'next/app';
|
||||
import * as React from 'react';
|
||||
import { InjectTypekitFont } from 'roleypoly/design-system/atoms/fonts';
|
||||
import { AuthProvider } from 'roleypoly/providers/auth/AuthContext';
|
||||
|
||||
type Props = AppProps & {
|
||||
sessionKey: string | null;
|
||||
|
@ -11,26 +9,7 @@ type Props = AppProps & {
|
|||
const App = (props: Props) => (
|
||||
<>
|
||||
<InjectTypekitFont />
|
||||
<AuthProvider sessionKey={props.sessionKey}>
|
||||
<props.Component {...props.pageProps} />
|
||||
</AuthProvider>
|
||||
<props.Component {...props.pageProps} />
|
||||
</>
|
||||
);
|
||||
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 };
|
||||
};
|
||||
|
|
|
@ -1,5 +1,20 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import * as React from 'react';
|
||||
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;
|
||||
|
|
|
@ -14,6 +14,7 @@ type Props = {
|
|||
const Logout = (props: Props) => {
|
||||
React.useEffect(() => {
|
||||
sessionStorage.removeItem('session_key');
|
||||
sessionStorage.removeItem('session_data');
|
||||
location.href = '/';
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { NextPageContext } from 'next';
|
||||
import getConfig from 'next/config';
|
||||
import nookies from 'nookies';
|
||||
import * as React from 'react';
|
||||
import { Hero } from 'roleypoly/design-system/atoms/hero';
|
||||
|
@ -7,15 +8,18 @@ import { AppShell } from 'roleypoly/design-system/organisms/app-shell';
|
|||
|
||||
type Props = {
|
||||
sessionID: string;
|
||||
apiURI: string;
|
||||
};
|
||||
|
||||
const NewSession = (props: Props) => {
|
||||
const { sessionID } = props;
|
||||
const { sessionID, apiURI } = props;
|
||||
|
||||
React.useEffect(() => {
|
||||
sessionStorage.setItem('session_key', sessionID);
|
||||
localStorage.setItem('api_uri', apiURI); // TODO: set better
|
||||
|
||||
location.href = '/';
|
||||
}, [sessionID]);
|
||||
}, [sessionID, apiURI]);
|
||||
|
||||
return (
|
||||
<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;
|
||||
if (!sessionID) {
|
||||
throw new Error("I shouldn't be here today.");
|
||||
|
@ -39,7 +47,7 @@ export const getServerSideProps = (context: NextPageContext): { props: Props } =
|
|||
sameSite: 'strict',
|
||||
});
|
||||
|
||||
return { props: { sessionID } };
|
||||
return { props: { sessionID, apiURI } };
|
||||
};
|
||||
|
||||
export default NewSession;
|
||||
|
|
59
src/pages/s/[id].tsx
Normal file
59
src/pages/s/[id].tsx
Normal 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
10
src/pages/s/[id]/edit.tsx
Normal 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
1
src/pages/servers.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export default () => <div></div>;
|
27
src/providers/appShellData.tsx
Normal file
27
src/providers/appShellData.tsx
Normal 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,
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
export * from './AuthContext';
|
12
yarn.lock
12
yarn.lock
|
@ -5603,6 +5603,11 @@ depd@~1.1.2:
|
|||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
|
||||
|
|
Loading…
Add table
Reference in a new issue