Skip to content

Commit b263080

Browse files
authored
Merge pull request #27 from howtographql/tutorial-saving-mutation
Tutorial saving mutation
2 parents 23f84cb + 3cc1e30 commit b263080

File tree

9 files changed

+305
-124
lines changed

9 files changed

+305
-124
lines changed

packages/gatsby-theme/src/components/TutorialListing.tsx

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import * as React from 'react';
2-
import { Heading, Text, Card, Flex, Box } from './shared/base';
2+
import { Heading, Text, Card, Flex, Box, Button } from './shared/base';
33
import { getTutorialOverviewSlug } from '../utils/getTutorialSlug';
44
import Upvote from './Upvote';
55
import { Link } from 'gatsby';
66
import { Query, Mutation } from 'react-apollo';
77
import gql from 'graphql-tag';
88
import { optionalChaining } from '../utils/helpers';
9+
import { loginUser } from '../utils/auth';
10+
import { handleMutationResponse, ApiErrors } from '../utils/errorHandling';
911

1012
type TutorialListingProps = {
1113
tutorial: Tutorial;
@@ -25,7 +27,7 @@ type FrontMatter = {
2527
const TutorialListing: React.FunctionComponent<TutorialListingProps> = ({
2628
tutorial,
2729
}) => {
28-
const tutorialId = "cjwb6f2hy7e4f0b14bxh1mar2";
30+
const tutorialId = 'cjwb6f2hy7e4f0b14bxh1mar2';
2931
return (
3032
<Query
3133
query={gql`
@@ -67,17 +69,68 @@ const TutorialListing: React.FunctionComponent<TutorialListingProps> = ({
6769
}
6870
`}
6971
variables={{
70-
id: tutorialId
72+
id: tutorialId,
7173
}}
7274
>
73-
{(upvote) => {
75+
{upvote => {
7476
return (
7577
<Upvote
76-
onClick={() => upvote()}
77-
active={optionalChaining(() => data.tutorial.viewerUserTutorial.upvoted)}
78+
onClick={async () => {
79+
const mutationRes = await handleMutationResponse(
80+
upvote(),
81+
);
82+
if (mutationRes.error) {
83+
if (mutationRes.error === ApiErrors.AUTHORIZATION) {
84+
loginUser();
85+
} else {
86+
console.log(mutationRes.error);
87+
}
88+
}
89+
}}
90+
active={optionalChaining(
91+
() => data.tutorial.viewerUserTutorial.upvoted,
92+
)}
7893
count={optionalChaining(() => data.tutorial.upvotes)}
7994
/>
80-
)
95+
);
96+
}}
97+
</Mutation>
98+
<Mutation
99+
mutation={gql`
100+
mutation SaveTutorial($id: ID!) {
101+
saveTutorial(tutorialId: $id) {
102+
code
103+
success
104+
userTutorial {
105+
id
106+
saved
107+
}
108+
}
109+
}
110+
`}
111+
variables={{
112+
id: tutorialId,
113+
}}
114+
>
115+
{save => {
116+
return (
117+
<Button
118+
onClick={async () => {
119+
const mutationRes = await handleMutationResponse(
120+
save(),
121+
);
122+
if (mutationRes.error) {
123+
if (mutationRes.error === ApiErrors.AUTHORIZATION) {
124+
loginUser();
125+
} else {
126+
console.log(mutationRes.error);
127+
}
128+
}
129+
}}
130+
>
131+
Save
132+
</Button>
133+
);
81134
}}
82135
</Mutation>
83136
</Box>
@@ -89,7 +142,7 @@ const TutorialListing: React.FunctionComponent<TutorialListingProps> = ({
89142
</Box>
90143
</Flex>
91144
</Card>
92-
)
145+
);
93146
}}
94147
</Query>
95148
);

packages/gatsby-theme/src/components/Upvote.tsx

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,6 @@
11
import * as React from 'react';
22
import { Heading, Flex } from './shared/base';
33
import { VoteButton } from './buttons';
4-
import { loginUser } from '../utils/auth/';
5-
import WithCurrentUser from '../utils/auth/WithCurrentUser';
6-
7-
// Still to-do:
8-
// Query the backend with the ID of the tutorial to see whcih tutorial was upvoted
9-
// Create a way to store which user has upvoted the tutorial so that they can only
10-
// upvote it once
11-
12-
// const Upvote = ({active}) => {
13-
// return (
14-
// <WithCurrentUser>
15-
// {({ user }) => {
16-
// if (user) {
17-
// return <UpvoteData active={active} event={() => console.log('upvoted!')} />;
18-
// }
19-
// return <UpvoteData active={active} event={() => loginUser()} />;
20-
// }}
21-
// </WithCurrentUser>
22-
// );
23-
// };
244

255
type UpvoteDataProps = {
266
onClick: () => any;
@@ -30,7 +10,11 @@ type UpvoteDataProps = {
3010

3111
// place holder until we have a backend that stores the number of upvotes
3212
// and can keep track of which tutorials a user upvotes
33-
const Upvote: React.FunctionComponent<UpvoteDataProps> = ({ onClick: event, active, count = "..." }) => {
13+
const Upvote: React.FunctionComponent<UpvoteDataProps> = ({
14+
onClick: event,
15+
active,
16+
count = '...',
17+
}) => {
3418
return (
3519
<Flex flexDirection="column" alignItems="center" justifyContent="center">
3620
<VoteButton active={active} onClick={event} />

packages/gatsby-theme/src/pages/profile.tsx

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,27 @@
11
import * as React from 'react';
22
import Layout from '../components/layout';
3-
import { Text, Image, Flex } from '../components/shared/base';
3+
import { Heading, Text, Image, Flex } from '../components/shared/base';
44
import { logoutUser } from '../utils/auth';
55
import { navigate } from 'gatsby';
6-
import WithCurrentUser from '../utils/auth/WithCurrentUser';
6+
import { Query } from 'react-apollo';
7+
import gql from 'graphql-tag';
8+
import { optionalChaining } from '../utils/helpers';
79
import { CenteredLoader } from '../components/Loader';
810

911
const Profile = () => {
1012
return (
11-
<WithCurrentUser>
12-
{({ user, loading }) => {
13+
<Query query={PROFILE_QUERY}>
14+
{({ data, loading }) => {
1315
if (loading) {
1416
return <CenteredLoader />;
1517
}
16-
if (user) {
17-
return <ProfilePage user={user} />;
18+
if (optionalChaining(() => data!.viewer!.user)) {
19+
return <ProfilePage user={data.viewer.user} />;
1820
}
1921
navigate('/signup/');
2022
return null;
2123
}}
22-
</WithCurrentUser>
24+
</Query>
2325
);
2426
};
2527

@@ -32,9 +34,22 @@ type User = {
3234
avatarUrl: string;
3335
name: string;
3436
githubHandle: string;
37+
bio: string;
38+
upvoted: [Tutorials];
39+
saved: [Tutorials];
40+
};
41+
42+
type Tutorials = {
43+
tutorial: Tutorial;
44+
};
45+
46+
type Tutorial = {
47+
id: 'string';
48+
name: 'string';
3549
};
3650

3751
const ProfilePage: React.FunctionComponent<ProfileProps> = ({ user }) => {
52+
console.log(user);
3853
return (
3954
<Layout>
4055
<Flex flexDirection="column">
@@ -52,7 +67,49 @@ const ProfilePage: React.FunctionComponent<ProfileProps> = ({ user }) => {
5267
viverra nibh nec, ultrices risus. */
5368
</Text>
5469
<button onClick={() => logoutUser()}> Log out </button>
70+
<Heading> Upvoted Tutorials </Heading>
71+
<ul>
72+
{user.upvoted.map(a => (
73+
<li key={a.tutorial.id}>
74+
<span>{a.tutorial.name}</span>
75+
</li>
76+
))}
77+
</ul>
78+
<Heading> Saved Tutorials </Heading>
79+
<ul>
80+
{user.saved.map(a => (
81+
<li key={a.tutorial.id}>
82+
<span>{a.tutorial.name}</span>
83+
</li>
84+
))}
85+
</ul>
5586
</Layout>
5687
);
5788
};
5889
export default Profile;
90+
91+
const PROFILE_QUERY = gql`
92+
query profileQuery {
93+
viewer {
94+
user {
95+
id
96+
name
97+
githubHandle
98+
avatarUrl
99+
bio
100+
upvoted: userTutorials(where: { upvoted: true }) {
101+
tutorial {
102+
id
103+
name
104+
}
105+
}
106+
saved: userTutorials(where: { saved: true }) {
107+
tutorial {
108+
id
109+
name
110+
}
111+
}
112+
}
113+
}
114+
}
115+
`;

packages/gatsby-theme/src/utils/auth/WithCurrentUser.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const WithCurrentUser: React.FunctionComponent<WithCurrentUserProps> = ({
1616
children,
1717
}) => {
1818
return (
19-
<Query<CurrentUserQuery> query={CURRENT_USER} >
19+
<Query<CurrentUserQuery> query={CURRENT_USER}>
2020
{({ data, loading }) => {
2121
if (loading) {
2222
return children({ loading });
@@ -27,7 +27,7 @@ const WithCurrentUser: React.FunctionComponent<WithCurrentUserProps> = ({
2727
return children({ user: false });
2828
}}
2929
</Query>
30-
)
30+
);
3131
};
3232

3333
export const CURRENT_USER = gql`
@@ -37,11 +37,11 @@ export const CURRENT_USER = gql`
3737
user {
3838
id
3939
name
40-
avatarUrl
41-
githubHandle
40+
avatarUrl
41+
githubHandle
42+
}
4243
}
4344
}
44-
}
4545
`;
4646

4747
export default WithCurrentUser;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export enum ApiErrors {
2+
AUTHORIZATION = 'AUTHORIZATION',
3+
INVALID = 'INVALID',
4+
}
5+
6+
export async function handleMutationResponse(
7+
mutationReq: Promise<any>,
8+
): Promise<any> {
9+
try {
10+
return await mutationReq;
11+
} catch (err) {
12+
if (err.message === 'GraphQL error: Not authorized') {
13+
return {
14+
error: ApiErrors.AUTHORIZATION,
15+
};
16+
}
17+
return {
18+
error: ApiErrors.INVALID,
19+
};
20+
}
21+
}

packages/server/.yoga/nexus.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ export interface NexusGenFieldTypes {
327327
}
328328
Mutation: { // field return type
329329
authenticate: NexusGenRootTypes['AuthenticateUserPayload'] | null; // AuthenticateUserPayload
330+
saveTutorial: NexusGenRootTypes['UserTutorialPayload']; // UserTutorialPayload!
330331
upvoteTutorial: NexusGenRootTypes['UserTutorialPayload']; // UserTutorialPayload!
331332
}
332333
Query: { // field return type
@@ -392,6 +393,9 @@ export interface NexusGenArgTypes {
392393
authenticate: { // args
393394
githubCode: string; // String!
394395
}
396+
saveTutorial: { // args
397+
tutorialId: string; // ID!
398+
}
395399
upvoteTutorial: { // args
396400
tutorialId: string; // ID!
397401
}

0 commit comments

Comments
 (0)