📬 Post (#12)

* update deploy test

* switch picture to avatarUrl; addLike and removeLike methods placeholders

* fix signup bug

* fetch posts from db, can post and like posts

* fetch posts from db, can post and like posts

* add params to post route

* connect to redux store

* can add comments
This commit is contained in:
Ruidy 2020-05-20 15:07:55 +02:00 committed by GitHub
parent 3c00f9e999
commit 4880d2853d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 245 additions and 119 deletions

View file

@ -47,6 +47,6 @@ jobs:
- name: Deploy to Firebase
uses: w9jds/firebase-action@master
with:
args: deploy
args: deploy --only hosting
env:
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}

View file

@ -0,0 +1,9 @@
/**
* Register all firestore collections
*/
enum Collections {
USERS = 'users',
POSTS = 'posts',
}
export default Collections;

View file

@ -6,8 +6,8 @@ import Repo from '../types/Repo';
/** Shorter dev interface */
export interface DevSummary {
id?: string;
displayName: string;
avatarUrl: string;
displayName?: string;
avatarUrl?: string;
description: string;
status: string;
company: string;
@ -40,18 +40,15 @@ export const getDescription = (status?: string, company?: string): string => {
* new Dev() returns a placeholder used when initializing a new profile.
* id is not specified to not overwrite document uid.
*/
export class Dev implements IDev {
id?: string;
isActive = true;
displayName = '';
status = 'Developer';
company = '';
avatarUrl = '';
description = '';
location = '';
skills: string[] = [];
github: string = '';
links: Links = {
export const blankDev: IDev = {
isActive: true,
status: 'Developer',
company: '',
description: '',
location: '',
skills: [],
github: '',
links: {
website: '',
instagram: '',
facebook: '',
@ -59,12 +56,12 @@ export class Dev implements IDev {
twitter: '',
github: '',
youtube: '',
},
bio: '',
experiences: [],
educations: [],
repos: [],
};
bio = '';
experiences: Experience[] = [];
educations: Education[] = [];
repos: Repo[] = [];
}
/**
* sample Dev for development and tests

View file

@ -5,10 +5,10 @@ import Comment from '../types/Comment';
*/
interface Post {
id: string;
userID: string;
name: string;
userID?: string;
name?: string;
text: string;
picture: string;
avatarUrl?: string;
likes: string[];
comments: Comment[];
// date: Date;
@ -20,21 +20,21 @@ interface Post {
export const dummyPost: Post = {
id: '12',
userID: '42',
picture:
avatarUrl:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
name: 'John Doe',
text:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint possimus corporis sunt necessitatibus! Minus nesciunt soluta suscipit nobis. Amet accusamus distinctio cupiditate blanditiis dolor? Illo perferendis eveniet cum cupiditate aliquam?',
comments: [
{
picture:
avatarUrl:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
name: 'John Doe',
text:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Sintpossimus corporis sunt necessitatibus! Minus nesciunt solutasuscipit nobis. Amet accusamus distinctio cupiditate blanditiis dolor? Illo perferendis eveniet cum cupiditate aliquam?',
},
{
picture:
avatarUrl:
'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200',
name: 'Ruidy Nemo',
text:

View file

@ -1,22 +1,58 @@
import React, {FC} from 'react';
import Post, {dummyPost as post} from '../models/Post';
// Routing
import {useParams, Link} from 'react-router-dom';
import Routes from '../constants/routes';
// Redux
import {compose} from '@reduxjs/toolkit';
import {connect} from 'react-redux';
import {firestoreConnect, WithFirestoreProps} from 'react-redux-firebase';
import {RootState} from '../store';
import Collections from '../constants/collections';
// Typing
import Post from '../models/Post';
import Comment from '../types/Comment';
// Form
import useForm from '../hooks';
interface IProps extends WithFirestoreProps {
post: Post;
}
/**
* Display a Post and the related comments. Shows a form to add a comment.
*/
const PostPage: FC<Post> = () => (
const PostPage: FC<IProps> = ({post, firestore}) => {
const newComment: Comment = {
name: post.name ?? 'error',
avatarUrl: post.avatarUrl ?? 'error',
text: '',
};
const {formData, handleChange, resetForm} = useForm(newComment);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
firestore
.update(`${Collections.POSTS}/${post.id}`, {
comments: [...post.comments, formData],
})
.then(() => resetForm())
.catch(err => console.error(err));
};
return (
<section className="container">
<a href="posts.html" className="btn btn-light">
<Link to={Routes.POSTS} className="btn btn-light">
Back To Posts
</a>
</Link>
<div className="post bg-white p-1 my-1">
<div>
<a href="profile.html">
<img className="round-img" src={post.picture} alt={post.name} />
<Link to={`${Routes.PROFILE}/${post.userID}`}>
<img className="round-img" src={post.avatarUrl} alt={post.name} />
<h4>{post.name}</h4>
</a>
</Link>
</div>
<div>
<p className="my-1">{post.text}</p>
@ -27,23 +63,25 @@ const PostPage: FC<Post> = () => (
<div className="post-form-header bg-primary">
<h3>Leave A Comment</h3>
</div>
<form className="form my-1">
<form className="form my-1" onSubmit={handleSubmit}>
<textarea
name="text"
cols={30}
rows={5}
placeholder="Comment on this post"
value={formData.text}
onChange={handleChange}
></textarea>
<input type="submit" className="btn btn-dark my-1" value="Submit" />
</form>
</div>
<div className="posts">
{post.comments.map((c: Comment, i: number) => (
{post.comments?.map((c: Comment, i: number) => (
<div className="post bg-white p-1 my-1" key={i}>
<div>
<a href="profile.html">
<img className="round-img" src={c.picture} alt={c.name} />
<img className="round-img" src={c.avatarUrl} alt={c.name} />
<h4>{c.name}</h4>
</a>
</div>
@ -55,5 +93,21 @@ const PostPage: FC<Post> = () => (
</div>
</section>
);
};
export default PostPage;
/**
* Container to fetch id params from thr URI and pass it to Profile page
*/
const PostPageContainer: FC = () => {
const {id} = useParams();
const Component = compose<FC>(
firestoreConnect(() => [`${Collections.POSTS}/${id}`]),
connect(({firestore: {data}}: RootState) => ({
post: data.posts && {...data.posts[id], id},
})),
)(PostPage);
return <Component />;
};
export default PostPageContainer;

View file

@ -1,14 +1,65 @@
import React, {FC} from 'react';
import Post, {dummyPost as post} from '../models/Post';
import Header from '../components/Header';
import React, {FC, useState} from 'react';
// Redux
import {compose} from '@reduxjs/toolkit';
import {connect, useSelector} from 'react-redux';
import {firestoreConnect, WithFirestoreProps} from 'react-redux-firebase';
import {RootState} from '../store';
import {selectProfile} from '../store/firebase';
// Routing
import {Link} from 'react-router-dom';
import Routes from '../constants/routes';
// Style
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faThumbsUp, faThumbsDown} from '@fortawesome/free-solid-svg-icons';
import {faThumbsUp} from '@fortawesome/free-solid-svg-icons';
import Header from '../components/Header';
// Typing
import Post, {dummyPost as post} from '../models/Post';
import Collections from '../constants/collections';
interface IProps extends WithFirestoreProps {
posts: Post[];
}
/**
* A Dev's Posts list
*/
const Posts: FC = () => {
const posts: Post[] = [post, post];
const Posts: FC<IProps> = ({posts, firestore, firebase}) => {
const [text, setText] = useState('');
const {avatarUrl, displayName} = useSelector(selectProfile);
const id = firebase.auth().currentUser?.uid;
const newPost = {
userID: id,
name: displayName,
text,
avatarUrl,
likes: [] as string[],
comments: [],
};
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) =>
setText(e.target.value);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
firestore
.add(Collections.POSTS, newPost)
.then(() => setText(''))
.catch(err => console.error(err));
};
// const removeLike = (e: React.MouseEvent<HTMLButtonElement>) =>
// new Error('Not implemented yet.');
const addLike = (postID: string) => (
e: React.MouseEvent<HTMLButtonElement>,
) => {
e.preventDefault();
const post = posts.find(p => p.id === postID);
if (post) {
firestore
.update(`${Collections.POSTS}/${post.id}`, {likes: [...post.likes, id]})
.catch(err => console.error(err));
}
};
return (
<section className="container">
@ -18,41 +69,55 @@ const Posts: FC = () => {
<h3>Say Something</h3>
</div>
<form className="form my-1">
<textarea cols={30} rows={5} placeholder="Create a post"></textarea>
<form className="form my-1" onSubmit={handleSubmit}>
<textarea
cols={30}
rows={5}
placeholder="Create a post"
value={text}
onChange={handleChange}
/>
<input type="submit" value="Submit" className="btn btn-dark my-1" />
</form>
<div className="posts">
{posts.map((post: Post) => (
{posts?.map((post: Post) => (
<div className="post bg-white p-1 my-1" key={post.id}>
<div>
<a href="profile.html">
<Link to={`${Routes.PROFILE}/${post.userID}`}>
<img
src={post.picture}
src={post.avatarUrl}
alt={post.name}
className="round-img"
/>
<h4>{post.name}</h4>
</a>
</Link>
</div>
<div>
<p className="my-1">{post.text}</p>
<button className="btn btn-light">
<FontAwesomeIcon icon={faThumbsUp} /> {post.likes.length}
<button className="btn btn-light" onClick={addLike(post.id)}>
<FontAwesomeIcon icon={faThumbsUp} /> {post.likes?.length}
</button>
<button className="btn btn-light">
{/* <button className="btn btn-light" onClick={removeLike}>
<FontAwesomeIcon icon={faThumbsDown} />
</button>
<a href="post.html" className="btn btn-primary">
</button> */}
<Link
to={`${Routes.POST}/${post.id}`}
className="btn btn-primary"
>
Discussion
</a>
</Link>
</div>
</div>
))}
</div>
</form>
</div>
</section>
);
};
export default Posts;
export default compose<FC>(
firestoreConnect(() => ['posts']), // or { collection: 'users' }
connect((state: RootState) => ({
posts: state.firestore.ordered.posts,
})),
)(Posts);

View file

@ -5,17 +5,18 @@ import Routes from '../constants/routes';
// Redux
import {WithFirebaseProps} from 'react-redux-firebase';
import {enhance} from '../store/firebase';
import User, {newUser} from '../models/User';
// Style
import GoogleButton from 'react-google-button';
import Alert from '../components/Alert';
import Header from '../components/Header';
// Typing
import IDev, {blankDev} from '../models/Dev';
import User, {newUser} from '../models/User';
// Form
import useForm from '../hooks';
import {Dev} from '../models/Dev';
// extends withFirebaseProps type to ad profile info
interface IProps extends Dev, WithFirebaseProps<User> {
interface IProps extends IDev, WithFirebaseProps<User> {
isEmpty: boolean;
isLoaded: boolean;
}
@ -57,7 +58,7 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
firebase
.createUser({email, password}, newUser(name, email))
.then(() => {
firebase.updateProfile(new Dev(), {useSet: true, merge: true});
firebase.updateProfile(blankDev, {useSet: true, merge: true});
resetForm();
})
.catch(err => setError(err));
@ -82,7 +83,7 @@ const SignUp: FC<IProps> = ({firebase, isEmpty, isLoaded, isActive}) => {
)
.then(() => {
if (!exists)
firebase.updateProfile(new Dev(), {useSet: true, merge: true});
firebase.updateProfile(blankDev, {useSet: true, merge: true});
});
})
.catch(err => setError(err));

View file

@ -31,7 +31,7 @@ const Router: FC = () => (
component={AddExperience}
/>
<PrivateRoute exact path={Routes.ADD_EDUCATION} component={AddEducation} />
<PrivateRoute exact path={Routes.POST} component={PostPage} />
<PrivateRoute exact path={`${Routes.POST}/:id`} component={PostPage} />
<PrivateRoute exact path={Routes.POSTS} component={Posts} />
<Route component={NotFound} />
</Switch>

View file

@ -2,7 +2,7 @@ interface Comment {
// userID: string;
text: string;
name: string;
picture: string;
avatarUrl: string;
// date: Date;
}