WIP: Basic Jodel Application

This commit is contained in:
Pete Gerlach
2024-06-02 13:48:26 +02:00
parent d2778cd922
commit f36ea7a34b
11 changed files with 384 additions and 215 deletions

View File

@@ -1,18 +1,126 @@
import React, { useState } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import Home from './components/Home';
import PostDetail from './components/PostDetail';
import PostList from './components/PostList';
import NewPostForm from './components/NewPostForm';
import Modal from './components/Modal';
import './App.scss';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCaretDown, faCaretUp} from "@fortawesome/free-solid-svg-icons";
const App = () => {
const [posts, setPosts] = useState([]);
const [selectedPost, setSelectedPost] = useState(null);
const [comment, setComment] = useState('');
const addPost = (text) => {
const newPost = { id: Date.now(), text, comments: [], upvotes: 0, downvotes: 0 };
setPosts([newPost, ...posts]);
};
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) => {
setPosts(posts.map(post =>
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) => {
setSelectedPost(post);
};
const handleCommentSubmit = (e) => {
e.preventDefault();
if (comment.trim() && selectedPost) {
addComment(selectedPost.id, comment);
setComment('');
}
};
const closeModal = () => {
setSelectedPost(null);
setComment('');
};
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/post/:id" element={<PostDetail />} />
</Routes>
</Router>
<div className="app">
<h1>Jodel Clone</h1>
<NewPostForm addPost={addPost} />
<PostList
posts={posts}
addComment={addComment}
upvotePost={upvotePost}
downvotePost={downvotePost}
upvoteComment={upvoteComment}
downvoteComment={downvoteComment}
onPostClick={handlePostClick}
/>
<Modal isOpen={!!selectedPost} onClose={closeModal}>
{selectedPost && (
<div>
<p>{selectedPost.text}</p>
<form className="comment-form" onSubmit={handleCommentSubmit}>
<input
type="text"
value={comment}
onChange={(e) => setComment(e.target.value)}
placeholder="Kommentieren..."
/>
<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={() => 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>
)}
</Modal>
</div>
);
}
export default App;

View File

@@ -1,33 +1,160 @@
body {
font-family: Arial, sans-serif;
background-color: #f6f7f9;
color: #4a4a4a;
padding: 20px;
}
.vote {
margin-left: 10px;
display: inline-block;
font-size: 20px;
color: #999;
}
.vote:hover {
cursor: pointer;
}
.container {
max-width: 600px;
.app {
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: white;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.post {
background-color: #fff;
border: 1px solid #ddd;
padding: 10px;
h1 {
color: #ff9908;
text-align: center;
font-weight: normal;
margin-bottom: 20px;
}
.comment {
background-color: #fff;
border: 1px solid #ddd;
padding: 10px;
.new-post-form {
display: flex;
flex-direction: column;
margin-bottom: 20px;
input[type="text"] {
padding: 15px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 10px;
font-size: 1em;
}
button {
padding: 15px;
background-color: #ff9908;
color: white;
border: none;
border-radius: 10px;
font-size: 1em;
cursor: pointer;
&:hover {
background-color: #e68a00;
}
}
}
.post-list {
.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;
flex-direction: column;
align-items: center;
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;
border: 1px solid #ddd;
border-radius: 10px;
margin-right: 10px;
font-size: 1em;
}
button {
padding: 10px;
background-color: #ff9908;
color: white;
border: none;
border-radius: 10px;
cursor: pointer;
font-size: 1em;
&:hover {
background-color: #e68a00;
}
}
}
.comments {
margin-top: 10px;
.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 {
font-size: 1em;
margin-bottom: 5px;
}
}
}
}
}
}

View File

@@ -1,38 +0,0 @@
import React, { useState } from 'react';
import WritePost from './WritePost';
import '../App.scss';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faCaretDown, faCaretUp} from "@fortawesome/free-solid-svg-icons"; // Importiere das Styling
function Home() {
const [posts, setPosts] = useState([]);
const handlePost = function(content) {
const newPost = {
id: posts.length + 1,
content: content,
upvotes: 0,
downvotes: 0
};
setPosts([...posts, newPost]);
};
return (
<div className="container"> {/* Verwende die Klasse container */}
<h1>Jodel Clone</h1>
<WritePost onPost={handlePost} />
{posts.map(post => (
<div key={post.id} className="post"> {/* Verwende die Klasse post */}
<p>{post.content}</p>
<div className="vote">
<FontAwesomeIcon icon={faCaretUp} onClick={() => console.log("upvote")} />
<FontAwesomeIcon icon={faCaretDown} onClick={() => console.log("downvote")} />
{post.upvotes - post.downvotes}
</div>
</div>
))}
</div>
);
}
export default Home;

21
src/components/Modal.js Normal file
View File

@@ -0,0 +1,21 @@
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;

34
src/components/Modal.scss Normal file
View File

@@ -0,0 +1,34 @@
.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;
}
}

View File

@@ -0,0 +1,27 @@
import React, { useState } from 'react';
const NewPostForm = ({ addPost }) => {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
addPost(text);
setText('');
}
};
return (
<form className="new-post-form" onSubmit={handleSubmit}>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="Schreibe einen neuen Jodel..."
/>
<button type="submit">Posten</button>
</form>
);
};
export default NewPostForm;

View File

@@ -1,34 +1,24 @@
import React, { useState } from 'react';
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCaretUp, faCaretDown } from '@fortawesome/free-solid-svg-icons';
function Post({ post }) {
const [upvotes, setUpvotes] = useState(post.upvotes);
const [downvotes, setDownvotes] = useState(post.downvotes);
const handleUpvote = () => {
// Logic to handle upvote
setUpvotes(upvotes + 1);
};
const handleDownvote = () => {
// Logic to handle downvote
setDownvotes(downvotes + 1);
};
const Post = ({ post, upvotePost, downvotePost }) => {
return (
<div className="post">
<p>{post.content}</p>
<div className="vote">
<FontAwesomeIcon icon={faCaretUp} onClick={handleUpvote} />
{upvotes}
<div className="post-content">
<p>{post.text}</p>
</div>
<div className="vote">
<FontAwesomeIcon icon={faCaretDown} onClick={handleDownvote} />
{downvotes}
<div className="votes">
<button onClick={(e) => { e.stopPropagation(); upvotePost(post.id); }}>
<FontAwesomeIcon icon={faCaretUp} />
</button>
<span>{post.upvotes - post.downvotes}</span>
<button onClick={(e) => { e.stopPropagation(); downvotePost(post.id); }}>
<FontAwesomeIcon icon={faCaretDown} />
</button>
</div>
</div>
);
}
};
export default Post;

View File

@@ -1,47 +0,0 @@
import React, { useState } from 'react';
import WriteComment from './WriteComment';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCaretUp, faCaretDown } from '@fortawesome/free-solid-svg-icons';
import '../App.scss'; // Importiere das Styling
function PostDetail() {
const [comments, setComments] = useState([]);
const handleComment = function(comment) {
setComments([...comments, comment]);
};
const calculateVotes = function(comment) {
return comment.upvotes - comment.downvotes;
};
const handleVote = function(index, type) {
const updatedComments = [...comments];
if (type === 'upvote') {
updatedComments[index].upvotes++;
} else if (type === 'downvote') {
updatedComments[index].downvotes++;
}
setComments(updatedComments);
};
return (
<div className="container"> {/* Verwende die Klasse container */}
<h2>Post Detail</h2>
<h3>Comments</h3>
{comments.map((comment, index) => (
<div key={index} className="comment"> {/* Verwende die Klasse comment */}
<p>{comment.content}</p>
<div className="vote">
<FontAwesomeIcon icon={faCaretUp} onClick={() => handleVote(index, 'upvote')} />
<FontAwesomeIcon icon={faCaretDown} onClick={() => handleVote(index, 'downvote')} />
{calculateVotes(comment)}
</div>
</div>
))}
<WriteComment onComment={handleComment} />
</div>
);
}
export default PostDetail;

View File

@@ -0,0 +1,20 @@
import React from 'react';
import Post from './Post';
const PostList = ({ posts, upvotePost, downvotePost, onPostClick }) => {
return (
<div className="post-list">
{posts.map(post => (
<div key={post.id} onClick={() => onPostClick(post)}>
<Post
post={post}
upvotePost={upvotePost}
downvotePost={downvotePost}
/>
</div>
))}
</div>
);
};
export default PostList;

View File

@@ -1,34 +0,0 @@
import React, { useState } from 'react';
function WriteComment({ onComment }) {
const [content, setContent] = useState('');
const handleChange = function(e) {
setContent(e.target.value);
};
const handleSubmit = function(e) {
e.preventDefault();
onComment({ content: content, upvotes: 0, downvotes: 0 });
setContent('');
};
return (
<div>
<h2>Write a Comment</h2>
<form onSubmit={handleSubmit}>
<textarea
value={content}
onChange={handleChange}
placeholder="Write your comment here..."
rows={4}
cols={50}
/>
<br />
<button type="submit">Submit</button>
</form>
</div>
);
}
export default WriteComment;

View File

@@ -1,39 +0,0 @@
import React, { useState } from 'react';
function WritePost({ onPost }) {
const [content, setContent] = useState('');
const handleChange = function(e) {
setContent(e.target.value);
};
const handleSubmit = function(e) {
e.preventDefault();
// Hier die Logik für das Hinzufügen des Posts implementieren
// z.B. eine Funktion ausführen, die von der übergeordneten Komponente übergeben wurde
onPost(content);
// Setze den Inhaltsbereich zurück
setContent('');
};
return React.createElement(
'div',
null,
React.createElement('h2', null, 'Write a Post'),
React.createElement(
'form',
{ onSubmit: handleSubmit },
React.createElement('textarea', {
value: content,
onChange: handleChange,
placeholder: 'Write your post here...',
rows: 4,
cols: 50
}),
React.createElement('br', null),
React.createElement('button', { type: 'submit' }, 'Submit')
)
);
}
export default WritePost;