basic items page, with user permissioning :)

(the permissioning happens on the backend in the prev change! but yeah we send the auth token in the headers, so the backend knows who you are and whether to show you private data)

(also it is just owned items not in any list!)
This commit is contained in:
Matchu 2020-09-04 05:59:35 -07:00
parent e2b5486168
commit 70d3b06742
5 changed files with 163 additions and 27 deletions

View file

@ -5,11 +5,13 @@ import { CSSReset, ChakraProvider } from "@chakra-ui/core";
import defaultTheme from "@chakra-ui/theme"; import defaultTheme from "@chakra-ui/theme";
import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import loadable from "@loadable/component"; import loadable from "@loadable/component";
import { useAuth0 } from "@auth0/auth0-react";
import apolloClient from "./apolloClient"; import buildApolloClient from "./apolloClient";
const WardrobePage = loadable(() => import("./WardrobePage")); const ItemsPage = loadable(() => import("./ItemsPage"));
const HomePage = loadable(() => import("./HomePage")); const HomePage = loadable(() => import("./HomePage"));
const WardrobePage = loadable(() => import("./WardrobePage"));
const theme = { const theme = {
...defaultTheme, ...defaultTheme,
@ -39,22 +41,40 @@ function App() {
audience="https://impress-2020.openneo.net/api" audience="https://impress-2020.openneo.net/api"
scope="" scope=""
> >
<ApolloProvider client={apolloClient}> <ApolloProviderWithAuth0>
<ChakraProvider theme={theme}> <ChakraProvider theme={theme}>
<CSSReset /> <CSSReset />
<Switch> <Switch>
<Route path="/outfits/new"> <Route path="/outfits/new">
<WardrobePage /> <WardrobePage />
</Route> </Route>
<Route path="/user/:userId/items">
<ItemsPage />
</Route>
<Route path="/"> <Route path="/">
<HomePage /> <HomePage />
</Route> </Route>
</Switch> </Switch>
</ChakraProvider> </ChakraProvider>
</ApolloProvider> </ApolloProviderWithAuth0>
</Auth0Provider> </Auth0Provider>
</Router> </Router>
); );
} }
function ApolloProviderWithAuth0({ children }) {
const auth0 = useAuth0();
const auth0Ref = React.useRef(auth0);
React.useEffect(() => {
auth0Ref.current = auth0;
}, [auth0]);
const client = React.useMemo(
() => buildApolloClient(() => auth0Ref.current),
[]
);
return <ApolloProvider client={client}>{children}</ApolloProvider>;
}
export default App; export default App;

View file

@ -13,12 +13,13 @@ import {
useToast, useToast,
} from "@chakra-ui/core"; } from "@chakra-ui/core";
import { MoonIcon, SunIcon } from "@chakra-ui/icons"; import { MoonIcon, SunIcon } from "@chakra-ui/icons";
import { useHistory, useLocation } from "react-router-dom"; import { Link, useHistory, useLocation } from "react-router-dom";
import { useLazyQuery } from "@apollo/client"; import { useLazyQuery } from "@apollo/client";
import { useAuth0 } from "@auth0/auth0-react"; import { useAuth0 } from "@auth0/auth0-react";
import { Heading1, usePageTitle } from "./util"; import { Heading1, usePageTitle } from "./util";
import OutfitPreview from "./components/OutfitPreview"; import OutfitPreview from "./components/OutfitPreview";
import useCurrentUser from "./components/useCurrentUser";
import HomepageSplashImg from "../images/homepage-splash.png"; import HomepageSplashImg from "../images/homepage-splash.png";
import HomepageSplashImg2x from "../images/homepage-splash@2x.png"; import HomepageSplashImg2x from "../images/homepage-splash@2x.png";
@ -76,32 +77,21 @@ function HomePage() {
} }
function UserLoginLogout() { function UserLoginLogout() {
const { const { isLoading, isAuthenticated, loginWithRedirect, logout } = useAuth0();
isLoading, const { id, username } = useCurrentUser();
user,
isAuthenticated,
loginWithRedirect,
logout,
} = useAuth0();
if (isLoading) { if (isLoading) {
return null; return null;
} }
if (isAuthenticated) { if (isAuthenticated) {
// NOTE: Users created correctly should have these attributes... but I'm
// coding defensively, because third-party integrations are always a
// bit of a thing, and I don't want failures to crash us!
const username = user["https://oauth.impress-2020.openneo.net/username"];
const id = user.sub?.match(/^auth0\|impress-([0-9]+)$/)?.[1];
return ( return (
<Box display="flex" alignItems="center"> <Box display="flex" alignItems="center">
{username && <Box fontSize="sm">Hi, {username}!</Box>} {username && <Box fontSize="sm">Hi, {username}!</Box>}
{id && ( {id && (
<Button <Button
as="a" as={Link}
href={`https://impress.openneo.net/user/${id}-${username}/closet`} to={`/user/${id}/items`}
size="sm" size="sm"
variant="outline" variant="outline"
marginLeft="2" marginLeft="2"

73
src/app/ItemsPage.js Normal file
View file

@ -0,0 +1,73 @@
import React from "react";
import { Box, Image, Wrap } from "@chakra-ui/core";
import gql from "graphql-tag";
import { useParams } from "react-router-dom";
import { useQuery } from "@apollo/client";
import HangerSpinner from "./components/HangerSpinner";
import { Heading1 } from "./util";
import useCurrentUser from "./components/useCurrentUser";
function ItemsPage() {
const { userId } = useParams();
const currentUser = useCurrentUser();
const isCurrentUser = currentUser.id === userId;
const { loading, error, data } = useQuery(
gql`
query ItemsPage($userId: ID!) {
user(id: $userId) {
id
username
itemsTheyOwn {
id
name
thumbnailUrl
}
}
}
`,
{ variables: { userId } }
);
if (loading) {
return (
<Box padding="6" display="flex" justifyContent="center">
<HangerSpinner boxSize="48px" />
</Box>
);
}
if (error) {
return (
<Box padding="6" color="red.400">
{error.message}
</Box>
);
}
return (
<Box padding="6" maxWidth="800px" margin="0 auto">
<Heading1 marginBottom="6">
{isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`}
</Heading1>
<Wrap justify="center">
{data.user.itemsTheyOwn.map((item) => (
<Box key={item.id} width="100px" textAlign="center">
<Image
src={item.thumbnailUrl}
alt=""
height="80px"
width="80px"
boxShadow="md"
margin="0 auto"
/>
{item.name}
</Box>
))}
</Wrap>
</Box>
);
}
export default ItemsPage;

View file

@ -1,4 +1,5 @@
import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { createPersistedQueryLink } from "apollo-link-persisted-queries"; import { createPersistedQueryLink } from "apollo-link-persisted-queries";
import gql from "graphql-tag"; import gql from "graphql-tag";
@ -55,7 +56,7 @@ const typePolicies = {
// don't love escape-hatching to the client like this, but... // don't love escape-hatching to the client like this, but...
let cachedData; let cachedData;
try { try {
cachedData = client.readQuery({ cachedData = hackyEscapeHatchClient.readQuery({
query: gql` query: gql`
query CacheLookupForItemAppearanceReader( query CacheLookupForItemAppearanceReader(
$speciesId: ID! $speciesId: ID!
@ -105,6 +106,32 @@ const persistedQueryLink = createPersistedQueryLink({
useGETForHashedQueries: true, useGETForHashedQueries: true,
}); });
const httpLink = createHttpLink({ uri: "/api/graphql" }); const httpLink = createHttpLink({ uri: "/api/graphql" });
const buildAuthLink = (getAuth0) =>
setContext(async (_, { headers }) => {
// Wait for auth0 to stop loading, so we can maybe get a token! We'll do
// this hackily by checking every 100ms until it's true.
await new Promise((resolve) => {
function check() {
if (getAuth0().isLoading) {
setTimeout(check, 100);
} else {
resolve();
}
}
check();
});
const { isAuthenticated, getAccessTokenSilently } = getAuth0();
if (isAuthenticated) {
const token = await getAccessTokenSilently();
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
};
}
});
const initialCache = {}; const initialCache = {};
for (const zone of cachedZones) { for (const zone of cachedZones) {
@ -115,10 +142,17 @@ for (const zone of cachedZones) {
* apolloClient is the global Apollo Client instance we use for GraphQL * apolloClient is the global Apollo Client instance we use for GraphQL
* queries. This is how we communicate with the server! * queries. This is how we communicate with the server!
*/ */
let hackyEscapeHatchClient = null;
const buildClient = (getAuth0) => {
const client = new ApolloClient({ const client = new ApolloClient({
link: persistedQueryLink.concat(httpLink), link: buildAuthLink(getAuth0).concat(persistedQueryLink).concat(httpLink),
cache: new InMemoryCache({ typePolicies }).restore(initialCache), cache: new InMemoryCache({ typePolicies }).restore(initialCache),
connectToDevTools: true, connectToDevTools: true,
}); });
export default client; hackyEscapeHatchClient = client;
return client;
};
export default buildClient;

View file

@ -0,0 +1,19 @@
import { useAuth0 } from "@auth0/auth0-react";
function useCurrentUser() {
const { isLoading, isAuthenticated, user } = useAuth0();
if (isLoading || !isAuthenticated) {
return { id: null, username: null };
}
// NOTE: Users created correctly should have these attributes... but I'm
// coding defensively, because third-party integrations are always a
// bit of a thing, and I don't want failures to crash us!
const id = user.sub?.match(/^auth0\|impress-([0-9]+)$/)?.[1];
const username = user["https://oauth.impress-2020.openneo.net/username"];
return { id, username };
}
export default useCurrentUser;