Skip to content

Commit

Permalink
Feat/post tweets (#9)
Browse files Browse the repository at this point in the history
* feat: return user tweets in graphql

* feat: show tweets of user

* feat: add account not found page

* refactor: improve tweet mutations

* feat: implement functionality to post tweets

* docs: change todo list
  • Loading branch information
wrongbyte committed Aug 15, 2022
1 parent 9495fe3 commit 06802aa
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 20 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -12,6 +12,7 @@
- [ ] show followers in profiles
- [ ] display following users' tweets on timeline
- [ ] create error modal
- [ ] remove unused fields (such as birthday)

### How to run

Expand Down
71 changes: 62 additions & 9 deletions packages/client/src/components/feed/NewTweetModal.tsx
@@ -1,24 +1,77 @@
import '../../styles/profile.css';
import { Dispatch, SetStateAction } from 'react';
import { useMutation } from 'react-relay';
import { useForm, SubmitHandler } from 'react-hook-form';
import type { TweetCreateMutation } from '../../relay/tweet/__generated__/TweetCreateMutation.graphql';
import { object, string, TypeOf } from 'zod';
import { TweetCreate } from '../../relay/tweet/TweetCreateMutation';
import { zodResolver } from '@hookform/resolvers/zod';

const tweetSchema = object({
content: string()
.min(1, 'You cannot post an empty tweet.')
.max(280, 'Maximum number of characters exceeded.'),
});
// todo: redirect if not logged in

type ITweet = TypeOf<typeof tweetSchema>;

const defaultValues: ITweet = {
content: '',
};

export default function NewTweetModal({
setOpenTweetModal,
}: {
setOpenTweetModal: Dispatch<SetStateAction<boolean>>;
}) {
const [handleSubmitTweet] = useMutation<TweetCreateMutation>(TweetCreate);
const {
handleSubmit,
register,
formState: { errors },
} = useForm<ITweet>({
resolver: zodResolver(tweetSchema),
defaultValues,
});

const onSubmitHandler: SubmitHandler<ITweet> = async (values: ITweet) => {
handleSubmitTweet({
variables: values,
onCompleted: (_, error) => {
if (error && error.length > 0) {
console.log(error);
return;
}
setOpenTweetModal(false);
},
});
};

return (
<>
<div className="new-tweet-modal">
<div className="close-x text-white" onClick={() => setOpenTweetModal(false)}>
x
</div>
<div className="flex ml-4 mt-16 gap-5 ">
<img src="default-pfp-tt.png" className="tweet-avatar"></img>
<div className="tweet-input-wrapper">
<textarea className="tweet-input" placeholder="What's happening?" />
<form onSubmit={handleSubmit(onSubmitHandler)}>
<div className="close-x text-white" onClick={() => setOpenTweetModal(false)}>
x
</div>
<div className="flex ml-4 mt-16 gap-5 ">
<img src="default-pfp-tt.png" className="tweet-avatar"></img>
<div className="tweet-input-wrapper">
<textarea
className="tweet-input"
placeholder="What's happening?"
{...register('content')}
/>
</div>
</div>
</div>
<button className="tweet-blue-button font-bold">Tweet</button>
<button type="submit" className="tweet-blue-button font-bold" value="Tweet">
Tweet
</button>
{errors && (
<span className="text-red-500 -mt-3 -mb-3 ml-4">{errors.content?.message}</span>
)}
</form>
</div>
<div className="modalBackground"></div>
</>
Expand Down
1 change: 0 additions & 1 deletion packages/client/src/components/user/UserPage.tsx
Expand Up @@ -42,7 +42,6 @@ export default function UserPage() {
return (
<MainColumn>
<LateralBar />

<UserTopBar />
{findUserByUsername ? (
<div className="user-column">
Expand Down
3 changes: 3 additions & 0 deletions packages/client/src/relay/fetchGraphQL.ts
Expand Up @@ -2,12 +2,15 @@ import { Variables } from 'relay-runtime';

// TODO: put the url into an env var

const token = localStorage.getItem('ACCESS_TOKEN');

export const fetchGraphQL = async (query: string, variables: Variables) => {
const response = await fetch('http://localhost:3001/graphql' as string, {
method: 'POST',
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json',
authorization: token || '',
},
body: JSON.stringify({
query,
Expand Down
11 changes: 11 additions & 0 deletions packages/client/src/relay/tweet/TweetCreateMutation.ts
@@ -0,0 +1,11 @@
import { graphql } from 'react-relay';

export const TweetCreate = graphql`
mutation TweetCreateMutation($content: String!) {
CreateTweetMutation(input: { content: $content }) {
tweet {
content
}
}
}
`;
3 changes: 1 addition & 2 deletions packages/server/src/graphql/schema.graphql
Expand Up @@ -59,7 +59,7 @@ type TweetEdge {
type Tweet implements Node {
"""The ID of an object"""
id: ID!
author: String!
author: User
content: String!
likedBy: [String]
retweetedBy: [String]
Expand Down Expand Up @@ -121,7 +121,6 @@ type CreateTweetPayload {
}

input CreateTweetInput {
author: String!
content: String!
replies: [String]
clientMutationId: String
Expand Down
21 changes: 15 additions & 6 deletions packages/server/src/modules/tweet/mutations/tweetCreateMutation.ts
@@ -1,23 +1,32 @@
import { GraphQLList, GraphQLNonNull, GraphQLString } from 'graphql';
import { mutationWithClientMutationId } from 'graphql-relay';
import { GraphQLContext } from '../../../getContext';
import { UserModel } from '../../user/userModel';
import { Tweet, TweetModel } from '../tweetModel';
import { findTweetById } from '../tweetService';
import { TweetType } from '../tweetType';

//TODO: needs to link the current user as the author

export const CreateTweetMutation = mutationWithClientMutationId({
name: 'CreateTweet',
description: 'Posts a new tweet',
inputFields: {
author: { type: new GraphQLNonNull(GraphQLString) },
content: { type: new GraphQLNonNull(GraphQLString) },
replies: { type: new GraphQLList(GraphQLString) },
},

mutateAndGetPayload: async (tweetPayload: Tweet) => {
const tweet = new TweetModel(tweetPayload);
return tweet.save();
mutateAndGetPayload: async (tweetPayload: Tweet, ctx: GraphQLContext) => {
if (!ctx?.user) {
throw new Error('User not logged in');
}
const tweet = await new TweetModel({
author: ctx.user.id,
...tweetPayload,
}).save();
await UserModel.findOneAndUpdate(
{ _id: ctx.user.id },
{ $addToSet: { tweets: tweet.id } }
);
return tweet;
},

outputFields: {
Expand Down
10 changes: 8 additions & 2 deletions packages/server/src/modules/tweet/tweetType.ts
Expand Up @@ -8,14 +8,20 @@ import { registerTypeLoader, nodeInterface } from '../../graphql/typeRegister';
import { connectionDefinitions, globalIdField } from 'graphql-relay';
import { Tweet } from './tweetModel';
import { load } from './TweetLoader';
import { UserType } from '../user/userType';
import * as UserLoader from '../user/UserLoader';
import { UserModel } from '../user/userModel';

export const TweetType = new GraphQLObjectType<Tweet>({
name: 'Tweet',
fields: () => ({
id: globalIdField('Tweet'),
author: {
type: new GraphQLNonNull(GraphQLString),
resolve: (tweet) => tweet.author,
type: UserType,
resolve: async (tweet, _, context) => {
const user = await UserModel.findById(tweet.author);
return await UserLoader.load(context, user._id);
},
},
content: {
type: new GraphQLNonNull(GraphQLString),
Expand Down

0 comments on commit 06802aa

Please sign in to comment.