Comments section #5

Merged
innocuous-symmetry merged 7 commits from comments-section into master 2022-01-30 00:26:22 +00:00
8 changed files with 194 additions and 52 deletions

View 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>
)
}

View File

@@ -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 (
<>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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;

View File

@@ -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()}/>
)
})
}

View File

@@ -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>
);
}