diff --git a/packages/design-system/molecules/guild-nav/GuildNav.tsx b/packages/design-system/molecules/guild-nav/GuildNav.tsx
index c6b36a4..0e18460 100644
--- a/packages/design-system/molecules/guild-nav/GuildNav.tsx
+++ b/packages/design-system/molecules/guild-nav/GuildNav.tsx
@@ -1,5 +1,5 @@
import { NavSlug } from '@roleypoly/design-system/molecules/nav-slug';
-import { sortBy } from '@roleypoly/misc-utils/sortBy';
+import { getRecentAndSortedGuilds } from '@roleypoly/misc-utils/guildListing';
import { GuildSlug, UserGuildPermissions } from '@roleypoly/types';
import * as React from 'react';
import Scrollbars from 'react-custom-scrollbars';
@@ -9,6 +9,7 @@ import { GuildNavItem } from './GuildNav.styled';
type Props = {
guilds: GuildSlug[];
+ recentGuilds: string[];
};
const tooltipId = 'guildnav';
@@ -27,23 +28,41 @@ const Badges = (props: { guild: GuildSlug }) => {
}, [props.guild.permissionLevel]);
};
-export const GuildNav = (props: Props) => (
-
-
- {sortBy(props.guilds, 'name', (a: string, b: string) =>
- a.toLowerCase() > b.toLowerCase() ? 1 : -1
- ).map((guild) => (
-
-
-
-
- ))}
-
-
-
+const NavList = (props: { guilds: Props['guilds'] }) => (
+ <>
+ {props.guilds.map((guild) => (
+
+
+
+
+ ))}
+ >
);
+
+export const GuildNav = (props: Props) => {
+ const { sortedGuildSlugs, recentGuildSlugs } = getRecentAndSortedGuilds(
+ props.guilds,
+ props.recentGuilds
+ );
+
+ return (
+
+
+ {recentGuildSlugs && (
+ <>
+ Recents
+
+ All Guilds
+ >
+ )}
+
+
+
+
+ );
+};
diff --git a/packages/design-system/organisms/app-shell/AppShell.tsx b/packages/design-system/organisms/app-shell/AppShell.tsx
index c724e4e..7154d57 100644
--- a/packages/design-system/organisms/app-shell/AppShell.tsx
+++ b/packages/design-system/organisms/app-shell/AppShell.tsx
@@ -13,6 +13,7 @@ export type AppShellProps = {
small?: boolean;
activeGuildId?: string | null;
guilds?: GuildSlug[];
+ recentGuilds?: string[];
disableGuildPicker?: boolean;
};
@@ -26,6 +27,7 @@ export const AppShell = (props: AppShellProps) => (
guilds={props.guilds || []}
activeGuildId={props.activeGuildId || null}
user={props.user}
+ recentGuilds={props.recentGuilds || []}
/>
) : (
diff --git a/packages/design-system/organisms/masthead/Authed.tsx b/packages/design-system/organisms/masthead/Authed.tsx
index bb21c09..857d340 100644
--- a/packages/design-system/organisms/masthead/Authed.tsx
+++ b/packages/design-system/organisms/masthead/Authed.tsx
@@ -22,6 +22,7 @@ type Props = {
activeGuildId: string | null;
guilds: GuildSlug[];
disableGuildPicker?: boolean;
+ recentGuilds: string[];
};
export const Authed = (props: Props) => {
@@ -65,7 +66,12 @@ export const Authed = (props: Props) => {
preferredWidth={560}
onExit={() => setServerPopoverState(false)}
>
- {() => }
+ {() => (
+
+ )}
diff --git a/packages/design-system/organisms/servers-listing/ServersListing.styled.ts b/packages/design-system/organisms/servers-listing/ServersListing.styled.ts
index 53cbe65..2982480 100644
--- a/packages/design-system/organisms/servers-listing/ServersListing.styled.ts
+++ b/packages/design-system/organisms/servers-listing/ServersListing.styled.ts
@@ -21,3 +21,7 @@ export const CardContainer = styled.div`
max-width: 30%;
`)}
`;
+
+export const SectionHead = styled.div`
+ flex: 1 1 100%;
+`;
diff --git a/packages/design-system/organisms/servers-listing/ServersListing.tsx b/packages/design-system/organisms/servers-listing/ServersListing.tsx
index 62c9bc1..d1de4b5 100644
--- a/packages/design-system/organisms/servers-listing/ServersListing.tsx
+++ b/packages/design-system/organisms/servers-listing/ServersListing.tsx
@@ -1,27 +1,45 @@
import { CompletelyStylelessLink } from '@roleypoly/design-system/atoms/typography';
import { ServerListingCard } from '@roleypoly/design-system/molecules/server-listing-card';
-import { sortBy } from '@roleypoly/misc-utils/sortBy';
+import { getRecentAndSortedGuilds } from '@roleypoly/misc-utils/guildListing';
import { GuildSlug } from '@roleypoly/types';
import * as React from 'react';
-import { CardContainer, ContentContainer } from './ServersListing.styled';
+import { CardContainer, ContentContainer, SectionHead } from './ServersListing.styled';
type ServersListingProps = {
guilds: GuildSlug[];
+ recentGuilds: string[];
};
-export const ServersListing = (props: ServersListingProps) => (
-
- {props.guilds &&
- sortBy(props.guilds, 'name', (a: string, b: string) =>
- a.toLowerCase() > b.toLowerCase() ? 1 : -1
- ).map((guild, idx) => (
-
-
-
-
-
-
-
- ))}
-
+const CardList = (props: { guilds: GuildSlug[] }) => (
+ <>
+ {props.guilds.map((guild, idx) => (
+
+
+
+
+
+
+
+ ))}
+ >
);
+
+export const ServersListing = (props: ServersListingProps) => {
+ const { recentGuildSlugs, sortedGuildSlugs } = getRecentAndSortedGuilds(
+ props.guilds,
+ props.recentGuilds
+ );
+
+ return (
+
+ {recentGuildSlugs.length !== 0 && (
+ <>
+ Recent Guilds
+
+ All Guilds
+ >
+ )}
+
+
+ );
+};
diff --git a/packages/design-system/templates/role-picker/RolePicker.tsx b/packages/design-system/templates/role-picker/RolePicker.tsx
index cec5674..ebf3012 100644
--- a/packages/design-system/templates/role-picker/RolePicker.tsx
+++ b/packages/design-system/templates/role-picker/RolePicker.tsx
@@ -8,9 +8,15 @@ import * as React from 'react';
export type RolePickerTemplateProps = RolePickerProps & Omit;
export const RolePickerTemplate = (props: RolePickerTemplateProps) => {
- const { user, guilds, activeGuildId, ...pickerProps } = props;
+ const { user, guilds, activeGuildId, recentGuilds, ...pickerProps } = props;
return (
-
+
);
diff --git a/packages/design-system/templates/servers/Servers.tsx b/packages/design-system/templates/servers/Servers.tsx
index 5f8f7e7..71b6574 100644
--- a/packages/design-system/templates/servers/Servers.tsx
+++ b/packages/design-system/templates/servers/Servers.tsx
@@ -1,14 +1,14 @@
import { AppShell, AppShellProps } from '@roleypoly/design-system/organisms/app-shell';
import { ServersListing } from '@roleypoly/design-system/organisms/servers-listing/ServersListing';
-import { GuildSlug } from '@roleypoly/types';
import * as React from 'react';
-type ServerTemplateProps = Omit & {
- guilds: GuildSlug[];
-};
+type ServerTemplateProps = Omit;
export const ServersTemplate = (props: ServerTemplateProps) => (
-
+
);
diff --git a/packages/misc-utils/guildListing.ts b/packages/misc-utils/guildListing.ts
new file mode 100644
index 0000000..e838472
--- /dev/null
+++ b/packages/misc-utils/guildListing.ts
@@ -0,0 +1,26 @@
+import { sortBy } from '@roleypoly/misc-utils/sortBy';
+import { GuildSlug } from '@roleypoly/types';
+
+type RecentAndSortedT = {
+ recentGuildSlugs: GuildSlug[];
+ sortedGuildSlugs: GuildSlug[];
+};
+
+export const getRecentAndSortedGuilds = (
+ guilds: GuildSlug[],
+ recentGuilds: string[]
+): RecentAndSortedT => {
+ return {
+ recentGuildSlugs: recentGuilds.reduce((acc, id) => {
+ const guild = guilds.find((guild) => guild.id === id);
+ if (guild) {
+ acc.push(guild);
+ }
+
+ return acc;
+ }, []),
+ sortedGuildSlugs: sortBy(guilds, 'name', (a: string, b: string) =>
+ a.toLowerCase() > b.toLowerCase() ? 1 : -1
+ ),
+ };
+};
diff --git a/packages/web/src/api-context/ApiContext.spec.tsx b/packages/web/src/contexts/api/ApiContext.spec.tsx
similarity index 100%
rename from packages/web/src/api-context/ApiContext.spec.tsx
rename to packages/web/src/contexts/api/ApiContext.spec.tsx
diff --git a/packages/web/src/api-context/ApiContext.tsx b/packages/web/src/contexts/api/ApiContext.tsx
similarity index 100%
rename from packages/web/src/api-context/ApiContext.tsx
rename to packages/web/src/contexts/api/ApiContext.tsx
diff --git a/packages/web/src/api-context/getDefaultApiUrl.spec.ts b/packages/web/src/contexts/api/getDefaultApiUrl.spec.ts
similarity index 100%
rename from packages/web/src/api-context/getDefaultApiUrl.spec.ts
rename to packages/web/src/contexts/api/getDefaultApiUrl.spec.ts
diff --git a/packages/web/src/api-context/getDefaultApiUrl.ts b/packages/web/src/contexts/api/getDefaultApiUrl.ts
similarity index 100%
rename from packages/web/src/api-context/getDefaultApiUrl.ts
rename to packages/web/src/contexts/api/getDefaultApiUrl.ts
diff --git a/packages/web/src/contexts/app-shell/AppShellContext.tsx b/packages/web/src/contexts/app-shell/AppShellContext.tsx
new file mode 100644
index 0000000..9a18ed0
--- /dev/null
+++ b/packages/web/src/contexts/app-shell/AppShellContext.tsx
@@ -0,0 +1,41 @@
+import { AppShellProps } from '@roleypoly/design-system/organisms/app-shell';
+import * as React from 'react';
+import { useRecentGuilds } from '../recent-guilds/RecentGuildsContext';
+import { useSessionContext } from '../session/SessionContext';
+
+type AppShellPropsT =
+ | {
+ user: Required;
+ guilds: Required;
+ recentGuilds: Required;
+ }
+ | {
+ user: undefined;
+ guilds: undefined;
+ recentGuilds: [];
+ };
+
+export const AppShellPropsContext = React.createContext({
+ user: undefined,
+ guilds: undefined,
+ recentGuilds: [],
+});
+
+export const useAppShellProps = () => React.useContext(AppShellPropsContext);
+
+export const AppShellPropsProvider = (props: { children: React.ReactNode }) => {
+ const { session } = useSessionContext();
+ const { recentGuilds } = useRecentGuilds();
+
+ const appShellProps: AppShellPropsT = {
+ user: session?.user,
+ guilds: session?.guilds,
+ recentGuilds,
+ };
+
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/packages/web/src/contexts/recent-guilds/RecentGuildsContext.tsx b/packages/web/src/contexts/recent-guilds/RecentGuildsContext.tsx
new file mode 100644
index 0000000..3319e50
--- /dev/null
+++ b/packages/web/src/contexts/recent-guilds/RecentGuildsContext.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+
+type RecentGuildsT = {
+ recentGuilds: string[];
+ pushRecentGuild: (id: string) => void;
+};
+
+export const RecentGuilds = React.createContext({
+ recentGuilds: [],
+ pushRecentGuild: () => {},
+});
+
+export const useRecentGuilds = () => React.useContext(RecentGuilds);
+
+const saveState = (state: string[]) => {
+ localStorage.setItem('rp_recent_guilds', JSON.stringify(state));
+};
+
+const pullState = (): string[] => {
+ const rawState = localStorage.getItem('rp_recent_guilds');
+ if (!rawState) {
+ return [];
+ }
+
+ try {
+ return JSON.parse(rawState);
+ } catch (e) {
+ console.warn('RecentGuilds failed to re-hydrate saved state', e);
+ return [];
+ }
+};
+
+export const RecentGuildsProvider = (props: { children: React.ReactNode }) => {
+ const [recentGuilds, setRecentGuilds] = React.useState(pullState());
+
+ const recentGuildsData: RecentGuildsT = {
+ recentGuilds,
+ pushRecentGuild: (id: string) => {
+ const nextState = [
+ id,
+ ...recentGuilds.slice(0, 19).filter((guild) => guild !== id),
+ ];
+
+ if (recentGuilds[0] !== id) {
+ setRecentGuilds(nextState);
+ }
+ },
+ };
+
+ React.useEffect(() => {
+ saveState(recentGuilds);
+ }, [recentGuilds]);
+
+ return (
+
+ {props.children}
+
+ );
+};
diff --git a/packages/web/src/session-context/SessionContext.tsx b/packages/web/src/contexts/session/SessionContext.tsx
similarity index 98%
rename from packages/web/src/session-context/SessionContext.tsx
rename to packages/web/src/contexts/session/SessionContext.tsx
index 511458f..ebedab4 100644
--- a/packages/web/src/session-context/SessionContext.tsx
+++ b/packages/web/src/contexts/session/SessionContext.tsx
@@ -1,6 +1,6 @@
import { SessionData } from '@roleypoly/types';
import * as React from 'react';
-import { useApiContext } from '../api-context/ApiContext';
+import { useApiContext } from '../api/ApiContext';
type SessionContextT = {
session?: Omit, 'tokens'>;
diff --git a/packages/web/src/index.tsx b/packages/web/src/index.tsx
index bcaef66..1b9b869 100644
--- a/packages/web/src/index.tsx
+++ b/packages/web/src/index.tsx
@@ -1,16 +1,33 @@
import React from 'react';
import ReactDOM from 'react-dom';
-import { ApiContextProvider } from './api-context/ApiContext';
import { AppRouter } from './app-router/AppRouter';
-import { SessionContextProvider } from './session-context/SessionContext';
+import { ApiContextProvider } from './contexts/api/ApiContext';
+import { AppShellPropsProvider } from './contexts/app-shell/AppShellContext';
+import { RecentGuildsProvider } from './contexts/recent-guilds/RecentGuildsContext';
+import { SessionContextProvider } from './contexts/session/SessionContext';
+
+const ProviderProvider = (props: {
+ providerChain: typeof ApiContextProvider[];
+ children: React.ReactNode;
+}) => {
+ return props.providerChain.reduceRight(
+ (acc, Provider) => {acc},
+ <>{props.children}>
+ );
+};
ReactDOM.render(
-
-
-
-
-
+
+
+
,
document.getElementById('root')
);
diff --git a/packages/web/src/pages/auth/login.tsx b/packages/web/src/pages/auth/login.tsx
index 408910e..0980dda 100644
--- a/packages/web/src/pages/auth/login.tsx
+++ b/packages/web/src/pages/auth/login.tsx
@@ -1,7 +1,7 @@
import { AuthLogin } from '@roleypoly/design-system/templates/auth-login';
import { GuildSlug } from '@roleypoly/types';
import React from 'react';
-import { useApiContext } from '../../api-context/ApiContext';
+import { useApiContext } from '../../contexts/api/ApiContext';
const Login = () => {
const { apiUrl, fetch } = useApiContext();
diff --git a/packages/web/src/pages/dev-tools/session-debug.tsx b/packages/web/src/pages/dev-tools/session-debug.tsx
index 8c6621a..53d30d2 100644
--- a/packages/web/src/pages/dev-tools/session-debug.tsx
+++ b/packages/web/src/pages/dev-tools/session-debug.tsx
@@ -1,5 +1,5 @@
-import { useApiContext } from '../../api-context/ApiContext';
-import { useSessionContext } from '../../session-context/SessionContext';
+import { useApiContext } from '../../contexts/api/ApiContext';
+import { useSessionContext } from '../../contexts/session/SessionContext';
const SessionDebug = () => {
const session = useSessionContext();
diff --git a/packages/web/src/pages/dev-tools/set-api.tsx b/packages/web/src/pages/dev-tools/set-api.tsx
index a7d817c..afce059 100644
--- a/packages/web/src/pages/dev-tools/set-api.tsx
+++ b/packages/web/src/pages/dev-tools/set-api.tsx
@@ -1,6 +1,6 @@
import { navigate } from '@reach/router';
import * as React from 'react';
-import { useApiContext } from '../../api-context/ApiContext';
+import { useApiContext } from '../../contexts/api/ApiContext';
const SetApi = () => {
const apiContext = useApiContext();
diff --git a/packages/web/src/pages/landing.tsx b/packages/web/src/pages/landing.tsx
index b5b3d86..eb8603d 100644
--- a/packages/web/src/pages/landing.tsx
+++ b/packages/web/src/pages/landing.tsx
@@ -1,16 +1,18 @@
import { Redirect } from '@reach/router';
import { LandingTemplate } from '@roleypoly/design-system/templates/landing';
import * as React from 'react';
-import { useSessionContext } from '../session-context/SessionContext';
+import { useAppShellProps } from '../contexts/app-shell/AppShellContext';
+import { useSessionContext } from '../contexts/session/SessionContext';
const Landing = () => {
const { isAuthenticated } = useSessionContext();
+ const appShellProps = useAppShellProps();
if (isAuthenticated) {
return ;
}
- return ;
+ return ;
};
export default Landing;
diff --git a/packages/web/src/pages/picker.tsx b/packages/web/src/pages/picker.tsx
index 2368c2f..680a0e9 100644
--- a/packages/web/src/pages/picker.tsx
+++ b/packages/web/src/pages/picker.tsx
@@ -1,8 +1,11 @@
import { Redirect } from '@reach/router';
import { RolePickerTemplate } from '@roleypoly/design-system/templates/role-picker';
+import { ServerSetupTemplate } from '@roleypoly/design-system/templates/server-setup';
import { PresentableGuild, RoleUpdate, UserGuildPermissions } from '@roleypoly/types';
import * as React from 'react';
-import { useSessionContext } from '../session-context/SessionContext';
+import { useAppShellProps } from '../contexts/app-shell/AppShellContext';
+import { useRecentGuilds } from '../contexts/recent-guilds/RecentGuildsContext';
+import { useSessionContext } from '../contexts/session/SessionContext';
import { makeRoleTransactions } from '../utils/roleTransactions';
type PickerProps = {
@@ -11,8 +14,12 @@ type PickerProps = {
const Picker = (props: PickerProps) => {
const { session, authedFetch, isAuthenticated } = useSessionContext();
+ const { pushRecentGuild } = useRecentGuilds();
+ const appShellProps = useAppShellProps();
- const [pickerData, setPickerData] = React.useState(null);
+ const [pickerData, setPickerData] = React.useState(
+ null
+ );
const [pending, setPending] = React.useState(false);
React.useEffect(() => {
@@ -20,11 +27,20 @@ const Picker = (props: PickerProps) => {
const response = await authedFetch(`/get-picker-data/${props.serverID}`);
const data = await response.json();
+ if (response.status !== 200) {
+ setPickerData(false);
+ return;
+ }
+
setPickerData(data);
};
fetchPickerData();
- }, [props.serverID, authedFetch]);
+ }, [props.serverID, authedFetch, pushRecentGuild]);
+
+ React.useCallback((serverID) => pushRecentGuild(serverID), [pushRecentGuild])(
+ props.serverID
+ );
if (!isAuthenticated) {
return ;
@@ -34,6 +50,25 @@ const Picker = (props: PickerProps) => {
return Loading...
;
}
+ if (pickerData === false) {
+ if (session && session.user && session.guilds) {
+ const guildSlug = session.guilds.find((guild) => guild.id === props.serverID);
+ if (!guildSlug) {
+ throw new Error('placeholder: guild not found in user slugs, 404');
+ }
+
+ return (
+
+ );
+ }
+
+ throw new Error('placeholder: session state is odd, 404');
+ }
+
const onSubmit = async (submittedRoles: string[]) => {
if (pending === true) {
return;
@@ -62,8 +97,7 @@ const Picker = (props: PickerProps) => {
return (
{
const { isAuthenticated, session } = useSessionContext();
+ const appShellProps = useAppShellProps();
if (!isAuthenticated || !session) {
return ;
}
- return ;
+ return ;
};
export default ServersPage;