Connecting features #2
3
package-lock.json
generated
3
package-lock.json
generated
@@ -16,6 +16,9 @@
|
||||
"react-dom": "^17.0.2",
|
||||
"react-redux": "^7.2.6",
|
||||
"react-scripts": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
|
||||
@@ -32,5 +32,8 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
55
src/App.css
55
src/App.css
@@ -1,5 +1,7 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -9,4 +11,55 @@
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.navbar > * {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.feed {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.about-the-app {
|
||||
display: hidden;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 12rem;
|
||||
position: fixed;
|
||||
background-color: black;
|
||||
color: white;
|
||||
right: 0;
|
||||
top: 5rem;
|
||||
padding: 1.5rem;
|
||||
transition: right 0.6s ease-out;
|
||||
}
|
||||
|
||||
.sidebar-hidden {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 12rem;
|
||||
position: fixed;
|
||||
background-color: black;
|
||||
color: white;
|
||||
right: -15rem;
|
||||
top: 5rem;
|
||||
padding: 1.5rem;
|
||||
transition: right 0.6s ease-out;
|
||||
}
|
||||
20
src/App.js
20
src/App.js
@@ -1,13 +1,29 @@
|
||||
import React from 'react';
|
||||
import './App.css';
|
||||
import Navbar from './features/navbar/Navbar';
|
||||
import redditSlice from './features/reddit/redditSlice';
|
||||
import Post from './features/posts/Post';
|
||||
import Feed from './features/posts/Feed';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<Navbar />
|
||||
<p>Stuff</p>
|
||||
<div className="content-container">
|
||||
|
||||
<div className="feed">
|
||||
{/* To do: import posts from post directory */}
|
||||
{/* Map post data onto individual post cards, handle undefined values */}
|
||||
<Feed />
|
||||
</div>
|
||||
|
||||
<div className="about-the-site">
|
||||
{/* To do: add mutable state to class name for this div, */}
|
||||
{/* determining whether or not it's active based on the state of */}
|
||||
{/* The action dispatched from the searchbar slice(?) */}
|
||||
{/* Do I need a searchbar slice? */}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import SearchBar from "../searchBar/searchBar";
|
||||
import Sidebar from "../sidebar/Sidebar";
|
||||
|
||||
export default function Navbar() {
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
|
||||
const handleCollapse = () => {
|
||||
setCollapsed(!collapsed);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="navbar">
|
||||
<h1>Reddit but it's all cats</h1>
|
||||
<p>Search bar here</p>
|
||||
<p>Expand sidebar here</p>
|
||||
<SearchBar />
|
||||
<button onClick={handleCollapse}>Sidebar</button>
|
||||
</div>
|
||||
<div className="sidebar-container">
|
||||
<Sidebar isCollapsed={collapsed} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
55
src/features/posts/Feed.js
Normal file
55
src/features/posts/Feed.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import postsSlice, { fetchBySub, updatePosts, selectPosts } from "./postsSlice";
|
||||
import { selectAllSubs } from "../reddit/redditSlice";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { v4 } from "uuid";
|
||||
import Post from "./Post";
|
||||
|
||||
export default function Feed() {
|
||||
const [feed, setFeed] = useState(null);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
|
||||
const getPosts = async() => {
|
||||
let myPosts = await dispatch(fetchBySub('https://www.reddit.com/r/cats.json'));
|
||||
myPosts = myPosts.payload;
|
||||
console.log(myPosts);
|
||||
|
||||
if (typeof myPosts === 'object' && isActive) {
|
||||
let newFeed = [];
|
||||
for (let post of myPosts) {
|
||||
newFeed.push(
|
||||
<Post
|
||||
title={post.data.title}
|
||||
author={post.data.author}
|
||||
subreddit={post.data.subreddit}
|
||||
ups={post.data.ups}
|
||||
comments={post.data.num_comments}
|
||||
time={post.data.created_utc}
|
||||
id={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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
setFeed(newFeed);
|
||||
}
|
||||
};
|
||||
getPosts();
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
}
|
||||
|
||||
}, [dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
{feed ? feed : <h1>Loading cats for you...</h1>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
30
src/features/posts/Post.css
Normal file
30
src/features/posts/Post.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.post-body {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
width: 75%;
|
||||
padding: 2rem;
|
||||
border: 1px solid black;
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
display: inline-flex;
|
||||
background-color: grey;
|
||||
width: 30%;
|
||||
height: 15rem;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
img, video {
|
||||
max-height: 45rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.post-metadata {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import './Post.css';
|
||||
|
||||
export default function Post({title,author,subreddit,ups,comments,time,id,media,permalink,selftext,video}) {
|
||||
const limit = 300;
|
||||
const [body, setBody] = useState(selftext);
|
||||
|
||||
useEffect(() => {
|
||||
if (selftext.length > limit) {
|
||||
setBody(selftext.substring(0,limit) + '...');
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="post-body">
|
||||
<a className="title" href={`https://reddit.com${permalink}`}>{title ? title : 'title'}</a>
|
||||
{media ? <img alt={title} src={media} /> : ''}
|
||||
{video ? <video controls type="video/mp4" src={video}></video> : ''}
|
||||
<p>{body}</p>
|
||||
<div className="post-metadata">
|
||||
<p className="user">{author ? 'u/' + author : 'u/username'}</p>
|
||||
<p className="time-posted">{time ? time : ''}</p>
|
||||
<p className="num-comments">{comments ? comments : 'comments'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
|
||||
|
||||
export const fetchBySub = createAsyncThunk(
|
||||
'reddit/fetchBySub',
|
||||
'posts/fetchBySub',
|
||||
async(subreddit) => { // expects an argument corresponding to the url, in json format, of a given subreddit
|
||||
try {
|
||||
const myRequest = new Request(subreddit); // initializes request
|
||||
@@ -25,7 +25,10 @@ export const postsSlice = createSlice({
|
||||
reducers: {
|
||||
filterPosts(state,action) { // Expects action.payload to be the searchterm imported from the state of searchBar
|
||||
state.posts.filter(post => (post.data.title !== action.payload) && (post.data.selftext !== action.payload));
|
||||
}
|
||||
},
|
||||
updatePosts(state,action) {
|
||||
state.posts = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(fetchBySub.pending, (state,action) => {
|
||||
@@ -47,4 +50,6 @@ export const postsSlice = createSlice({
|
||||
});
|
||||
|
||||
export default postsSlice.reducer;
|
||||
export const { filterPosts } = postsSlice.actions;
|
||||
export const selectPosts = state => state.postsSlice.posts;
|
||||
export const { filterPosts, updatePosts } = postsSlice.actions;
|
||||
// exports also includes fetchBySub (takes argument of a sub)
|
||||
@@ -73,4 +73,5 @@ export const redditSlice = createSlice({
|
||||
});
|
||||
|
||||
export default redditSlice.reducer;
|
||||
export const selectAllSubs = state => state.redditSlice.subreddits;
|
||||
export const { updateSubVisibility } = redditSlice.actions;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
export default function SearchBar() {
|
||||
const [term, setTerm] = useState('');
|
||||
|
||||
const handleChange = (e) => {
|
||||
e.preventDefault();
|
||||
setTerm(e.target.value);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (term) {
|
||||
// dispatch an action which filters content by {term}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}, [term])
|
||||
|
||||
return (
|
||||
<>
|
||||
<input type="text" className="searchbar" placeholder="Search posts" value={term ? term : ''} onChange={handleChange} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export default function Sidebar({isCollapsed}) {
|
||||
return (
|
||||
<div className={isCollapsed ? 'sidebar-hidden' : 'sidebar'}>
|
||||
<p>Hard coded subreddit</p>
|
||||
<p>Hard coded subreddit</p>
|
||||
<p>Hard coded subreddit</p>
|
||||
<p>Hard coded subreddit</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user