Comments section #5
56
src/features/discussion/Discussion.js
Normal file
56
src/features/discussion/Discussion.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { unwrapResult } from '@reduxjs/toolkit';
|
||||
import { fetchComments, isPending } from '../posts/postsSlice';
|
||||
|
||||
export default function Discussion({permalink, isVisible}) {
|
||||
const [thread, setThread] = useState(null);
|
||||
const [data, setData] = useState(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isLoading = useSelector(isPending);
|
||||
|
||||
const formattedLink = permalink.substring(0,(permalink.length-1));
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
if (isActive) {
|
||||
if (isVisible === 'hide ') {
|
||||
dispatch(fetchComments(formattedLink))
|
||||
.then(unwrapResult)
|
||||
.then((result) => setData(result));
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
isActive = false;
|
||||
}
|
||||
}, [isVisible, thread, formattedLink, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
let commentData = data[1];
|
||||
console.log(commentData);
|
||||
|
||||
let commentArray = commentData.data.children;
|
||||
console.log(commentArray);
|
||||
|
||||
let toExport = [];
|
||||
for (let comment of commentArray) {
|
||||
toExport.push(
|
||||
<>
|
||||
<p>{'u/' + comment.data.author}</p>
|
||||
<p>{comment.data.body}</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
setThread(toExport);
|
||||
}
|
||||
}, [data, thread]);
|
||||
|
||||
return (
|
||||
<div className="discussion-thread">
|
||||
{thread}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { fetchBySub } from "./postsSlice";
|
||||
import { fetchBySub, selectPosts } from "./postsSlice";
|
||||
import { selectAllSubs } from "../reddit/redditSlice";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { updatePosts } from "./postsSlice";
|
||||
@@ -11,17 +11,21 @@ export default function Feed() {
|
||||
const [data, setData] = useState(null); // Receives data from getPosts and them maps it onto Post components
|
||||
const [feed, setFeed] = useState(null); // Expects to receive an array of Post components mapped with data from fetchBySub
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const subs = useSelector(selectAllSubs); // Selects subreddits from redditSlice
|
||||
const subArray = Object.values(subs); // Places the values within an array
|
||||
|
||||
const posts = useSelector(selectPosts);
|
||||
const subs = useSelector(selectAllSubs); // Selects subreddits from redditSlice
|
||||
|
||||
|
||||
useEffect(() => { // this useEffect loop pulls the endpoints from the selected subreddits and stores them as an array in "endpoints"
|
||||
useEffect(() => { // this useEffect loop pulls the endpoints from the selected subreddits and stores them as an array in "endpoints"
|
||||
const subArray = Object.values(subs); // extracts values from selected subs
|
||||
|
||||
const prepareData = () => {
|
||||
let myEndpoints = [];
|
||||
for (let sub of subArray) {
|
||||
myEndpoints.push(sub.access);
|
||||
for (let sub of subArray) { // this loop filters subs which are set to isSelected in state
|
||||
if (sub.isSelected) {
|
||||
myEndpoints.push(sub.access);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
setEndpoints(myEndpoints);
|
||||
}
|
||||
@@ -38,14 +42,14 @@ export default function Feed() {
|
||||
|
||||
const getPosts = async(arr) => {
|
||||
if (endpoints) {
|
||||
const mappedResults = arr.map(each => dispatch(fetchBySub(each)));
|
||||
const mappedResults = arr.map(each => dispatch(fetchBySub(each))); // maps each endpoint into a call to dispatch fetchBySub
|
||||
return Promise.all([
|
||||
...mappedResults
|
||||
]).then((results) => setData(results)); // data is now set to an object (returned promise)
|
||||
...mappedResults // ...then promises each of the calls within this array,
|
||||
]).then((results) => setData(results)); // and stores the returned promises in state as data
|
||||
}
|
||||
}
|
||||
|
||||
getPosts(endpoints); // calls this logic on the current value of endpoints
|
||||
getPosts(endpoints);
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
@@ -70,6 +74,8 @@ export default function Feed() {
|
||||
}
|
||||
};
|
||||
|
||||
console.log(extractedPosts);
|
||||
|
||||
const comparePosts = (a,b) => { // sorting function: compares time posted within each object in array
|
||||
if (a.data.created_utc > b.data.created_utc) {
|
||||
return -1;
|
||||
@@ -82,27 +88,16 @@ export default function Feed() {
|
||||
|
||||
let sortedPosts = extractedPosts.sort(comparePosts); // implements sorting function
|
||||
|
||||
console.log(sortedPosts);
|
||||
|
||||
let newFeed = sortedPosts.map((post) => {
|
||||
return (
|
||||
<Post
|
||||
title={post.data.title} // each variable passed in as props
|
||||
author={post.data.author}
|
||||
subreddit={post.data.subreddit}
|
||||
ups={post.data.ups}
|
||||
comments={post.data.num_comments}
|
||||
time={post.data.created_utc}
|
||||
data={post.data}
|
||||
key={v4()}
|
||||
media={post.data.post_hint === 'image' && post.data.url}
|
||||
permalink={post.data.permalink}
|
||||
selftext={post.data.selftext}
|
||||
video={post.data.is_video ? post.data.media.reddit_video.fallback_url : null} // to do: handle media edge cases, especially video
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
setFeed(newFeed); // stores this array of posts in feed
|
||||
// dispatch(updatePosts(newFeed)); // stores current feed in state of postsSlice
|
||||
setFeed(newFeed);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -115,7 +110,7 @@ export default function Feed() {
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
}, [data, setFeed]);
|
||||
}, [data, setFeed, dispatch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -32,4 +32,14 @@ img, video {
|
||||
|
||||
.post-text {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.comments-visible {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comments-hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -1,13 +1,51 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { fetchComments } from "./postsSlice";
|
||||
import Discussion from "../discussion/Discussion";
|
||||
import './Post.css';
|
||||
|
||||
export default function Post({title,author,subreddit,ups,comments,time,key,media,permalink,selftext,video}) {
|
||||
export default function Post({data, key}) {
|
||||
|
||||
let title = data.title; // imports from data passed in from Feed
|
||||
let author = data.author;
|
||||
let subreddit = data.subreddit;
|
||||
let ups = data.ups;
|
||||
let comments = data.num_comments;
|
||||
let time = data.created_utc;
|
||||
let media = data.post_hint === 'image' && data.url;
|
||||
let permalink = data.permalink;
|
||||
let selftext = data.selftext;
|
||||
let video = data.is_video ? data.media.reddit_video.fallback_url : null; // to do: handle media edge cases, especially video
|
||||
|
||||
const limit = 300;
|
||||
const [body, setBody] = useState(selftext);
|
||||
const [visible, setVisible] = useState('show ');
|
||||
const [commentStyle, setCommentStyle] = useState('comments-hidden');
|
||||
|
||||
const postDate = new Date(time * 1000); // handles conversion from unix timestamp to local time and date strings
|
||||
const localTime = postDate.toLocaleTimeString();
|
||||
const localDate = postDate.toLocaleDateString();
|
||||
|
||||
/*********
|
||||
* Handles hiding/showing comment threads. When this is clicked, the thread is fetched, parsed, and displayed.
|
||||
* Doing so for all posts at once would reduce rendering times.
|
||||
*********/
|
||||
|
||||
const handleClick = () => {
|
||||
if (visible === 'hide ') {
|
||||
setVisible('show ');
|
||||
setCommentStyle('comments-hidden');
|
||||
} else if (visible === 'show ') {
|
||||
setVisible('hide ');
|
||||
setCommentStyle('comments-visible');
|
||||
} else {
|
||||
throw new Error('error in button handling in Post.js');
|
||||
}
|
||||
}
|
||||
|
||||
/*********
|
||||
* Functions below to handle post body so as not to clog visual space
|
||||
*********/
|
||||
|
||||
useEffect(() => {
|
||||
if (selftext.length === 0) { // in the case that the post body is empty, it does not include an ellipsis on mouseout
|
||||
@@ -27,13 +65,12 @@ export default function Post({title,author,subreddit,ups,comments,time,key,media
|
||||
if (selftext.length === 0) { // ...and then doesn't add it in after a mouseover/out
|
||||
return;
|
||||
}
|
||||
|
||||
setBody(selftext.substring(0,limit) + '...');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="post-body">
|
||||
<div className="post-body" key={key}>
|
||||
|
||||
{title ?
|
||||
<a className="title" href={`https://reddit.com${permalink}`}>{title}</a>
|
||||
@@ -56,7 +93,11 @@ export default function Post({title,author,subreddit,ups,comments,time,key,media
|
||||
|
||||
<p className="user">{author ? 'u/' + author : 'u/username'}</p>
|
||||
<p className="time-posted">posted at {time ? (localTime + ' on ' + localDate) : '...?'}</p>
|
||||
<p className="num-comments">{comments ? comments : 'no'} comments</p>
|
||||
<button className="num-comments" onClick={handleClick}>{comments ? visible + comments : 'no'} comments</button>
|
||||
</div>
|
||||
|
||||
<div className={commentStyle}>
|
||||
<Discussion permalink={permalink} isVisible={visible} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -15,10 +15,25 @@ export const fetchBySub = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchComments = createAsyncThunk(
|
||||
'posts/fetchComments',
|
||||
async(permalink) => {
|
||||
try {
|
||||
const myRequest = new Request(`https://www.reddit.com${permalink}.json`);
|
||||
let response = await fetch(myRequest);
|
||||
let postData = await response.json(); // returns an array of two objects, the first containing data about
|
||||
return postData; // the post, the second containing data about the discussion thread
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const postsSlice = createSlice({
|
||||
name: 'posts',
|
||||
initialState: {
|
||||
posts: [],
|
||||
activeComments: [],
|
||||
requestsPending: false,
|
||||
requestDenied: false,
|
||||
},
|
||||
@@ -46,10 +61,26 @@ export const postsSlice = createSlice({
|
||||
state.posts.push(sub); // nesting arrays within the state's posts
|
||||
}
|
||||
})
|
||||
|
||||
builder.addCase(fetchComments.pending, (state,action) => {
|
||||
state.requestsPending = true;
|
||||
state.requestDenied = false;
|
||||
})
|
||||
builder.addCase(fetchComments.rejected, (state,action) => {
|
||||
state.requestsPending = false;
|
||||
state.requestDenied = true;
|
||||
})
|
||||
builder.addCase(fetchComments.fulfilled, (state,action) => {
|
||||
state.requestsPending = false;
|
||||
state.requestDenied = false;
|
||||
state.activeComments.push(action.payload);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
export default postsSlice.reducer;
|
||||
export const selectPosts = state => state.postsSlice.posts;
|
||||
export const isPending = state => state.postsSlice.requestsPending;
|
||||
export const { filterPosts, updatePosts } = postsSlice.actions;
|
||||
// exports also includes fetchBySub (takes argument of a sub)
|
||||
// exports also includes fetchBySub (takes argument of a sub)
|
||||
// exports also includes fetchComments (takes argument of a post permalink)
|
||||
@@ -66,12 +66,22 @@ export const redditSlice = createSlice({
|
||||
reducers: {
|
||||
updateSubVisibility(state,action) { // receives a subreddit name as action.payload
|
||||
state.subreddits[action.payload].isSelected = !state.subreddits[action.payload].isSelected;
|
||||
}
|
||||
},
|
||||
getActiveSubs(state,action) {
|
||||
let activeSubs = [];
|
||||
let allSubs = state.redditSlice.subreddits;
|
||||
for (let sub in allSubs) {
|
||||
if (sub.isSelected) {
|
||||
activeSubs.push(sub);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: {},
|
||||
});
|
||||
|
||||
export default redditSlice.reducer;
|
||||
export const selectAllSubs = state => state.redditSlice.subreddits;
|
||||
export const selectActiveSubs = state => {
|
||||
let activeSubs = [];
|
||||
@@ -84,4 +94,5 @@ export const selectActiveSubs = state => {
|
||||
}
|
||||
return activeSubs;
|
||||
}
|
||||
export const { updateSubVisibility } = redditSlice.actions;
|
||||
export const { updateSubVisibility, getActiveSubs } = redditSlice.actions;
|
||||
export default redditSlice.reducer;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { selectAllSubs } from "../reddit/redditSlice";
|
||||
import { v4 } from 'uuid';
|
||||
import SidebarItem from "./SidebarItem";
|
||||
import './Sidebar.css';
|
||||
|
||||
@@ -26,17 +27,13 @@ export default function Sidebar({isCollapsed}) {
|
||||
}
|
||||
}
|
||||
|
||||
const handleClick = (e) => {
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={isCollapsed ? 'sidebar-hidden' : 'sidebar'}> {/* Is collapsed is passed from the parent component, and is mutable within the navbar */}
|
||||
{
|
||||
subs.map((sub) => { // Maps each sub to its own line within the sidebar, along with a button that toggles its "isSelected" property
|
||||
return (
|
||||
<SidebarItem sub={sub} isChecked={true}/>
|
||||
<SidebarItem sub={sub} key={v4()}/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import React, { useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { updateSubVisibility, selectAllSubs } from "../reddit/redditSlice";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { updateSubVisibility } from "../reddit/redditSlice";
|
||||
|
||||
export default function SidebarItem({sub, isChecked}) {
|
||||
const [checked, setChecked] = useState(isChecked);
|
||||
export default function SidebarItem({sub}) {
|
||||
const [visible, setVisible] = useState('hide');
|
||||
const dispatch = useDispatch();
|
||||
const allSubs = useSelector(selectAllSubs);
|
||||
|
||||
const handleClick = () => {
|
||||
setChecked(!checked);
|
||||
dispatch(updateSubVisibility(sub));
|
||||
if (visible === 'hide') {
|
||||
setVisible('show');
|
||||
} else if (visible === 'show') {
|
||||
setVisible('hide');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(allSubs);
|
||||
|
||||
return (
|
||||
<div className="individual-sub">
|
||||
<input type="checkbox" id={sub} htmlFor={sub} checked={checked} onChange={handleClick}></input>
|
||||
<label htmlFor={sub}>{sub}</label>
|
||||
{/* <input type="checkbox" id={sub} checked={checked} onChange={handleClick}></input> */}
|
||||
<button id={sub} onClick={handleClick}>{visible}</button>
|
||||
<label id={sub}>{sub}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user