93 | ): React.ReactNode {
94 | return [
95 | ,
98 |
99 | {this.input(type, input, inputHtml, selectOptions)}
100 |
101 | ];
102 | }
103 |
104 | private checkboxWrapper(type: string, input: IFormInput, inputHtml: any): React.ReactNode {
105 | return (
106 |
107 |
111 |
112 | );
113 | }
114 |
115 | private fileWrapper(type: string, input: IFormInput, inputHtml: any) {
116 | const value = input.value;
117 | const filename = value && value.name;
118 | const newInputHtml = { ...inputHtml, className: classnames(inputHtml.className, 'file-input') };
119 |
120 | return (
121 |
122 |
132 |
133 | );
134 | }
135 |
136 | public render() {
137 | const { type, meta, className, hint, input, inputHtml, options: selectOptions } = this.props;
138 |
139 | let wrapper: React.ReactNode = null;
140 | switch (type) {
141 | case 'checkbox':
142 | wrapper = this.checkboxWrapper(type, input, inputHtml);
143 | break;
144 | case 'file':
145 | wrapper = this.fileWrapper(type, input, inputHtml);
146 | break;
147 | default:
148 | wrapper = this.defaultWrapper(type, input, inputHtml, selectOptions);
149 | }
150 |
151 | return (
152 |
153 | {wrapper}
154 | {(meta.error || meta.submitError) &&
155 | meta.touched &&
{meta.error || meta.submitError}
}
156 | {hint &&
{hint}
}
157 |
158 | );
159 | }
160 |
161 | private stateClass() {
162 | const { meta } = this.props;
163 | return { 'is-danger': meta.touched && meta.invalid };
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/client/src/components/form/SubmitField.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import classnames from 'classnames';
3 | import { Link } from 'react-router-dom';
4 |
5 | // typings
6 | interface IProps {
7 | value?: string;
8 | loading?: boolean;
9 | cancel?: boolean;
10 | disabled?: boolean;
11 | }
12 |
13 | export default class SubmitField extends React.Component {
14 | public static defaultProps: IProps = {
15 | value: 'Submit',
16 | loading: false,
17 | cancel: true,
18 | disabled: false
19 | };
20 |
21 | public render() {
22 | const { loading, value, cancel, disabled } = this.props;
23 | return (
24 |
25 |
26 |
33 |
34 | {cancel ? (
35 |
36 |
37 | Back
38 |
39 |
40 | ) : null}
41 |
42 | );
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/client/src/components/form/renderField.d.ts:
--------------------------------------------------------------------------------
1 | export interface IFormInput {
2 | name: string;
3 | onBlur: (event?: any) => void;
4 | onChange: (event: any) => void;
5 | onFocus: (event?: any) => void;
6 | value: any;
7 | }
8 |
9 | export interface IMeta {
10 | active: boolean;
11 | dirty: boolean;
12 | error: boolean;
13 | initial: boolean;
14 | invalid: boolean;
15 | pristine: boolean;
16 | submitError: boolean;
17 | submitFailed: boolean;
18 | submitSucceeded: boolean;
19 | touched: boolean;
20 | valid: boolean;
21 | visited: boolean;
22 | }
23 |
24 | export interface IOption {
25 | label: string;
26 | value: string;
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/components/form/validation.ts:
--------------------------------------------------------------------------------
1 | export const required = (value: any) => (value ? undefined : "can't be blank");
2 |
--------------------------------------------------------------------------------
/client/src/config/apolloClient.ts:
--------------------------------------------------------------------------------
1 | import { ApolloClient } from 'apollo-client';
2 | import { ApolloLink } from 'apollo-link';
3 | import { InMemoryCache } from 'apollo-cache-inmemory';
4 | import { DedupLink } from 'apollo-link-dedup';
5 |
6 | import { authLink, formatErrorsLink, onErrorLink, batchHttpLink } from 'config/links';
7 | import { flashMessageLocalLink } from 'components/flash/flashMessageLocalLink';
8 |
9 | export default new ApolloClient({
10 | link: ApolloLink.from([
11 | new DedupLink(),
12 | flashMessageLocalLink,
13 | onErrorLink,
14 | authLink,
15 | formatErrorsLink,
16 | batchHttpLink
17 | ]),
18 | cache: new InMemoryCache()
19 | });
20 |
--------------------------------------------------------------------------------
/client/src/config/links.ts:
--------------------------------------------------------------------------------
1 | import { ApolloLink } from 'apollo-link';
2 | import { setContext } from 'apollo-link-context';
3 | import { onError } from 'apollo-link-error';
4 | import { BatchHttpLink } from 'apollo-link-batch-http';
5 |
6 | import ROOT_URL from 'config/rootUrl';
7 | import formatErrors from 'utils/errorsUtils';
8 | import client from 'config/apolloClient';
9 |
10 | import CREATE_FLASH_MESSAGE from 'graphql/flash/createFlashMessageMutation.graphql';
11 |
12 | export const batchHttpLink = new BatchHttpLink({
13 | uri: `${ROOT_URL}/graphql`
14 | });
15 |
16 | export const authLink = setContext((_, { headers }) => {
17 | const token = localStorage.getItem('blog:token');
18 | return {
19 | headers: {
20 | ...headers,
21 | authorization: token ? `Bearer ${token}` : null
22 | }
23 | };
24 | });
25 |
26 | export const onErrorLink = onError(({}) => {
27 | error("Oops, we're sorry, but something went wrong");
28 | });
29 |
30 | export const formatErrorsLink = new ApolloLink((operation: any, forward: any) => {
31 | return forward(operation).map((response: any) => {
32 | // Add format errors properties to payload if an error is happend
33 | for (const operationName of Object.keys(response.data)) {
34 | const payload: any = response.data[operationName];
35 | if (payload && payload.messages && payload.messages.length > 0) {
36 | response.data[operationName].errors = formatErrors(payload.messages);
37 | if (payload.errors.base) {
38 | error(payload.errors.base);
39 | } else {
40 | error('Please review the problems below:');
41 | }
42 | }
43 | }
44 | return response;
45 | });
46 | });
47 |
48 | const error = (text: string) => {
49 | client.mutate({ mutation: CREATE_FLASH_MESSAGE, variables: { type: 'error', text } });
50 | };
51 |
--------------------------------------------------------------------------------
/client/src/config/rootUrl.ts:
--------------------------------------------------------------------------------
1 | export default (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : '');
2 |
--------------------------------------------------------------------------------
/client/src/containers/comments/_Comment.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import moment from 'moment';
3 |
4 | // typings
5 | import { CommentFragment } from 'types';
6 |
7 | interface IProps {
8 | comment: CommentFragment;
9 | }
10 |
11 | export default class Comment extends React.Component {
12 | public render() {
13 | const { comment } = this.props;
14 |
15 | return (
16 |
17 |
{comment.content}
18 |
19 |
20 | Commented by: {comment.author.name}
21 |
22 | {moment(new Date(comment.created_at)).fromNow()}
23 |
24 |
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/containers/comments/_NewComment.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import withMutationState from 'apollo-mutation-state';
4 | import { Link } from 'react-router-dom';
5 | import { Form, Field } from 'react-final-form';
6 | import shortid from 'shortid';
7 |
8 | import RenderField from 'components/form/RenderField';
9 | import SubmitField from 'components/form/SubmitField';
10 | import withCurrentUser from 'queries/currentUserQuery';
11 | import withFlashMessage from 'components/flash/withFlashMessage';
12 |
13 | import CREATE_COMMENT from 'graphql/posts/createCommentMutation.graphql';
14 | import POST from 'graphql/posts/postQuery.graphql';
15 |
16 | // typings
17 | import { ApolloQueryResult } from 'apollo-client/core/types';
18 | import { DataProxy } from 'apollo-cache';
19 | import {
20 | CreateCommentMutation,
21 | CreateCommentMutationVariables,
22 | PostQuery,
23 | User,
24 | MutationState,
25 | MutationStateProps
26 | } from 'types';
27 |
28 | interface IProps {
29 | postId: string;
30 | createComment: (
31 | postId: string,
32 | { content }: CreateCommentMutationVariables
33 | ) => Promise>;
34 | handleSubmit: (event: any) => void;
35 | deleteFlashMessage: () => void;
36 | currentUser: User;
37 | mutation: MutationState;
38 | }
39 |
40 | class NewComment extends React.Component {
41 | private createCommentForm: any;
42 |
43 | constructor(props: IProps) {
44 | super(props);
45 | this.submitForm = this.submitForm.bind(this);
46 | }
47 |
48 | public async submitForm(values: any) {
49 | const { createComment, postId } = this.props;
50 | const { data: { createComment: { errors } } } = await createComment(postId, values);
51 | if (!errors) {
52 | this.props.deleteFlashMessage();
53 | this.createCommentForm.form.change('content', '');
54 | } else {
55 | return errors;
56 | }
57 | }
58 |
59 | public render() {
60 | const { mutation: { loading }, currentUser } = this.props;
61 |
62 | if (!currentUser) {
63 | return (
64 |
65 | You need to sign in or sign up before
66 | continuing.
67 |
68 | );
69 | }
70 |
71 | return (
72 |
73 |
83 | )}
84 | />
85 |
86 | );
87 | }
88 | }
89 |
90 | type CurrentUserProps = {
91 | currentUser: User;
92 | };
93 |
94 | const withCreateComment = graphql<
95 | CreateCommentMutation,
96 | CreateCommentMutationVariables & MutationStateProps & CurrentUserProps
97 | >(CREATE_COMMENT, {
98 | props: ({ ownProps, mutate }) => ({
99 | createComment(postId: string, comment: CreateCommentMutationVariables) {
100 | return ownProps.wrapMutate(
101 | mutate!({
102 | variables: { postId, ...comment },
103 | update: (store: DataProxy, { data: { createComment: { newComment } } }: any): void => {
104 | if (!newComment) return;
105 | const id = ownProps.postId;
106 | const data = store.readQuery({ query: POST, variables: { id } }) as PostQuery;
107 | if (!data || !data.post) return;
108 |
109 | data.post.comments.unshift(newComment);
110 | store.writeQuery({ query: POST, variables: { id }, data });
111 | },
112 | optimisticResponse: {
113 | __typename: 'Mutation',
114 | createComment: {
115 | __typename: 'Post',
116 | newComment: {
117 | __typename: 'Comment',
118 | id: shortid.generate(),
119 | content: comment.content,
120 | created_at: +new Date(),
121 | pending: true,
122 | author: {
123 | __typename: 'User',
124 | name: ownProps.currentUser.name
125 | }
126 | },
127 | messages: null
128 | }
129 | }
130 | })
131 | );
132 | }
133 | })
134 | });
135 |
136 | export default compose(
137 | withCurrentUser,
138 | withMutationState({ wrapper: true, propagateError: true }),
139 | withCreateComment,
140 | withFlashMessage
141 | )(NewComment);
142 |
--------------------------------------------------------------------------------
/client/src/containers/layouts/App.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { compose } from 'react-apollo';
3 | import { withRouter } from 'react-router';
4 | import { Route, Switch } from 'react-router-dom';
5 |
6 | import AllPosts from 'containers/posts/AllPosts';
7 | import SearchPosts from 'containers/posts/SearchPosts';
8 | import Post from 'containers/posts/Post';
9 | import NewPost from 'containers/posts/NewPost';
10 | import EditPost from 'containers/posts/EditPost';
11 |
12 | import SignInUser from 'containers/users/SignInUser';
13 | import SignUpUser from 'containers/users/SignUpUser';
14 | import EditUserProfile from 'containers/users/EditUserProfile';
15 | import ChangeUserPassword from 'containers/users/ChangeUserPassword';
16 |
17 | import UserIsAuthenticated from 'components/UserIsAuthenticated';
18 | import NotFound from 'components/NotFound';
19 | import Header from 'containers/layouts/Header';
20 |
21 | import FlashMessage from 'components/flash/FlashMessage';
22 | import withFlashMessage from 'components/flash/withFlashMessage';
23 | import withCurrentUser from 'queries/currentUserQuery';
24 |
25 | import 'assets/stylesheets/css/application.css';
26 |
27 | // typings
28 | import { User } from 'types';
29 | import { History } from 'history';
30 |
31 | interface IProps {
32 | history: History;
33 | deleteFlashMessage: () => void;
34 | currentUser: User;
35 | currentUserLoading: boolean;
36 | }
37 |
38 | class App extends React.Component {
39 | private unsubscribeFromHistory: any;
40 |
41 | public componentWillMount() {
42 | const { history } = this.props;
43 | this.unsubscribeFromHistory = history.listen(this.handleLocationChange);
44 | this.handleLocationChange();
45 | }
46 |
47 | public componentWillUnmount() {
48 | if (this.unsubscribeFromHistory) this.unsubscribeFromHistory();
49 | }
50 |
51 | public handleLocationChange = () => {
52 | this.props.deleteFlashMessage();
53 | };
54 |
55 | public render() {
56 | const { currentUser, currentUserLoading } = this.props;
57 |
58 | return (
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 | }
89 |
90 | export default compose(withCurrentUser, withFlashMessage, withRouter)(App);
91 |
--------------------------------------------------------------------------------
/client/src/containers/layouts/Header.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import { Link } from 'react-router-dom';
4 |
5 | import withFlashMessage from 'components/flash/withFlashMessage';
6 | import REVOKE_TOKEN from 'graphql/auth/revokeTokenMutation.graphql';
7 |
8 | // typings
9 | import { User, FlashMessageVariables, RevokeTokenMutation } from 'types';
10 | import { ApolloQueryResult } from 'apollo-client/core/types';
11 |
12 | interface IProps {
13 | redirect: (path: string, message: FlashMessageVariables) => void;
14 | revokeToken: () => Promise>;
15 | currentUser: User;
16 | currentUserLoading: boolean;
17 | }
18 |
19 | class Header extends React.Component {
20 | constructor(props: IProps) {
21 | super(props);
22 | this.logout = this.logout.bind(this);
23 | }
24 |
25 | private logout(event: any) {
26 | event.preventDefault();
27 | this.props.revokeToken().then(response => {
28 | const errors = response.data.revokeToken.errors;
29 | if (!errors) {
30 | window.localStorage.removeItem('blog:token');
31 | (window as any).location = '/';
32 | }
33 | });
34 | }
35 |
36 | private renderSignInLinks() {
37 | const { currentUser, currentUserLoading } = this.props;
38 | if (currentUserLoading) {
39 | return null;
40 | }
41 |
42 | if (currentUser) {
43 | return (
44 |
52 | );
53 | }
54 |
55 | return (
56 |
57 |
58 | Register
59 |
60 |
61 | Login
62 |
63 |
64 | );
65 | }
66 |
67 | public render() {
68 | return (
69 |
70 |
71 |
79 |
80 |
81 | );
82 | }
83 | }
84 |
85 | const withRevokeToken = graphql(REVOKE_TOKEN, {
86 | props: ({ mutate }) => ({
87 | revokeToken() {
88 | return mutate!({});
89 | }
90 | })
91 | });
92 |
93 | export default compose(withFlashMessage, withRevokeToken)(Header);
94 |
--------------------------------------------------------------------------------
/client/src/containers/posts/AllPosts.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { compose } from 'react-apollo';
3 |
4 | import ListPosts from 'containers/posts/_ListPosts';
5 | import HeadListPosts from 'containers/posts/_HeadListPosts';
6 | import withPosts from 'queries/postsQuery';
7 |
8 | // typings
9 | import { ApolloQueryResult } from 'apollo-client/core/types';
10 | import { PostsQuery } from 'types';
11 |
12 | interface IProps {
13 | data: PostsQuery;
14 | match: any;
15 | loadMorePosts: () => ApolloQueryResult;
16 | }
17 |
18 | class AllPosts extends React.Component {
19 | public render() {
20 | const { data: { posts, postsCount }, loadMorePosts } = this.props;
21 | const { params: { keywords } } = this.props.match;
22 |
23 | if (!posts) {
24 | return null;
25 | }
26 |
27 | return (
28 |
29 |
Listing posts
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | export default compose(withPosts)(AllPosts);
40 |
--------------------------------------------------------------------------------
/client/src/containers/posts/EditPost.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import withMutationState from 'apollo-mutation-state';
4 |
5 | import PostForm from 'containers/posts/_PostForm';
6 | import withFlashMessage from 'components/flash/withFlashMessage';
7 |
8 | import POST_FOR_EDITING from 'graphql/posts/postForEditingQuery.graphql';
9 | import UPDATE_POST from 'graphql/posts/updatePostMutation.graphql';
10 |
11 | // typings
12 | import { ApolloQueryResult } from 'apollo-client/core/types';
13 | import {
14 | FlashMessageVariables,
15 | UpdatePostMutation,
16 | UpdatePostMutationVariables,
17 | PostForEditingQuery,
18 | PostFragment,
19 | MutationState,
20 | MutationStateProps
21 | } from 'types';
22 |
23 | interface IProps {
24 | redirect: (path: string, message: FlashMessageVariables) => void;
25 | updatePost: ({ }: UpdatePostMutationVariables) => Promise>;
26 | data: PostForEditingQuery;
27 | mutation: MutationState;
28 | }
29 |
30 | class EditPost extends React.Component {
31 | constructor(props: IProps) {
32 | super(props);
33 | this.action = this.action.bind(this);
34 | }
35 |
36 | private async action(values: any) {
37 | return new Promise(async (_, reject) => {
38 | const { data: { updatePost: { errors } } } = await this.props.updatePost(values);
39 | if (!errors) {
40 | this.props.redirect('/', { notice: 'Post was successfully updated' });
41 | } else {
42 | reject(errors);
43 | }
44 | });
45 | }
46 |
47 | public render() {
48 | const { data: { post } } = this.props;
49 | if (!post) {
50 | return null;
51 | }
52 |
53 | return (
54 |
55 |
Editing post
56 |
62 |
63 | );
64 | }
65 | }
66 |
67 | const withPostForEditing = graphql(POST_FOR_EDITING, {
68 | options: (ownProps: any) => ({
69 | variables: {
70 | id: ownProps.match.params.id
71 | },
72 | fetchPolicy: 'network-only'
73 | })
74 | });
75 |
76 | const withUpdatePost = graphql(UPDATE_POST, {
77 | props: ({ mutate, ownProps: { wrapMutate } }) => ({
78 | updatePost(post: PostFragment) {
79 | return wrapMutate(mutate!({ variables: { ...post } }));
80 | }
81 | })
82 | });
83 |
84 | export default compose(
85 | withPostForEditing,
86 | withMutationState({ wrapper: true, propagateError: true }),
87 | withUpdatePost,
88 | withFlashMessage
89 | )(EditPost);
90 |
--------------------------------------------------------------------------------
/client/src/containers/posts/NewPost.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import withMutationState from 'apollo-mutation-state';
4 |
5 | import PostForm from 'containers/posts/_PostForm';
6 | import withFlashMessage from 'components/flash/withFlashMessage';
7 |
8 | import CREATE_POST from 'graphql/posts/createPostsMutation.graphql';
9 | import POSTS from 'graphql/posts/postsQuery.graphql';
10 |
11 | // typings
12 | import { ApolloQueryResult } from 'apollo-client/core/types';
13 | import { DataProxy } from 'apollo-cache';
14 | import {
15 | FlashMessageVariables,
16 | CreatePostMutation,
17 | CreatePostMutationVariables,
18 | PostsQuery,
19 | PostFragment,
20 | MutationState,
21 | MutationStateProps
22 | } from 'types';
23 |
24 | interface IProps {
25 | redirect: (path: string, message: FlashMessageVariables) => void;
26 | createPost: ({ }: CreatePostMutationVariables) => Promise>;
27 | mutation: MutationState;
28 | }
29 |
30 | class NewPost extends React.Component {
31 | constructor(props: IProps) {
32 | super(props);
33 | this.action = this.action.bind(this);
34 | }
35 |
36 | private async action(values: any) {
37 | return new Promise(async (_, reject) => {
38 | const { data: { createPost: { errors } } } = await this.props.createPost(values);
39 | if (!errors) {
40 | this.props.redirect('/', { notice: 'Post was successfully created.' });
41 | } else {
42 | reject(errors);
43 | }
44 | });
45 | }
46 |
47 | public render() {
48 | return (
49 |
53 | );
54 | }
55 | }
56 |
57 | const withCreatePost = graphql(CREATE_POST, {
58 | props: ({ mutate, ownProps: { wrapMutate } }) => ({
59 | createPost(post: PostFragment) {
60 | return wrapMutate(
61 | mutate!({
62 | variables: { ...post },
63 | update: (store: DataProxy, { data: { createPost: { newPost } } }: any): void => {
64 | if (!newPost) return;
65 | const data = store.readQuery({ query: POSTS }) as PostsQuery;
66 | if (!data.posts) return;
67 |
68 | data.posts.unshift(newPost);
69 | data.postsCount += 1;
70 | store.writeQuery({ query: POSTS, data });
71 | }
72 | })
73 | );
74 | }
75 | })
76 | });
77 |
78 | export default compose(withMutationState({ wrapper: true, propagateError: true }), withCreatePost, withFlashMessage)(
79 | NewPost
80 | );
81 |
--------------------------------------------------------------------------------
/client/src/containers/posts/Post.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 |
4 | import PostInfos from 'containers/posts/_PostInfos';
5 | import PostActions from 'containers/posts/_PostActions';
6 | import Comment from 'containers/comments/_Comment';
7 | import NewComment from 'containers/comments/_NewComment';
8 | import withCurrentUser from 'queries/currentUserQuery';
9 |
10 | import POST from 'graphql/posts/postQuery.graphql';
11 |
12 | // typings
13 | import { PostQuery, User, PostFragment, CommentFragment } from 'types';
14 |
15 | interface IProps {
16 | data: PostQuery;
17 | currentUser: User;
18 | }
19 |
20 | class Post extends React.Component {
21 | constructor(props: IProps) {
22 | super(props);
23 | this.listComments = this.listComments.bind(this);
24 | }
25 |
26 | private listComments(post: PostFragment) {
27 | const { comments } = post;
28 | if (!comments || comments.length === 0) {
29 | return null;
30 | }
31 | return comments.map((comment: CommentFragment) => {
32 | return ;
33 | });
34 | }
35 |
36 | public render() {
37 | const { data: { post }, currentUser } = this.props;
38 | if (!post) {
39 | return null;
40 | }
41 |
42 | return (
43 |
44 |
45 |
{post.title}
46 |
47 | {currentUser && currentUser.id === post.author.id ?
: null}
48 |
49 |
50 |
51 |
52 |
{post.content}
53 |
54 |
Comments
55 | {this.listComments(post)}
56 |
57 |
58 |
59 | );
60 | }
61 | }
62 |
63 | const withPost = graphql(POST, {
64 | options: (ownProps: any) => ({
65 | variables: {
66 | id: ownProps.match.params.id
67 | }
68 | })
69 | });
70 |
71 | export default compose(withCurrentUser, withPost)(Post);
72 |
--------------------------------------------------------------------------------
/client/src/containers/posts/SearchPosts.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { compose } from 'react-apollo';
3 |
4 | import withPosts from 'queries/postsQuery';
5 | import HeadListPosts from 'containers/posts/_HeadListPosts';
6 | import ListPosts from 'containers/posts/_ListPosts';
7 |
8 | // typings
9 | import { ApolloQueryResult } from 'apollo-client/core/types';
10 | import { PostsQuery } from 'types';
11 |
12 | interface IProps {
13 | data: PostsQuery;
14 | match: any;
15 | loadMorePosts: () => ApolloQueryResult;
16 | }
17 |
18 | class SearchPosts extends React.Component {
19 | public componentWillUpdate(nextProps: IProps) {
20 | if (nextProps.match.params.keywords !== this.props.match.params.keywords) {
21 | this.props.data.posts = [];
22 | }
23 | }
24 |
25 | public render() {
26 | const { data: { posts, postsCount }, loadMorePosts } = this.props;
27 | const { params: { keywords } } = this.props.match;
28 |
29 | if (!posts) {
30 | return null;
31 | }
32 |
33 | return (
34 |
35 |
Search: {keywords}
36 |
37 |
38 |
39 |
40 | {posts && posts.length === 0 ? (
41 | No results ...
42 | ) : (
43 |
44 | )}
45 |
46 | );
47 | }
48 | }
49 |
50 | export default compose(withPosts)(SearchPosts);
51 |
--------------------------------------------------------------------------------
/client/src/containers/posts/_HeadListPosts.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { compose } from 'react-apollo';
3 | import { Link } from 'react-router-dom';
4 |
5 | import SearchForm from 'containers/posts/_SearchForm';
6 | import withCurrentUser from 'queries/currentUserQuery';
7 |
8 | // typings
9 | import { User } from 'types';
10 |
11 | interface IProps {
12 | keywords: string;
13 | currentUser: User;
14 | }
15 |
16 | class HeadListRecipes extends React.Component {
17 | public render() {
18 | const { keywords, currentUser } = this.props;
19 |
20 | return (
21 |
22 |
27 | {currentUser ? (
28 |
29 |
30 | New Post
31 |
32 |
33 | ) : null}
34 |
35 | );
36 | }
37 | }
38 |
39 | export default compose(withCurrentUser)(HeadListRecipes);
40 |
--------------------------------------------------------------------------------
/client/src/containers/posts/_ListPosts.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import PostPreview from 'containers/posts/_PostPreview';
4 |
5 | // typings
6 | import { PostsQuery, PostPreviewFragment } from 'types';
7 | import { ApolloQueryResult } from 'apollo-client/core/types';
8 |
9 | interface IProps {
10 | posts: Array;
11 | postsCount: number;
12 | loadMorePosts: () => ApolloQueryResult;
13 | }
14 |
15 | export default class ListPosts extends React.Component {
16 | public render() {
17 | const { posts, postsCount, loadMorePosts } = this.props;
18 |
19 | if (!posts) {
20 | return null;
21 | }
22 |
23 | return (
24 |
25 | {posts.map(post =>
)}
26 |
27 | {posts && posts.length < postsCount ? (
28 |
31 | ) : null}
32 |
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/client/src/containers/posts/_PostActions.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import { Link } from 'react-router-dom';
4 |
5 | import withFlashMessage from 'components/flash/withFlashMessage';
6 |
7 | import DELETE_POST from 'graphql/posts/deletePostMutation.graphql';
8 | import POSTS from 'graphql/posts/postsQuery.graphql';
9 |
10 | // typings
11 | import { ApolloQueryResult } from 'apollo-client/core/types';
12 | import { DataProxy } from 'apollo-cache';
13 | import { PostPreviewFragment, PostsQuery, DeletePostMutation, DeletePostMutationVariables } from 'types';
14 |
15 | interface IProps {
16 | post: PostPreviewFragment;
17 | deletePost: (id: string) => Promise>;
18 | notice: (text: string) => void;
19 | }
20 |
21 | class PostActions extends React.Component {
22 | constructor(props: IProps) {
23 | super(props);
24 | this.destroy = this.destroy.bind(this);
25 | }
26 |
27 | private destroy() {
28 | if (window.confirm('êtes vous sûre ?')) {
29 | this.props.deletePost(this.props.post.id).then(response => {
30 | if (!response.data.deletePost.errors) {
31 | this.props.notice('Post was successfully destroyed');
32 | }
33 | });
34 | }
35 | return false;
36 | }
37 |
38 | public render() {
39 | const { post } = this.props;
40 |
41 | return (
42 |
54 | );
55 | }
56 | }
57 |
58 | const withDeletePost = graphql(DELETE_POST, {
59 | props: ({ mutate }) => ({
60 | deletePost(postID: string) {
61 | return mutate!({
62 | variables: { id: postID },
63 | update: (store: DataProxy, { data: { deletePost: { post: postDeleted } } }: any): void => {
64 | if (!postDeleted) return;
65 | const data = store.readQuery({ query: POSTS }) as PostsQuery;
66 | if (!data.posts) return;
67 | data.posts = data.posts.filter(post => post.id !== postDeleted.id);
68 | data.postsCount -= 1;
69 | store.writeQuery({ query: POSTS, data });
70 | }
71 | });
72 | }
73 | })
74 | });
75 |
76 | export default compose(withDeletePost, withFlashMessage)(PostActions);
77 |
--------------------------------------------------------------------------------
/client/src/containers/posts/_PostForm.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Form, Field } from 'react-final-form';
3 |
4 | import RenderField from 'components/form/RenderField';
5 | import SubmitField from 'components/form/SubmitField';
6 | import { required } from 'components/form/validation';
7 |
8 | // typings
9 | import { PostForEditingFragment, MutationState } from 'types';
10 |
11 | interface IProps {
12 | handleSubmit?: (event: any) => void;
13 | action: (values: any) => Promise;
14 | submitName: string;
15 | initialValues?: PostForEditingFragment;
16 | mutation: MutationState;
17 | }
18 |
19 | export default class PostForm extends React.Component {
20 | constructor(props: IProps) {
21 | super(props);
22 | this.submitForm = this.submitForm.bind(this);
23 | }
24 |
25 | private async submitForm(values: any) {
26 | try {
27 | await this.props.action(values);
28 | } catch (errors) {
29 | return errors;
30 | }
31 | }
32 |
33 | public render() {
34 | const { submitName, initialValues: post, mutation: { loading } } = this.props;
35 |
36 | return (
37 |
46 | )}
47 | />
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/client/src/containers/posts/_PostInfos.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import moment from 'moment';
3 |
4 | // typings
5 | import { PostPreviewFragment } from 'types';
6 |
7 | interface IProps {
8 | post: PostPreviewFragment;
9 | }
10 |
11 | export default class PostInfos extends React.Component {
12 | public render() {
13 | const { post } = this.props;
14 |
15 | return (
16 |
17 | By {post.author.name} -
18 | {moment(new Date(post.created_at)).fromNow()}
19 |
20 | );
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/client/src/containers/posts/_PostPreview.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { compose } from 'react-apollo';
3 | import { Link } from 'react-router-dom';
4 |
5 | import PostInfos from 'containers/posts/_PostInfos';
6 | import PostActions from 'containers/posts/_PostActions';
7 | import withCurrentUser from 'queries/currentUserQuery';
8 |
9 | // typings
10 | import { PostPreviewFragment, User } from 'types';
11 |
12 | interface IProps {
13 | post: PostPreviewFragment;
14 | currentUser: User;
15 | }
16 |
17 | class PostPreview extends React.Component {
18 | public render() {
19 | const { post, currentUser } = this.props;
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | {post.title}
27 |
28 | {currentUser && currentUser.id === post.author.id ?
: null}
29 |
30 |
31 |
32 |
{post.description}
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | export default compose(withCurrentUser)(PostPreview);
40 |
--------------------------------------------------------------------------------
/client/src/containers/posts/_SearchForm.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { compose } from 'react-apollo';
3 | import { withRouter } from 'react-router';
4 |
5 | // typings
6 | import { History } from 'history';
7 |
8 | interface IProps {
9 | initialKeywords: string;
10 | history: History;
11 | }
12 |
13 | interface IState {
14 | keywords: string;
15 | }
16 |
17 | class SearchForm extends React.Component {
18 | constructor(props: IProps) {
19 | super(props);
20 | this.onSearch = this.onSearch.bind(this);
21 | this.onInputChange = this.onInputChange.bind(this);
22 | }
23 |
24 | public componentWillMount() {
25 | this.setState({ keywords: this.props.initialKeywords });
26 | }
27 |
28 | private onInputChange(event: any) {
29 | this.setState({ keywords: event.target.value });
30 | }
31 |
32 | private onSearch(event: any) {
33 | event.preventDefault();
34 | const { keywords } = this.state;
35 | const pathName = keywords ? `/posts/search/${keywords}` : '/';
36 | this.props.history.push(pathName);
37 | }
38 |
39 | public render() {
40 | return (
41 |
57 | );
58 | }
59 | }
60 |
61 | export default compose(withRouter)(SearchForm);
62 |
--------------------------------------------------------------------------------
/client/src/containers/users/ChangeUserPassword.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import withMutationState from 'apollo-mutation-state';
4 | import { Form, Field } from 'react-final-form';
5 |
6 | import withFlashMessage from 'components/flash/withFlashMessage';
7 | import RenderField from 'components/form/RenderField';
8 | import SubmitField from 'components/form/SubmitField';
9 |
10 | import CHANGE_USER_PASSWORD from 'graphql/users/changeUserPasswordMutation.graphql';
11 |
12 | // typings
13 | import { ApolloQueryResult } from 'apollo-client/core/types';
14 | import {
15 | FlashMessageVariables,
16 | ChangePasswordMutation,
17 | ChangePasswordMutationVariables,
18 | MutationState,
19 | MutationStateProps,
20 | User
21 | } from 'types';
22 |
23 | interface IProps {
24 | redirect: (path: string, message: FlashMessageVariables) => void;
25 | handleSubmit: (event: any) => void;
26 | changePassword: ({ }: ChangePasswordMutationVariables) => Promise>;
27 | mutation: MutationState;
28 | }
29 |
30 | class ChangeUserPassword extends React.Component {
31 | private changePasswordForm: any;
32 |
33 | constructor(props: IProps) {
34 | super(props);
35 | this.submitForm = this.submitForm.bind(this);
36 | }
37 |
38 | private async submitForm(values: any) {
39 | const { data: { changePassword: { errors } } } = await this.props.changePassword(values);
40 | if (!errors) {
41 | this.props.redirect('/', { notice: 'User password was successfully updated' });
42 | } else {
43 | this.changePasswordForm.form.change('current_password', '');
44 | this.changePasswordForm.form.change('password', '');
45 | this.changePasswordForm.form.change('password_confirmation', '');
46 | return errors;
47 | }
48 | }
49 |
50 | public render() {
51 | const { mutation: { loading } } = this.props;
52 |
53 | return (
54 |
55 |
56 |
57 |
Changer votre mot de passe
58 |
75 | )}
76 | />
77 |
78 |
79 |
80 | );
81 | }
82 | }
83 |
84 | const withChangeUserPassword = graphql(
85 | CHANGE_USER_PASSWORD,
86 | {
87 | props: ({ mutate, ownProps: { wrapMutate } }) => ({
88 | changePassword(user: User) {
89 | return wrapMutate(mutate!({ variables: { ...user } }));
90 | }
91 | })
92 | }
93 | );
94 |
95 | export default compose(
96 | withMutationState({ wrapper: true, propagateError: true }),
97 | withChangeUserPassword,
98 | withFlashMessage
99 | )(ChangeUserPassword);
100 |
--------------------------------------------------------------------------------
/client/src/containers/users/EditUserProfile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import withMutationState from 'apollo-mutation-state';
4 | import { Link } from 'react-router-dom';
5 | import { Form, Field } from 'react-final-form';
6 |
7 | import withFlashMessage from 'components/flash/withFlashMessage';
8 | import RenderField from 'components/form/RenderField';
9 | import SubmitField from 'components/form/SubmitField';
10 | import { required } from 'components/form/validation';
11 |
12 | import USER_FOR_EDITING from 'graphql/users/userForEditingQuery.graphql';
13 | import UPDATE_USER from 'graphql/users/updateUserMutation.graphql';
14 | import CANCEL_ACCOUNT from 'graphql/users/cancelAccountMutation.graphql';
15 |
16 | // typings
17 | import { ApolloQueryResult } from 'apollo-client/core/types';
18 | import {
19 | FlashMessageVariables,
20 | CancelAccountMutation,
21 | UpdateUserMutationVariables,
22 | UpdateUserMutation,
23 | GetUserForEditingQuery,
24 | MutationState,
25 | MutationStateProps,
26 | User
27 | } from 'types';
28 |
29 | interface IProps {
30 | redirect: (path: string, message: FlashMessageVariables) => void;
31 | handleSubmit: (event: any) => void;
32 | cancelAccount: () => Promise>;
33 | updateUser: ({ }: UpdateUserMutationVariables) => Promise>;
34 | data: GetUserForEditingQuery;
35 | mutation: MutationState;
36 | }
37 |
38 | class EditUserProfile extends React.Component {
39 | constructor(props: IProps) {
40 | super(props);
41 | this.submitForm = this.submitForm.bind(this);
42 | this.onCancelAccount = this.onCancelAccount.bind(this);
43 | }
44 |
45 | private async submitForm(values: any) {
46 | const { data: { updateUser: { errors } } } = await this.props.updateUser(values);
47 | if (!errors) {
48 | this.props.redirect('/', { notice: 'User was successfully updated' });
49 | } else {
50 | return errors;
51 | }
52 | }
53 |
54 | private async onCancelAccount() {
55 | if (window.confirm('Are you sure ?')) {
56 | const { data: { cancelAccount: { errors } } } = await this.props.cancelAccount();
57 | if (!errors) {
58 | window.localStorage.removeItem('blog:token');
59 | (window as any).location = '/';
60 | }
61 | }
62 | }
63 |
64 | public render() {
65 | const { mutation: { loading }, data: { currentUser } } = this.props;
66 |
67 | return (
68 |
69 |
70 |
71 |
Edit profile
72 |
81 | )}
82 | />
83 |
84 |
85 |
Password
86 |
87 |
88 |
89 |
90 | Change password
91 |
92 |
93 |
94 |
103 |
104 |
105 |
106 | );
107 | }
108 | }
109 |
110 | const withUserForEditing = graphql(USER_FOR_EDITING, {
111 | options: () => ({
112 | fetchPolicy: 'network-only'
113 | })
114 | });
115 |
116 | const withUpdateUser = graphql(UPDATE_USER, {
117 | props: ({ mutate, ownProps: { wrapMutate } }) => ({
118 | updateUser(user: User) {
119 | return wrapMutate(mutate!({ variables: { ...user } }));
120 | }
121 | })
122 | });
123 |
124 | const withCancelAccount = graphql(CANCEL_ACCOUNT, {
125 | props: ({ mutate }) => ({
126 | cancelAccount() {
127 | return mutate!({});
128 | }
129 | })
130 | });
131 |
132 | export default compose(
133 | withUserForEditing,
134 | withFlashMessage,
135 | withCancelAccount,
136 | withMutationState({ wrapper: true, propagateError: true }),
137 | withUpdateUser
138 | )(EditUserProfile);
139 |
--------------------------------------------------------------------------------
/client/src/containers/users/SignInUser.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import withMutationState from 'apollo-mutation-state';
4 | import { Link } from 'react-router-dom';
5 | import { Form, Field } from 'react-final-form';
6 |
7 | import RenderField from 'components/form/RenderField';
8 | import SubmitField from 'components/form/SubmitField';
9 | import withFlashMessage from 'components/flash/withFlashMessage';
10 | import withPosts from 'queries/postsQuery';
11 | import withCurrentUser, { fetchCurrentUser } from 'queries/currentUserQuery';
12 |
13 | import SIGN_IN from 'graphql/auth/signInMutation.graphql';
14 |
15 | // typings
16 | import { ApolloQueryResult } from 'apollo-client/core/types';
17 | import {
18 | FlashMessageVariables,
19 | SignInMutationVariables,
20 | SignInMutation,
21 | User,
22 | PostsQuery,
23 | MutationState,
24 | MutationStateProps
25 | } from 'types';
26 |
27 | interface IProps {
28 | redirect: (path: string, message: FlashMessageVariables) => void;
29 | handleSubmit: (event: any) => void;
30 | signIn: ({ email, password }: SignInMutationVariables) => Promise>;
31 | currentUser: User;
32 | currentUserLoading: boolean;
33 | refetchPosts: () => Promise>;
34 | mutation: MutationState;
35 | }
36 |
37 | class SignInUser extends React.Component {
38 | private signInForm: any;
39 |
40 | constructor(props: IProps) {
41 | super(props);
42 | this.submitForm = this.submitForm.bind(this);
43 | this.redirectIfUserIsAuthenticated = this.redirectIfUserIsAuthenticated.bind(this);
44 | }
45 |
46 | public componentWillMount() {
47 | this.redirectIfUserIsAuthenticated();
48 | }
49 |
50 | public componentWillReceiveProps(nextProps: IProps) {
51 | this.redirectIfUserIsAuthenticated(nextProps);
52 | }
53 |
54 | private redirectIfUserIsAuthenticated(props?: IProps) {
55 | const { currentUser, currentUserLoading } = props || this.props;
56 | if (!currentUserLoading && currentUser) {
57 | this.props.redirect('/', { error: 'You are already signed in.' });
58 | }
59 | }
60 |
61 | private async submitForm(values: any) {
62 | const { data: { signIn: payload } } = await this.props.signIn(values);
63 | if (!payload.errors && payload.token) {
64 | window.localStorage.setItem('blog:token', payload.token);
65 | await fetchCurrentUser();
66 | this.props.refetchPosts();
67 | this.props.redirect('/', { notice: 'Signed in successfully.' });
68 | } else {
69 | window.localStorage.removeItem('blog:token');
70 | this.signInForm.form.change('password', '');
71 | }
72 | }
73 |
74 | public render() {
75 | const { mutation: { loading } } = this.props;
76 |
77 | return (
78 |
79 |
80 |
91 | )}
92 | />
93 | Sign up
94 |
95 |
96 | );
97 | }
98 | }
99 |
100 | const withSignIn = graphql(SIGN_IN, {
101 | props: ({ mutate, ownProps: { wrapMutate } }) => ({
102 | signIn(user: SignInMutationVariables) {
103 | return wrapMutate(mutate!({ variables: { ...user } }));
104 | }
105 | })
106 | });
107 |
108 | export default compose(
109 | withCurrentUser,
110 | withMutationState({ wrapper: true, propagateError: true }),
111 | withSignIn,
112 | withFlashMessage,
113 | withPosts
114 | )(SignInUser);
115 |
--------------------------------------------------------------------------------
/client/src/containers/users/SignUpUser.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { graphql, compose } from 'react-apollo';
3 | import withMutationState from 'apollo-mutation-state';
4 | import { Link } from 'react-router-dom';
5 | import { Form, Field } from 'react-final-form';
6 |
7 | import RenderField from 'components/form/RenderField';
8 | import SubmitField from 'components/form/SubmitField';
9 | import { required } from 'components/form/validation';
10 | import withPosts from 'queries/postsQuery';
11 | import withFlashMessage from 'components/flash/withFlashMessage';
12 |
13 | import SIGN_UP from 'graphql/users/signUpMutation.graphql';
14 | import CURRENT_USER from 'graphql/users/currentUserQuery.graphql';
15 |
16 | // typings
17 | import { ApolloQueryResult } from 'apollo-client/core/types';
18 | import { DataProxy } from 'apollo-cache';
19 | import {
20 | FlashMessageVariables,
21 | SignUpMutation,
22 | SignUpMutationVariables,
23 | PostsQuery,
24 | CurrentUserQuery,
25 | MutationState,
26 | MutationStateProps,
27 | User
28 | } from 'types';
29 |
30 | interface IProps {
31 | redirect: (path: string, message: FlashMessageVariables) => void;
32 | handleSubmit: (event: any) => void;
33 | signUp: ({ }: SignUpMutationVariables) => Promise>;
34 | refetchPosts: () => Promise>;
35 | mutation: MutationState;
36 | }
37 |
38 | class SignUpUser extends React.Component {
39 | private signUpForm: any;
40 |
41 | constructor(props: IProps) {
42 | super(props);
43 | this.submitForm = this.submitForm.bind(this);
44 | }
45 |
46 | private async submitForm(values: any) {
47 | const { data: { signUp: payload } } = await this.props.signUp(values);
48 | if (!payload.errors && payload.currentUser && payload.currentUser.token) {
49 | window.localStorage.setItem('blog:token', payload.currentUser.token);
50 | await this.props.refetchPosts();
51 | this.props.redirect('/', { notice: 'Welcome! You have signed up successfully.' });
52 | } else {
53 | this.signUpForm.form.change('password', '');
54 | this.signUpForm.form.change('password_confirmation', '');
55 | return payload.errors;
56 | }
57 | }
58 |
59 | public render() {
60 | const { mutation: { loading } } = this.props;
61 |
62 | return (
63 |
64 |
65 |
83 | )}
84 | />
85 | Log in
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | const withSignUp = graphql(SIGN_UP, {
93 | props: ({ mutate, ownProps: { wrapMutate } }) => ({
94 | signUp(user: User) {
95 | return wrapMutate(
96 | mutate!({
97 | variables: { ...user },
98 | update: (store: DataProxy, { data: { signUp: { currentUser } } }: any): void => {
99 | if (!currentUser) return;
100 | const data = store.readQuery({ query: CURRENT_USER }) as CurrentUserQuery;
101 | data.currentUser = currentUser;
102 | store.writeQuery({ query: CURRENT_USER, data });
103 | }
104 | })
105 | );
106 | }
107 | })
108 | });
109 |
110 | export default compose(
111 | withMutationState({ wrapper: true, propagateError: true }),
112 | withSignUp,
113 | withFlashMessage,
114 | withPosts
115 | )(SignUpUser);
116 |
--------------------------------------------------------------------------------
/client/src/graphql/auth/revokeTokenMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation revokeToken {
2 | revokeToken(input: {}) {
3 | messages {
4 | message
5 | field
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/graphql/auth/signInMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation signIn($email: String, $password: String) {
2 | signIn(input: { email: $email, password: $password }) {
3 | token
4 | messages {
5 | message
6 | field
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/graphql/flash/createFlashMessageMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation createFlashMessage($type: String, $text: String) {
2 | createFlashMessage(type: $type, text: $text) @client
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/graphql/flash/deleteFlashMessageMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation deleteFlashMessage {
2 | deleteFlashMessage @client
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/graphql/flash/flashMessageQuery.graphql:
--------------------------------------------------------------------------------
1 | query message {
2 | message @client {
3 | type
4 | text
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/graphql/fragments/commentFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment CommentFragment on Comment {
2 | id
3 | content
4 | created_at
5 | author {
6 | name
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/graphql/fragments/postForEditingFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment PostForEditingFragment on Post {
2 | id
3 | title
4 | content
5 | description
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/graphql/fragments/postFragment.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/commentFragment.graphql"
2 |
3 | fragment PostFragment on Post {
4 | id
5 | title
6 | content
7 | created_at
8 | comments_count
9 | author {
10 | id
11 | name
12 | }
13 | comments {
14 | ...CommentFragment
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/client/src/graphql/fragments/postPreviewFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment PostPreviewFragment on Post {
2 | id
3 | title
4 | description
5 | created_at
6 | author {
7 | id
8 | name
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/graphql/fragments/userFragment.graphql:
--------------------------------------------------------------------------------
1 | fragment UserForEditingFragment on User {
2 | id
3 | name
4 | email
5 | }
6 |
--------------------------------------------------------------------------------
/client/src/graphql/posts/createCommentMutation.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/commentFragment.graphql"
2 |
3 | mutation createComment($postId: ID, $content: String) {
4 | createComment(input: { postId: $postId, content: $content }) {
5 | newComment: comment {
6 | ...CommentFragment
7 | }
8 | messages {
9 | field
10 | message
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/graphql/posts/createPostsMutation.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/postPreviewFragment.graphql"
2 |
3 | mutation createPost($title: String, $content: String) {
4 | createPost(input: { title: $title, content: $content }) {
5 | newPost: post {
6 | ...PostPreviewFragment
7 | }
8 | messages {
9 | field
10 | message
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/graphql/posts/deletePostMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation deletePost($id: ID) {
2 | deletePost(input: { id: $id }) {
3 | post {
4 | id
5 | }
6 | messages {
7 | field
8 | message
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/graphql/posts/postForEditingQuery.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/postForEditingFragment.graphql"
2 |
3 | query getPost($id: ID) {
4 | post(id: $id) {
5 | ...PostForEditingFragment
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/graphql/posts/postQuery.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/postFragment.graphql"
2 |
3 | query getPost($id: ID) {
4 | post(id: $id) {
5 | ...PostFragment
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/graphql/posts/postsQuery.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/postPreviewFragment.graphql"
2 |
3 | query posts($offset: Int, $keywords: String) {
4 | postsCount(keywords: $keywords)
5 | posts(offset: $offset, keywords: $keywords) {
6 | ...PostPreviewFragment
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/graphql/posts/updatePostMutation.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/postForEditingFragment.graphql"
2 |
3 | mutation updatePost($id: ID, $title: String, $content: String) {
4 | updatePost(input: { id: $id, title: $title, content: $content }) {
5 | post {
6 | ...PostForEditingFragment
7 | }
8 | messages {
9 | field
10 | message
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/graphql/users/cancelAccountMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation cancelAccount {
2 | cancelAccount(input: {})
3 | }
4 |
--------------------------------------------------------------------------------
/client/src/graphql/users/changeUserPasswordMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation changePassword($password: String, $password_confirmation: String, $current_password: String) {
2 | changePassword(
3 | input: { password: $password, password_confirmation: $password_confirmation, current_password: $current_password }
4 | ) {
5 | messages {
6 | field
7 | message
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/client/src/graphql/users/currentUserQuery.graphql:
--------------------------------------------------------------------------------
1 | query currentUser {
2 | currentUser {
3 | id
4 | name
5 | email
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/graphql/users/signUpMutation.graphql:
--------------------------------------------------------------------------------
1 | mutation signUp($name: String, $email: String, $password: String, $password_confirmation: String) {
2 | signUp(input: { name: $name, email: $email, password: $password, password_confirmation: $password_confirmation }) {
3 | currentUser: user {
4 | id
5 | name
6 | email
7 | token
8 | }
9 | messages {
10 | field
11 | message
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/client/src/graphql/users/updateUserMutation.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/userFragment.graphql"
2 |
3 | mutation updateUser($name: String, $email: String) {
4 | updateUser(input: { name: $name, email: $email }) {
5 | user {
6 | ...UserForEditingFragment
7 | }
8 | messages {
9 | field
10 | message
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/graphql/users/userForEditingQuery.graphql:
--------------------------------------------------------------------------------
1 | #import "graphql/fragments/userFragment.graphql"
2 |
3 | query getUserForEditing {
4 | currentUser {
5 | ...UserForEditingFragment
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render } from 'react-dom';
3 | import { BrowserRouter as Router } from 'react-router-dom';
4 | import { ApolloProvider } from 'react-apollo';
5 |
6 | import client from 'config/apolloClient';
7 | import App from 'containers/layouts/App';
8 | import ScrollToTop from 'components/ScrollToTop';
9 |
10 | render(
11 |
12 |
13 |
14 |
15 |
16 |
17 | ,
18 | document.getElementById('root')
19 | );
20 |
--------------------------------------------------------------------------------
/client/src/queries/currentUserQuery.ts:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-apollo';
2 | import client from 'config/apolloClient';
3 |
4 | import CURRENT_USER from 'graphql/users/currentUserQuery.graphql';
5 |
6 | // typings
7 | import { CurrentUserQuery } from 'types';
8 |
9 | export function fetchCurrentUser() {
10 | return client.query({ query: CURRENT_USER, fetchPolicy: 'network-only' });
11 | }
12 |
13 | export default graphql(CURRENT_USER, {
14 | props: ({ data: { currentUser, loading } }: any) => ({
15 | currentUser,
16 | currentUserLoading: loading
17 | })
18 | });
19 |
--------------------------------------------------------------------------------
/client/src/queries/postsQuery.ts:
--------------------------------------------------------------------------------
1 | import { graphql } from 'react-apollo';
2 |
3 | import POSTS from 'graphql/posts/postsQuery.graphql';
4 |
5 | // typings
6 | import { PostsQuery, PostsQueryVariables } from 'types';
7 |
8 | export default graphql(POSTS, {
9 | options: (ownProps: any) => ({
10 | variables: { keywords: ownProps.match.params.keywords }
11 | }),
12 | props: ({ data }) => {
13 | return {
14 | data,
15 | refetchPosts: data && data.refetch,
16 | loadMorePosts() {
17 | return (
18 | data &&
19 | data.fetchMore({
20 | variables: { offset: data.posts && data.posts.length },
21 | updateQuery(state, { fetchMoreResult }) {
22 | const { posts, postsCount } = fetchMoreResult as PostsQuery;
23 | if (!posts) return false;
24 | return {
25 | posts: [...state.posts, ...posts],
26 | postsCount
27 | };
28 | }
29 | })
30 | );
31 | }
32 | };
33 | }
34 | });
35 |
--------------------------------------------------------------------------------
/client/src/types.ts:
--------------------------------------------------------------------------------
1 | export type FlashMessageVariables = {
2 | notice?: string | null;
3 | error?: string | null;
4 | };
5 |
6 | export type FlashMessage = {
7 | type: string;
8 | text: string;
9 | };
10 |
11 | export type FlashMessageQuery = {
12 | message: FlashMessage;
13 | };
14 |
15 | export type MutationState = {
16 | loading: boolean;
17 | error: any;
18 | success: boolean;
19 | };
20 |
21 | export type MutationStateProps = {
22 | wrapMutate: (promise: Promise) => Promise;
23 | };
24 |
25 | export type RevokeTokenMutation = {
26 | revokeToken: {
27 | errors: any;
28 | };
29 | };
30 |
31 | export type SignInMutationVariables = {
32 | email: string;
33 | password: string;
34 | };
35 |
36 | export type SignInMutation = {
37 | signIn: {
38 | __typename: 'signInPayload';
39 | token?: string;
40 | errors: any;
41 | messages: Array | null;
42 | };
43 | };
44 |
45 | export type CreateCommentMutationVariables = {
46 | postId: string;
47 | content: string;
48 | };
49 |
50 | export type CreateCommentMutation = {
51 | createComment: {
52 | __typename: 'CommentPayload';
53 | newComment: CommentFragment | null;
54 | errors: any;
55 | messages: Array | null;
56 | };
57 | };
58 |
59 | export type CreatePostMutationVariables = {
60 | title: string;
61 | content: string;
62 | };
63 |
64 | export type CreatePostMutation = {
65 | createPost: {
66 | __typename: 'PostPayload';
67 | newPost: PostForEditingFragment | null;
68 | errors: any;
69 | messages: Array | null;
70 | };
71 | };
72 |
73 | export type DeletePostMutationVariables = {
74 | id: string;
75 | };
76 |
77 | export type DeletePostMutation = {
78 | deletePost: {
79 | __typename: 'PostPayload';
80 | post: {
81 | __typename: 'Post';
82 | id: string;
83 | };
84 | errors: any;
85 | messages: Array | null;
86 | };
87 | };
88 |
89 | export type PostForEditingQueryVariables = {
90 | id: string;
91 | };
92 |
93 | export type PostForEditingQuery = {
94 | post: PostForEditingFragment | null;
95 | };
96 |
97 | export type PostQueryVariables = {
98 | id: string;
99 | };
100 |
101 | export type PostQuery = {
102 | post: PostFragment | null;
103 | };
104 |
105 | export type PostsQueryVariables = {
106 | offset?: number | null;
107 | keywords?: string | null;
108 | };
109 |
110 | export type PostsQuery = {
111 | postsCount: number;
112 | posts: Array | null;
113 | };
114 |
115 | export type UpdatePostMutationVariables = {
116 | id: string;
117 | title?: string | null;
118 | content?: string | null;
119 | };
120 |
121 | export type UpdatePostMutation = {
122 | updatePost: {
123 | __typename: 'PostPayload';
124 | post: PostForEditingFragment | null;
125 | errors: any;
126 | messages: Array | null;
127 | };
128 | };
129 |
130 | export type CancelAccountMutation = {
131 | cancelAccount: {
132 | errors: any;
133 | };
134 | };
135 |
136 | export type ChangePasswordMutationVariables = {
137 | password: string;
138 | passwordConfirmation: string;
139 | currentPassword: string;
140 | };
141 |
142 | export type ChangePasswordMutation = {
143 | changePassword: {
144 | __typename: 'UserPayload';
145 | errors: any;
146 | messages: Array | null;
147 | };
148 | };
149 |
150 | export type CurrentUserQuery = {
151 | currentUser: User | null;
152 | };
153 |
154 | export type SignUpMutationVariables = {
155 | name: string;
156 | email: string;
157 | password: string;
158 | passwordConfirmation: string;
159 | };
160 |
161 | export type SignUpMutation = {
162 | signUp: {
163 | __typename: 'UserPayload';
164 | currentUser: {
165 | __typename: 'User';
166 | id: string;
167 | name: string;
168 | email: string;
169 | token: string;
170 | } | null;
171 | errors: any;
172 | messages: Array | null;
173 | };
174 | };
175 |
176 | export type UpdateUserMutationVariables = {
177 | name?: string | null;
178 | email?: string | null;
179 | };
180 |
181 | export type UpdateUserMutation = {
182 | updateUser: {
183 | __typename: 'UserPayload';
184 | user: UserForEditingFragment | null;
185 | errors: any;
186 | messages: Array | null;
187 | };
188 | };
189 |
190 | export type GetUserForEditingQuery = {
191 | // Fetch the current user
192 | currentUser: UserForEditingFragment | null;
193 | };
194 |
195 | export type CommentFragment = {
196 | __typename: 'Comment';
197 | id: string;
198 | content: string;
199 | created_at: string;
200 | author: {
201 | __typename: string;
202 | name: string;
203 | };
204 | };
205 |
206 | export type PostForEditingFragment = {
207 | __typename: 'Post';
208 | id: string;
209 | title: string;
210 | content: string;
211 | description: string;
212 | };
213 |
214 | export type PostFragment = {
215 | __typename: 'Post';
216 | id: string;
217 | title: string;
218 | content: string;
219 | description: string;
220 | created_at: string;
221 | author: {
222 | __typename: string;
223 | id: string;
224 | name: string;
225 | };
226 | comments: Array;
227 | };
228 |
229 | export type PostPreviewFragment = {
230 | __typename: 'Post';
231 | id: string;
232 | title: string;
233 | description: string;
234 | created_at: string;
235 | author: {
236 | __typename: string;
237 | id: string;
238 | name: string;
239 | };
240 | };
241 |
242 | export type UserForEditingFragment = {
243 | __typename: 'User';
244 | id: string;
245 | name: string;
246 | email: string;
247 | };
248 |
249 | export type ValidationMessage = {
250 | __typename: 'ValidationMessage';
251 | field: string;
252 | message: string;
253 | };
254 |
255 | export type User = {
256 | __typename: 'User';
257 | id: string;
258 | name: string;
259 | email: string;
260 | };
261 |
--------------------------------------------------------------------------------
/client/src/utils/errorsUtils.ts:
--------------------------------------------------------------------------------
1 | // typings
2 | import { ValidationMessage } from 'types';
3 |
4 | export default function formatErrors(errors: Array) {
5 | if (!errors || errors.length === 0) {
6 | return null;
7 | }
8 | const errorsFormatted: any = {};
9 | errors.forEach(error => {
10 | errorsFormatted[error.field || 'base'] = error.message;
11 | });
12 | return errorsFormatted;
13 | }
14 |
--------------------------------------------------------------------------------
/client/src/utils/stringUtils.ts:
--------------------------------------------------------------------------------
1 | export default function capitalize(str: string) {
2 | return `${str.slice(0, 1).toUpperCase()}${str.slice(1)}`;
3 | }
4 |
--------------------------------------------------------------------------------
/client/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src",
4 | "rootDir": "src",
5 | "outDir": "build",
6 | "module": "esnext",
7 | "target": "es5",
8 | "lib": [
9 | "es6",
10 | "dom",
11 | "esnext"
12 | ],
13 | "sourceMap": true,
14 | "allowJs": true,
15 | "jsx": "react",
16 | "moduleResolution": "node",
17 | "pretty": true,
18 | "forceConsistentCasingInFileNames": false,
19 | "removeComments": true,
20 | "skipLibCheck": true,
21 | "strict": true,
22 | "noImplicitReturns": true,
23 | "traceResolution": true,
24 | "noUnusedLocals": true,
25 | "noUnusedParameters": true,
26 | "allowSyntheticDefaultImports": true
27 | },
28 | "include": [
29 | "./typings/*",
30 | "./src/**/*.ts",
31 | "./src/**/*.tsx"
32 | ],
33 | "exclude": [
34 | "node_modules",
35 | "build",
36 | "config-overrides"
37 | ]
38 | }
--------------------------------------------------------------------------------
/client/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint:latest", "tslint-config-prettier"],
3 | "rules": {
4 | "no-implicit-dependencies": false,
5 | "no-submodule-imports": false,
6 | "curly": [true, "ignore-same-line"],
7 | "interface-over-type-literal": false,
8 | "object-literal-sort-keys": false,
9 | "ordered-imports": false,
10 | "member-ordering": false,
11 | "array-type": [true, "generic"]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/client/typings/apollo-mutation-state.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'apollo-mutation-state';
2 |
--------------------------------------------------------------------------------
/client/typings/form.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'react-final-form';
2 |
--------------------------------------------------------------------------------
/client/typings/graphql.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.graphql' {
2 | import { DocumentNode } from 'graphql';
3 |
4 | const value: DocumentNode;
5 | export = value;
6 | }
7 |
--------------------------------------------------------------------------------
/client/typings/image.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.png';
2 |
--------------------------------------------------------------------------------
/client/typings/lodash.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'lodash.flowright';
2 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require_relative 'config/environment'
4 |
5 | run Rails.application
6 |
--------------------------------------------------------------------------------
/config/application.rb:
--------------------------------------------------------------------------------
1 | require_relative 'boot'
2 |
3 | require 'rails'
4 | # Pick the frameworks you want:
5 | require 'active_model/railtie'
6 | require 'active_job/railtie'
7 | require 'active_record/railtie'
8 | require 'action_controller/railtie'
9 | require 'action_mailer/railtie'
10 | require 'action_view/railtie'
11 | require 'action_cable/engine'
12 | require 'sprockets/railtie'
13 | require 'rails/test_unit/railtie'
14 |
15 | # Require the gems listed in Gemfile, including any gems
16 | # you've limited to :test, :development, or :production.
17 | Bundler.require(*Rails.groups)
18 |
19 | module GraphqlRailsBlog
20 | class Application < Rails::Application
21 | # Initialize configuration defaults for originally generated Rails version.
22 | config.load_defaults 5.1
23 |
24 | config.api_only = true
25 |
26 | # Settings in config/environments/* take precedence over those specified here.
27 | # Application configuration should go into files in config/initializers
28 | # -- all .rb files in that directory are automatically loaded.
29 |
30 | config.autoload_paths << Rails.root.join('app/graph')
31 | config.autoload_paths << Rails.root.join('app/graph/utils')
32 | config.autoload_paths << Rails.root.join('app/graph/mutations')
33 | config.autoload_paths << Rails.root.join('app/graph/types')
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/config/cable.yml:
--------------------------------------------------------------------------------
1 | development:
2 | adapter: async
3 |
4 | test:
5 | adapter: async
6 |
7 | production:
8 | adapter: redis
9 | url: redis://localhost:6379/1
10 | channel_prefix: graphql_rails_blog_production
11 |
--------------------------------------------------------------------------------
/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require_relative 'application'
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports.
13 | config.consider_all_requests_local = true
14 |
15 | # Enable/disable caching. By default caching is disabled.
16 | if Rails.root.join('tmp/caching-dev.txt').exist?
17 | config.action_controller.perform_caching = true
18 |
19 | config.cache_store = :memory_store
20 | config.public_file_server.headers = {
21 | 'Cache-Control' => "public, max-age=#{2.days.seconds.to_i}"
22 | }
23 | else
24 | config.action_controller.perform_caching = false
25 |
26 | config.cache_store = :null_store
27 | end
28 |
29 | # Don't care if the mailer can't send.
30 | config.action_mailer.raise_delivery_errors = false
31 |
32 | config.action_mailer.perform_caching = false
33 |
34 | # Print deprecation notices to the Rails logger.
35 | config.active_support.deprecation = :log
36 |
37 | # Raise an error on page load if there are pending migrations.
38 | config.active_record.migration_error = :page_load
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 |
43 | # Use an evented file watcher to asynchronously detect changes in source code,
44 | # routes, locales, etc. This feature depends on the listen gem.
45 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker
46 |
47 | config.middleware.insert_before 0, Rack::Cors do
48 | allow do
49 | origins 'localhost:8080'
50 | resource '*', headers: :any, credentials: true, methods: [:get, :post, :put, :patch, :delete, :head, :options], expose: ['X-Requested-With', 'Content-Type', 'Accept']
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Attempt to read encrypted secrets from `config/secrets.yml.enc`.
18 | # Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
19 | # `config/secrets.yml.key`.
20 | config.read_encrypted_secrets = true
21 |
22 | # Disable serving static files from the `/public` folder by default since
23 | # Apache or NGINX already handles this.
24 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
25 |
26 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
27 |
28 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
29 | # config.action_controller.asset_host = 'http://assets.example.com'
30 |
31 | # Specifies the header that your server uses for sending files.
32 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
33 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
34 |
35 | # Mount Action Cable outside main process or domain
36 | # config.action_cable.mount_path = nil
37 | # config.action_cable.url = 'wss://example.com/cable'
38 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
39 |
40 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
41 | # config.force_ssl = true
42 |
43 | # Use the lowest log level to ensure availability of diagnostic information
44 | # when problems arise.
45 | config.log_level = :debug
46 |
47 | # Prepend all log lines with the following tags.
48 | config.log_tags = [ :request_id ]
49 |
50 | # Use a different cache store in production.
51 | # config.cache_store = :mem_cache_store
52 |
53 | # Use a real queuing backend for Active Job (and separate queues per environment)
54 | # config.active_job.queue_adapter = :resque
55 | # config.active_job.queue_name_prefix = "graphql_rails_blog_#{Rails.env}"
56 | config.action_mailer.perform_caching = false
57 |
58 | # Ignore bad email addresses and do not raise email delivery errors.
59 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
60 | # config.action_mailer.raise_delivery_errors = false
61 |
62 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
63 | # the I18n.default_locale when a translation cannot be found).
64 | config.i18n.fallbacks = true
65 |
66 | # Send deprecation notices to registered listeners.
67 | config.active_support.deprecation = :notify
68 |
69 | # Use default logging formatter so that PID and timestamp are not suppressed.
70 | config.log_formatter = ::Logger::Formatter.new
71 |
72 | # Use a different logger for distributed setups.
73 | # require 'syslog/logger'
74 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
75 |
76 | if ENV["RAILS_LOG_TO_STDOUT"].present?
77 | logger = ActiveSupport::Logger.new(STDOUT)
78 | logger.formatter = config.log_formatter
79 | config.logger = ActiveSupport::TaggedLogging.new(logger)
80 | end
81 |
82 | # Do not dump schema after migrations.
83 | config.active_record.dump_schema_after_migration = false
84 | end
85 |
--------------------------------------------------------------------------------
/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure public file server for tests with Cache-Control for performance.
16 | config.public_file_server.enabled = true
17 | config.public_file_server.headers = {
18 | 'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}"
19 | }
20 |
21 | # Show full error reports and disable caching.
22 | config.consider_all_requests_local = true
23 | config.action_controller.perform_caching = false
24 |
25 | # Raise exceptions instead of rendering exception templates.
26 | config.action_dispatch.show_exceptions = false
27 |
28 | # Disable request forgery protection in test environment.
29 | config.action_controller.allow_forgery_protection = false
30 | config.action_mailer.perform_caching = false
31 |
32 | # Tell Action Mailer not to deliver emails to the real world.
33 | # The :test delivery method accumulates sent emails in the
34 | # ActionMailer::Base.deliveries array.
35 | config.action_mailer.delivery_method = :test
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/config/initializers/application_controller_renderer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # ApplicationController.renderer.defaults.merge!(
4 | # http_host: 'example.org',
5 | # https: false
6 | # )
7 |
--------------------------------------------------------------------------------
/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Specify a serializer for the signed and encrypted cookie jars.
4 | # Valid options are :json, :marshal, and :hybrid.
5 | Rails.application.config.action_dispatch.cookies_serializer = :json
6 |
--------------------------------------------------------------------------------
/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/config/initializers/graphql_patch.rb:
--------------------------------------------------------------------------------
1 | module GraphQL
2 | class Query
3 | class Arguments
4 | def to_params
5 | ActionController::Parameters.new(self.to_h).permit(*self.to_h.keys)
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/config/initializers/optics_agent.rb:
--------------------------------------------------------------------------------
1 | if Rails.env.production?
2 | optics_agent = OpticsAgent::Agent.new
3 | optics_agent.configure { schema Schema }
4 | Rails.application.config.middleware.use optics_agent.rack_middleware
5 | end
6 |
--------------------------------------------------------------------------------
/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json]
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # The following keys must be escaped otherwise they will not be retrieved by
20 | # the default I18n backend:
21 | #
22 | # true, false, on, off, yes, no
23 | #
24 | # Instead, surround them with single quotes.
25 | #
26 | # en:
27 | # 'true': 'foo'
28 | #
29 | # To learn more, please read the Rails Internationalization guide
30 | # available at http://guides.rubyonrails.org/i18n.html.
31 |
32 | en:
33 | hello: "Hello world"
34 |
--------------------------------------------------------------------------------
/config/newrelic.yml:
--------------------------------------------------------------------------------
1 | #
2 | # This file configures the New Relic Agent. New Relic monitors Ruby, Java,
3 | # .NET, PHP, Python and Node applications with deep visibility and low
4 | # overhead. For more information, visit www.newrelic.com.
5 | #
6 | # Generated February 09, 2017
7 | #
8 | # This configuration file is custom generated for app57702606@heroku.com
9 | #
10 | # For full documentation of agent configuration options, please refer to
11 | # https://docs.newrelic.com/docs/agents/ruby-agent/installation-configuration/ruby-agent-configuration
12 |
13 | common: &default_settings
14 | # Required license key associated with your New Relic account.
15 | license_key: 872fe674f71c666a88bb94b15fb8c657ea372a66
16 |
17 | # Your application name. Renaming here affects where data displays in New
18 | # Relic. For more details, see https://docs.newrelic.com/docs/apm/new-relic-apm/maintenance/renaming-applications
19 | app_name: graphql-rails-blog
20 |
21 | # To disable the agent regardless of other settings, uncomment the following:
22 | # agent_enabled: false
23 |
24 | # Logging level for log/newrelic_agent.log
25 | log_level: info
26 |
27 |
28 | # Environment-specific settings are in this section.
29 | # RAILS_ENV or RACK_ENV (as appropriate) is used to determine the environment.
30 | # If your application has other named environments, configure them here.
31 | development:
32 | <<: *default_settings
33 | app_name: graphql-rails-blog (Development)
34 |
35 | # NOTE: There is substantial overhead when running in developer mode.
36 | # Do not use for production or load testing.
37 | developer_mode: true
38 |
39 | test:
40 | <<: *default_settings
41 | # It doesn't make sense to report to New Relic from automated test runs.
42 | monitor_mode: false
43 |
44 | staging:
45 | <<: *default_settings
46 | app_name: graphql-rails-blog (Staging)
47 |
48 | production:
49 | <<: *default_settings
50 |
--------------------------------------------------------------------------------
/config/puma.rb:
--------------------------------------------------------------------------------
1 | # Puma can serve each request in a thread from an internal thread pool.
2 | # The `threads` method setting takes two numbers: a minimum and maximum.
3 | # Any libraries that use thread pools should be configured to match
4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum
5 | # and maximum; this matches the default thread size of Active Record.
6 | #
7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
8 | threads threads_count, threads_count
9 |
10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000.
11 | #
12 | port ENV.fetch("PORT") { 3000 }
13 |
14 | # Specifies the `environment` that Puma will run in.
15 | #
16 | environment ENV.fetch("RAILS_ENV") { "development" }
17 |
18 | # Specifies the number of `workers` to boot in clustered mode.
19 | # Workers are forked webserver processes. If using threads and workers together
20 | # the concurrency of the application would be max `threads` * `workers`.
21 | # Workers do not work on JRuby or Windows (both of which do not support
22 | # processes).
23 | #
24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 }
25 |
26 | # Use the `preload_app!` method when specifying a `workers` number.
27 | # This directive tells Puma to first boot the application and load code
28 | # before forking the application. This takes advantage of Copy On Write
29 | # process behavior so workers use less memory. If you use this option
30 | # you need to make sure to reconnect any threads in the `on_worker_boot`
31 | # block.
32 | #
33 | # preload_app!
34 |
35 | # If you are preloading your application and using Active Record, it's
36 | # recommended that you close any connections to the database before workers
37 | # are forked to prevent connection leakage.
38 | #
39 | # before_fork do
40 | # ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord)
41 | # end
42 |
43 | # The code in the `on_worker_boot` will be called if you are using
44 | # clustered mode by specifying a number of `workers`. After each worker
45 | # process is booted, this block will be run. If you are using the `preload_app!`
46 | # option, you will want to use this block to reconnect to any threads
47 | # or connections that may have been created at application boot, as Ruby
48 | # cannot share connections between processes.
49 | #
50 | # on_worker_boot do
51 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
52 | # end
53 | #
54 |
55 | # Allow puma to be restarted by `rails restart` command.
56 | plugin :tmp_restart
57 |
--------------------------------------------------------------------------------
/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | root "application#index"
3 | post "/graphql", to: "graphql#create"
4 |
5 | if Rails.env.development?
6 | mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
7 | end
8 |
9 | get '*all', to: 'application#index'
10 | end
11 |
--------------------------------------------------------------------------------
/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rails secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | # Shared secrets are available across all environments.
14 |
15 | # shared:
16 | # api_key: a1B2c3D4e5F6
17 |
18 | # Environmental secrets are only available for that specific environment.
19 |
20 | development:
21 | secret_key_base: 398dbc5ed9db909beb8335b6af107714786128ff0cd5cb6471f08464797088ade7d0ab3dcc1c2e2eb310590267474970563c21fa903ca3093e0844c5f8ae282a
22 |
23 | test:
24 | secret_key_base: 080877cfd2200b87b5591da22e92372acca2a4d4e641b04fc10d642f5c43c479d7fc47bd82318c9ac9cd851dd088725fc2e79297f4ca7b53de466e97ef75a217
25 |
26 | # Do not keep production secrets in the unencrypted secrets file.
27 | # Instead, either read values from the environment.
28 | # Or, use `bin/rails secrets:setup` to configure encrypted secrets
29 | # and move the `production:` environment over there.
30 |
31 | production:
32 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
33 |
--------------------------------------------------------------------------------
/config/spring.rb:
--------------------------------------------------------------------------------
1 | %w(
2 | .ruby-version
3 | .rbenv-vars
4 | tmp/restart.txt
5 | tmp/caching-dev.txt
6 | ).each { |path| Spring.watch(path) }
7 |
--------------------------------------------------------------------------------
/db/migrate/20161111124620_create_posts.rb:
--------------------------------------------------------------------------------
1 | class CreatePosts < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :posts do |t|
4 | t.string :title
5 | t.text :content
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20161111124813_create_comments.rb:
--------------------------------------------------------------------------------
1 | class CreateComments < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :comments do |t|
4 | t.text :content
5 | t.references :post, index: true, foreign_key: true
6 |
7 | t.timestamps
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/db/migrate/20161111130057_devise_create_users.rb:
--------------------------------------------------------------------------------
1 | class DeviseCreateUsers < ActiveRecord::Migration[5.0]
2 | def change
3 | create_table :users do |t|
4 | ## Database authenticatable
5 | t.string :name, null: false, default: ""
6 | t.string :email, null: false, default: ""
7 | t.string :encrypted_password, null: false, default: ""
8 | t.boolean :admin, default: false, null: false
9 |
10 | ## Recoverable
11 | t.string :reset_password_token
12 | t.datetime :reset_password_sent_at
13 |
14 | ## Rememberable
15 | t.datetime :remember_created_at
16 |
17 | ## Trackable
18 | t.integer :sign_in_count, default: 0, null: false
19 | t.datetime :current_sign_in_at
20 | t.datetime :last_sign_in_at
21 | t.string :current_sign_in_ip
22 | t.string :last_sign_in_ip
23 |
24 | ## Confirmable
25 | # t.string :confirmation_token
26 | # t.datetime :confirmed_at
27 | # t.datetime :confirmation_sent_at
28 | # t.string :unconfirmed_email # Only if using reconfirmable
29 |
30 | ## Lockable
31 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
32 | # t.string :unlock_token # Only if unlock strategy is :email or :both
33 | # t.datetime :locked_at
34 |
35 |
36 | t.timestamps null: false
37 | end
38 |
39 | add_index :users, :email, unique: true
40 | add_index :users, :reset_password_token, unique: true
41 | # add_index :users, :confirmation_token, unique: true
42 | # add_index :users, :unlock_token, unique: true
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/db/migrate/20161111130300_add_user_reference_to_posts_and_comments.rb:
--------------------------------------------------------------------------------
1 | class AddUserReferenceToPostsAndComments < ActiveRecord::Migration[5.0]
2 | def change
3 | add_reference :posts, :user, index: true, foreign_key: true
4 | add_reference :comments, :user, index: true, foreign_key: true
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/db/migrate/20161111130742_add_comments_count_to_posts.rb:
--------------------------------------------------------------------------------
1 | class AddCommentsCountToPosts < ActiveRecord::Migration[5.0]
2 | def change
3 | add_column :posts, :comments_count, :integer, null: false, default: 0
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20161111133933_create_active_admin_comments.rb:
--------------------------------------------------------------------------------
1 | class CreateActiveAdminComments < ActiveRecord::Migration[5.0]
2 | def self.up
3 | create_table :active_admin_comments do |t|
4 | t.string :namespace
5 | t.text :body
6 | t.string :resource_id, null: false
7 | t.string :resource_type, null: false
8 | t.references :author, polymorphic: true
9 | t.timestamps
10 | end
11 | add_index :active_admin_comments, [:namespace]
12 | #add_index :active_admin_comments, [:author_type, :author_id]
13 | add_index :active_admin_comments, [:resource_type, :resource_id]
14 | end
15 |
16 | def self.down
17 | drop_table :active_admin_comments
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/db/migrate/20171017143939_add_refresh_token_to_users.rb:
--------------------------------------------------------------------------------
1 | class AddRefreshTokenToUsers < ActiveRecord::Migration[5.1]
2 | def change
3 | add_column :users, :refresh_token, :string
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20171019193932_rename_password_from_users.rb:
--------------------------------------------------------------------------------
1 | class RenamePasswordFromUsers < ActiveRecord::Migration[5.1]
2 | def change
3 | rename_column :users, :encrypted_password, :password_digest
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/migrate/20171124131242_rename_token_to_users.rb:
--------------------------------------------------------------------------------
1 | class RenameTokenToUsers < ActiveRecord::Migration[5.1]
2 | def change
3 | rename_column :users, :refresh_token, :access_token
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/db/schema.rb:
--------------------------------------------------------------------------------
1 | # This file is auto-generated from the current state of the database. Instead
2 | # of editing this file, please use the migrations feature of Active Record to
3 | # incrementally modify your database, and then regenerate this schema definition.
4 | #
5 | # Note that this schema.rb definition is the authoritative source for your
6 | # database schema. If you need to create the application database on another
7 | # system, you should be using db:schema:load, not running all the migrations
8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9 | # you'll amass, the slower it'll run and the greater likelihood for issues).
10 | #
11 | # It's strongly recommended that you check this file into your version control system.
12 |
13 | ActiveRecord::Schema.define(version: 20171124131242) do
14 |
15 | create_table "active_admin_comments", force: :cascade do |t|
16 | t.string "namespace"
17 | t.text "body"
18 | t.string "resource_id", null: false
19 | t.string "resource_type", null: false
20 | t.string "author_type"
21 | t.integer "author_id"
22 | t.datetime "created_at", null: false
23 | t.datetime "updated_at", null: false
24 | t.index ["author_type", "author_id"], name: "index_active_admin_comments_on_author_type_and_author_id"
25 | t.index ["namespace"], name: "index_active_admin_comments_on_namespace"
26 | t.index ["resource_type", "resource_id"], name: "index_active_admin_comments_on_resource_type_and_resource_id"
27 | end
28 |
29 | create_table "comments", force: :cascade do |t|
30 | t.text "content"
31 | t.integer "post_id"
32 | t.datetime "created_at", null: false
33 | t.datetime "updated_at", null: false
34 | t.integer "user_id"
35 | t.index ["post_id"], name: "index_comments_on_post_id"
36 | t.index ["user_id"], name: "index_comments_on_user_id"
37 | end
38 |
39 | create_table "posts", force: :cascade do |t|
40 | t.string "title"
41 | t.text "content"
42 | t.datetime "created_at", null: false
43 | t.datetime "updated_at", null: false
44 | t.integer "user_id"
45 | t.integer "comments_count", default: 0, null: false
46 | t.index ["user_id"], name: "index_posts_on_user_id"
47 | end
48 |
49 | create_table "users", force: :cascade do |t|
50 | t.string "name", default: "", null: false
51 | t.string "email", default: "", null: false
52 | t.string "password_digest", default: "", null: false
53 | t.boolean "admin", default: false, null: false
54 | t.string "reset_password_token"
55 | t.datetime "reset_password_sent_at"
56 | t.datetime "remember_created_at"
57 | t.integer "sign_in_count", default: 0, null: false
58 | t.datetime "current_sign_in_at"
59 | t.datetime "last_sign_in_at"
60 | t.string "current_sign_in_ip"
61 | t.string "last_sign_in_ip"
62 | t.datetime "created_at", null: false
63 | t.datetime "updated_at", null: false
64 | t.string "access_token"
65 | t.index ["email"], name: "index_users_on_email", unique: true
66 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
67 | end
68 |
69 | end
70 |
--------------------------------------------------------------------------------
/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
7 | # Character.create(name: 'Luke', movie: movies.first)
8 |
9 | Comment.destroy_all
10 | Post.destroy_all
11 | User.destroy_all
12 |
13 | # Create Admin
14 | user = User.create! name: "Admin", email: "admin@graphql-rails-blog.com", password: "password", password_confirmation: "password"
15 | user.admin = true
16 | user.save!
17 |
18 | # Posts association
19 | 10.times do
20 | post = user.posts.create! title: Faker::Lorem.sentence(rand(2..4)), content: Faker::Lorem.paragraph(rand(30..50))
21 | rand(0..3).times do
22 | user.comments.create! content: Faker::Lorem.paragraph(rand(1..5)), post: post
23 | end
24 | end
25 |
26 | # Create users
27 | 10.times do |i|
28 | user = User.create! name: "user#{i}", email: "user#{i}@graphql-rails-blog.com", password: "password", password_confirmation: "password"
29 | post = user.posts.create! title: Faker::Lorem.sentence(rand(2..4)), content: Faker::Lorem.paragraph(rand(30..50))
30 | rand(0..3).times do
31 | user.comments.create! content: Faker::Lorem.paragraph(rand(1..5)), post: post
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/assets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/lib/assets/.keep
--------------------------------------------------------------------------------
/lib/tasks/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/lib/tasks/.keep
--------------------------------------------------------------------------------
/log/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/log/.keep
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphql-rails-blog",
3 | "version": "1.0.0",
4 | "description": "Blog App build with Rails 5, React and GraphQL",
5 | "engines": {
6 | "node": "9.2.0",
7 | "npm": "5.5.1"
8 | },
9 | "repository": {
10 | "url": "git@github.com:MatthieuSegret/graphql-rails-blog.git",
11 | "type": "git"
12 | },
13 | "scripts": {
14 | "start": "cd client && npm start",
15 | "build:clean": "rimraf ./public/*",
16 | "build": "cd client && npm install && npm run build && cd ..",
17 | "deploy": "npm run build:clean && cp -a client/build/. public/",
18 | "postinstall": "npm run build && npm run deploy && echo 'Client built!'"
19 | },
20 | "author": "Matthieu Segret ",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/MatthieuSegret/graphql-rails-blog/issues"
24 | },
25 | "homepage": "https://github.com/MatthieuSegret/graphql-rails-blog#readme",
26 | "devDependencies": {
27 | "rimraf": "^2.6.2"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/test/controllers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/test/controllers/.keep
--------------------------------------------------------------------------------
/test/fixtures/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/test/fixtures/.keep
--------------------------------------------------------------------------------
/test/fixtures/comments.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | content: MyText
5 | post: one
6 |
7 | two:
8 | content: MyText
9 | post: two
10 |
--------------------------------------------------------------------------------
/test/fixtures/files/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/test/fixtures/files/.keep
--------------------------------------------------------------------------------
/test/fixtures/posts.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | one:
4 | title: MyString
5 | content: MyText
6 |
7 | two:
8 | title: MyString
9 | content: MyText
10 |
--------------------------------------------------------------------------------
/test/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | # Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
2 |
3 | # This model initially had no columns defined. If you add columns to the
4 | # model remove the '{}' from the fixture names and add the columns immediately
5 | # below each fixture, per the syntax in the comments below
6 | #
7 | one: {}
8 | # column: value
9 | #
10 | two: {}
11 | # column: value
12 |
--------------------------------------------------------------------------------
/test/helpers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/test/helpers/.keep
--------------------------------------------------------------------------------
/test/integration/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/test/integration/.keep
--------------------------------------------------------------------------------
/test/mailers/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/test/mailers/.keep
--------------------------------------------------------------------------------
/test/models/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/test/models/.keep
--------------------------------------------------------------------------------
/test/models/comment_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class CommentTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/post_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class PostTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/models/user_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class UserTest < ActiveSupport::TestCase
4 | # test "the truth" do
5 | # assert true
6 | # end
7 | end
8 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | ENV['RAILS_ENV'] ||= 'test'
2 | require File.expand_path('../../config/environment', __FILE__)
3 | require 'rails/test_help'
4 |
5 | class ActiveSupport::TestCase
6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
7 | fixtures :all
8 |
9 | # Add more helper methods to be used by all tests here...
10 | end
11 |
--------------------------------------------------------------------------------
/tmp/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/tmp/.keep
--------------------------------------------------------------------------------
/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/vendor/assets/javascripts/.keep
--------------------------------------------------------------------------------
/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MatthieuSegret/graphql-rails-blog/6423881bec223522f8af09cd4b18905ca20fef0f/vendor/assets/stylesheets/.keep
--------------------------------------------------------------------------------