restructured code (modal => internal view)

This commit is contained in:
Pete Gerlach
2024-06-11 15:26:17 +02:00
parent 0ab53963fb
commit 5ef0b504e1
4 changed files with 213 additions and 267 deletions

View File

@@ -1,12 +1,12 @@
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import PostList from './components/PostList'; import PostList from './components/PostList';
import NewPostForm from './components/NewPostForm'; import NewPostForm from './components/NewPostForm';
import Modal from './components/Modal';
import './App.scss'; import './App.scss';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCaretDown, faCaretUp} from "@fortawesome/free-solid-svg-icons"; import {faCaretDown, faCaretUp, faTimes} from "@fortawesome/free-solid-svg-icons";
import {postApi} from "./api/posts.api"; import {postApi} from "./api/posts.api";
import {locationUtils} from "./utils/location"; import {locationUtils} from "./utils/location";
import Post from "./components/Post";
const App = () => { const App = () => {
const [posts, setPosts] = useState([]); const [posts, setPosts] = useState([]);
@@ -15,7 +15,9 @@ const App = () => {
useEffect(() => { useEffect(() => {
if(posts.length === 0) { if(posts.length === 0) {
loadPosts().then(data => setPosts(data)) loadPosts().then(data => setPosts(data));
} else {
console.log(posts);
} }
}) })
@@ -24,50 +26,17 @@ const App = () => {
return await postApi.getPostsByCoords(location.lon, location.lat); return await postApi.getPostsByCoords(location.lon, location.lat);
} }
const addPost = (text) => { const addPost = async (title, text) => {
const newPost = { id: Date.now(), text, comments: [], upvotes: 0, downvotes: 0 }; await postApi.createNewPost(title, text);
setPosts([newPost, ...posts]); loadPosts().then(data => setPosts(data));
}; };
const addComment = (postId, commentText) => { const addComment = (postId, commentText) => {
const newComment = { id: Date.now(), text: commentText, upvotes: 0, downvotes: 0 };
setPosts(posts.map(post =>
post.id === postId ? { ...post, comments: [...post.comments, newComment] } : post
));
}; };
const upvotePost = (postId) => { const vote = (postId, reaction) => {
setPosts(posts.map(post => // TODO postApi.reactToPost(postId)
post.id === postId ? { ...post, upvotes: post.upvotes + 1 } : post
));
};
const downvotePost = (postId) => {
setPosts(posts.map(post =>
post.id === postId ? { ...post, downvotes: post.downvotes + 1 } : post
));
};
const upvoteComment = (postId, commentId) => {
setPosts(posts.map(post =>
post.id === postId ? {
...post,
comments: post.comments.map(comment =>
comment.id === commentId ? { ...comment, upvotes: comment.upvotes + 1 } : comment
)
} : post
));
};
const downvoteComment = (postId, commentId) => {
setPosts(posts.map(post =>
post.id === postId ? {
...post,
comments: post.comments.map(comment =>
comment.id === commentId ? { ...comment, downvotes: comment.downvotes + 1 } : comment
)
} : post
));
}; };
const handlePostClick = (post) => { const handlePostClick = (post) => {
@@ -82,56 +51,66 @@ const App = () => {
} }
}; };
const closeModal = () => { const closePost = () => {
setSelectedPost(null); setSelectedPost(null);
setComment(''); setComment('');
}; };
return ( return (
<div className="app"> <div className="app">
<h1>SWA - Jodel</h1> {!selectedPost && (
<NewPostForm addPost={addPost} /> <div>
<PostList <h1>SWA - Jodel</h1>
posts={posts} <NewPostForm addPost={addPost}/>
addComment={addComment} <PostList
upvotePost={upvotePost} posts={posts}
downvotePost={downvotePost} addComment={addComment}
upvoteComment={upvoteComment} vote={vote}
downvoteComment={downvoteComment} onPostClick={handlePostClick}
onPostClick={handlePostClick} />
/> </div>
<Modal isOpen={!!selectedPost} onClose={closeModal}> )}
{selectedPost && ( {selectedPost && (
<div> <div className="single-post-view">
<p>{selectedPost.text}</p> <div className="close-post">
<form className="comment-form" onSubmit={handleCommentSubmit}> <FontAwesomeIcon className={"close-button"} onClick={closePost} icon={faTimes}/>
<input </div>
type="text" <Post post={selectedPost} vote={vote}></Post>
<form className="comment-form" onSubmit={handleCommentSubmit}>
<div className="textarea-container">
<textarea
rows={15}
value={comment} value={comment}
onChange={(e) => setComment(e.target.value)} onChange={(e) => setComment(e.target.value)}
placeholder="Kommentieren..." placeholder="Kommentieren..."
maxLength={500}
/> />
<button type="submit">Kommentieren</button> <div className="char-count-container">
</form> <span className={comment.length === 500 ? "char-count max-char-count" : "char-count"}>
<div className="comments"> {comment.length} / 500
{selectedPost.comments.map((comment) => ( </span>
<div key={comment.id} className="comment"> </div>
<p>{comment.text}</p>
<div className="votes">
<button onClick={() => upvoteComment(selectedPost.id, comment.id)}>
<FontAwesomeIcon icon={faCaretUp} />
</button>
<span>{comment.upvotes - comment.downvotes}</span>
<button onClick={() => downvoteComment(selectedPost.id, comment.id)}>
<FontAwesomeIcon icon={faCaretDown} />
</button>
</div>
</div>
))}
</div> </div>
<button type="submit">Kommentieren</button>
</form>
<div className="comments">
{selectedPost.comments.map((comment) => (
<div key={comment.id} className="comment">
<p>{comment.text}</p>
<div className="votes">
<button onClick={() => vote(selectedPost.id, comment.id)}>
<FontAwesomeIcon icon={faCaretUp}/>
</button>
<span>{comment.upvotes - comment.downvotes}</span>
<button onClick={() => vote(selectedPost.id, comment.id)}>
<FontAwesomeIcon icon={faCaretDown}/>
</button>
</div>
</div>
))}
</div> </div>
)} </div>
</Modal> )}
</div> </div>
); );
} }

View File

@@ -30,49 +30,6 @@ h3 {
flex-direction: column; flex-direction: column;
margin-bottom: 20px; margin-bottom: 20px;
input[type="text"] {
padding: 15px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 1em;
}
textarea {
width: 100%;
box-sizing: border-box;
padding: 15px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 1em;
font-family: Arial, sans-serif;
&.single-row {
white-space: nowrap;
}
}
.textarea-container {
position: relative;
width: 100%;
.char-count-container {
position: absolute;
bottom: 8px;
right: 12px;
font-size: 12px;
color: grey;
background: white;
padding: 2px 4px;
border-radius: 4px;
}
.max-char-count {
background-color: rgba(255, 0, 0, 0.25);
}
}
button { button {
padding: 15px; padding: 15px;
background-color: #ff9908; background-color: #ff9908;
@@ -88,112 +45,177 @@ h3 {
} }
} }
.post-list { .post {
.post { display: flex;
justify-content: space-between;
align-items: flex-start;
border: 1px solid #ddd;
margin: 10px 0;
padding: 15px;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
.post-content {
flex: 1;
margin-right: 20px;
}
.votes {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: flex-start; align-items: center;
border: 1px solid #ddd;
margin: 10px 0;
padding: 15px;
border-radius: 10px;
background-color: #fff;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
.post-content { button {
padding: 10px;
margin-bottom: 5px;
background-color: transparent;
color: #ff9908;
border: none;
cursor: pointer;
font-size: 1.5em;
&:hover {
color: #e68a00;
}
}
span {
font-size: 1.2em;
margin-bottom: 5px;
}
}
.comment-form {
display: flex;
margin-top: 10px;
input[type="text"] {
padding: 10px;
flex: 1; flex: 1;
margin-right: 20px; border: 1px solid #ddd;
border-radius: 10px;
margin-right: 10px;
font-size: 1em;
} }
.votes { button {
display: flex; padding: 10px;
flex-direction: column; background-color: #ff9908;
align-items: center; color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 1em;
button { &:hover {
padding: 10px; background-color: #e68a00;
margin-bottom: 5px;
background-color: transparent;
color: #ff9908;
border: none;
cursor: pointer;
font-size: 1.5em;
&:hover {
color: #e68a00;
}
}
span {
font-size: 1.2em;
margin-bottom: 5px;
} }
} }
}
.comment-form { .comments {
display: flex; margin-top: 10px;
margin-top: 10px;
input[type="text"] { .comment {
padding: 10px; padding: 10px;
flex: 1; border: 1px solid #eee;
border: 1px solid #ddd; border-radius: 10px;
border-radius: 10px; background-color: #fafafa;
margin-right: 10px; margin-bottom: 10px;
font-size: 1em;
}
button { .votes {
padding: 10px; display: flex;
background-color: #ff9908; flex-direction: column;
color: white; align-items: center;
border: none; margin-top: 5px;
border-radius: 10px;
cursor: pointer;
font-size: 1em;
&:hover { button {
background-color: #e68a00; padding: 10px;
} margin-bottom: 5px;
} background-color: transparent;
} color: #ff9908;
border: none;
cursor: pointer;
font-size: 1.5em;
.comments { &:hover {
margin-top: 10px; color: #e68a00;
.comment {
padding: 10px;
border: 1px solid #eee;
border-radius: 10px;
background-color: #fafafa;
margin-bottom: 10px;
.votes {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 5px;
button {
padding: 10px;
margin-bottom: 5px;
background-color: transparent;
color: #ff9908;
border: none;
cursor: pointer;
font-size: 1.5em;
&:hover {
color: #e68a00;
}
} }
}
span { span {
font-size: 1em; font-size: 1em;
margin-bottom: 5px; margin-bottom: 5px;
}
} }
} }
} }
} }
} }
.single-post-view {
.close-post {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.close-button {
font-size: 1.5em;
cursor: pointer;
&:hover {
color: #ff9908;
}
}
}
textarea {
width: 100%;
box-sizing: border-box;
padding: 15px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 1em;
font-family: Arial, sans-serif;
&.single-row {
white-space: nowrap;
}
}
.textarea-container {
position: relative;
width: 100%;
.char-count-container {
position: absolute;
bottom: 8px;
right: 12px;
font-size: 12px;
color: grey;
background: white;
padding: 2px 4px;
border-radius: 4px;
}
.max-char-count {
background-color: rgba(255, 0, 0, 0.25);
}
}
button {
padding: 15px;
background-color: #ff9908;
color: white;
border: none;
width: 100%;
border-radius: 10px;
font-size: 1em;
cursor: pointer;
&:hover {
background-color: #e68a00;
}
}

View File

@@ -1,21 +0,0 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import './Modal.scss';
const Modal = ({ isOpen, onClose, children }) => {
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div className="modal">
<button className="close-button" onClick={onClose}>
<FontAwesomeIcon icon={faTimes} />
</button>
{children}
</div>
</div>
);
};
export default Modal;

View File

@@ -1,34 +0,0 @@
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal {
background: white;
padding: 20px;
border-radius: 10px;
width: 90%;
max-width: 600px;
position: relative;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
background: transparent;
border: none;
font-size: 1.5em;
cursor: pointer;
&:hover {
color: #ff9908;
}
}