source: reactapp/src/Pages/Topic.js@ af801e3

main
Last change on this file since af801e3 was af801e3, checked in by viktor <viktor@…>, 2 years ago

finished edit/delete/displace opinion/thread from report (react); todo reporting user/opinion/thread interface, public user pages and messaging (springboot)

  • Property mode set to 100644
File size: 18.7 KB
Line 
1import React, { useState, useEffect, useContext } from "react";
2import { useParams, Outlet } from "react-router-dom";
3import JSOG from "jsog";
4import { CurrentPageNav } from "../Components/Styled/Main.style";
5import AuthApi from "../api/AuthApi";
6import { useNavigate } from "react-router-dom";
7import {
8 OpinionCard,
9 OpinionCardContent,
10 OpinionCardContentTime,
11 OpinionReplyCard,
12 OpinionReplyCardContent,
13 OpinionReplyCardContentTime,
14 VoteCount,
15} from "../Components/Styled/OpinionCard.style";
16import { dateConverter } from "../Util/dateConverter";
17import { StyledFontAwesomeIcon } from "../Components/Styled/OpinionCard.style";
18import { solid } from "@fortawesome/fontawesome-svg-core/import.macro";
19import {
20 AddOpinionButton,
21 Modal,
22 ModalContent,
23 ModalHeader,
24 ModalClose,
25 ModalBody,
26 ModalTextarea,
27 ModalFooter,
28} from "../Components/Styled/Modal.style";
29import axios from "../api/axios";
30import LoadingSpinner from "../Components/Styled/LoadingSpinner.style";
31
32const Topic = () => {
33 let params = useParams();
34 let navigate = useNavigate();
35 const { auth, setAuth } = useContext(AuthApi);
36
37 const [thread, setThread] = useState(null);
38 const [loadedThread, setLoadedThread] = useState(false);
39 const [user, setUser] = useState(null);
40 const [loadedUser, setLoadedUser] = useState(false);
41 const [fetchError, setFetchError] = useState(false);
42
43 const [postModalDisplay, setPostModalDisplay] = useState("none");
44 const [postContent, setPostContent] = useState("");
45 const [replyModalDisplay, setReplyModalDisplay] = useState("none");
46 const [replyContent, setReplyContent] = useState("");
47 const [postForModal, setPostForModal] = useState(null);
48 const [errorMessage, setErrorMessage] = useState("");
49
50 useEffect(() => {
51 const url1 = `http://192.168.0.29:8080/public/thread/${params.topicId}`;
52 const url2 = `http://192.168.0.29:8080/secure/currentUser`;
53
54 const fetchTopic = async () => {
55 try {
56 const response = await fetch(url1);
57 let cyclicGraph = await response.json();
58 let jsogStructure = JSOG.encode(cyclicGraph);
59 cyclicGraph = JSOG.decode(jsogStructure);
60 setThread(cyclicGraph);
61 setLoadedThread(true);
62 } catch (error) {
63 setFetchError(true);
64 }
65 };
66
67 const fetchUser = async () => {
68 try {
69 const response = await axios.get(url2, { withCredentials: true });
70 var cyclicGraph = await response.data;
71 var jsogStructure = JSOG.encode(cyclicGraph);
72 cyclicGraph = JSOG.decode(jsogStructure);
73 setUser(cyclicGraph);
74 setLoadedUser(true);
75 } catch (error) {
76 setFetchError(true);
77 }
78 };
79
80 fetchTopic().then(fetchUser);
81 }, []);
82
83 const handleReply = (post) => {
84 if (auth) {
85 setReplyModalDisplay("block");
86 setPostForModal(post);
87 document.body.style.overflowY = "hidden";
88 } else {
89 navigate("/login");
90 }
91 };
92
93 const handleReplyContentChange = (e) => {
94 setReplyContent(e.target.value);
95 };
96
97 const handleReplySubmit = async (e, postId) => {
98 e.preventDefault();
99
100 if (!replyContent.length < 1) {
101 const response = await axios(
102 `http://192.168.0.29:8080/secure/subject/${thread.targetSubject.subjectId}/replyToThread/${postId}`,
103 {
104 method: "post",
105 data: {
106 content: replyContent,
107 },
108 withCredentials: true,
109 }
110 );
111 setErrorMessage("");
112 window.location.reload();
113 } else {
114 setErrorMessage("Полето за содржина не смее да биде празно");
115 }
116 };
117
118 const handleAddOpinionButtonClick = () => {
119 if (auth) {
120 setPostModalDisplay("block");
121 document.body.style.overflowY = "hidden";
122 } else {
123 navigate("/login");
124 }
125 };
126
127 const handlePostSubmit = async (e) => {
128 e.preventDefault();
129 if (!postContent.length < 1) {
130 const response = await axios(
131 `http://192.168.0.29:8080/secure/subject/${thread.targetSubject.subjectId}/replyToThread/${params.topicId}`,
132 {
133 method: "post",
134 data: {
135 content: postContent,
136 },
137 withCredentials: true,
138 }
139 );
140 setErrorMessage("");
141 window.location.reload();
142 } else {
143 setErrorMessage("Полето за содржина не смее да биде празно");
144 }
145 };
146 const handleModalCloseClick = () => {
147 setPostModalDisplay("none");
148 setReplyModalDisplay("none");
149 document.body.style.overflowY = "auto";
150 };
151 const handleContentChange = (e) => {
152 setPostContent(e.target.value);
153 };
154
155 const handleLike = async (post) => {
156 if (auth) {
157 if (
158 loadedUser &&
159 user &&
160 !post.votes.some((e) => e.user.id === user.id)
161 ) {
162 const response = await axios(
163 `http://192.168.0.29:8080/secure/upvoteThread/${post.postId}`,
164 {
165 method: "get",
166 withCredentials: true,
167 }
168 );
169 window.location.reload();
170 } else {
171 return;
172 }
173 } else {
174 navigate("/login");
175 }
176 };
177
178 const handleDislike = async (post) => {
179 if (auth) {
180 if (
181 loadedUser &&
182 user &&
183 !post.votes.some((e) => e.user.id === user.id)
184 ) {
185 const response = await axios(
186 `http://192.168.0.29:8080/secure/downvoteThread/${post.postId}`,
187 {
188 method: "get",
189 withCredentials: true,
190 }
191 );
192
193 window.location.reload();
194 } else {
195 return;
196 }
197 } else {
198 navigate("/login");
199 }
200 };
201
202 function displayChildPosts(child, parentPostAuthorUsername, replyIndent) {
203 if (child == null) return;
204 //postCount = renderedOpinionIds.push(child.postId);
205 return (
206 <div key={child.postId}>
207 <OpinionReplyCard indent={replyIndent + "px"}>
208 <OpinionReplyCardContent>
209 <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
210 <a href={"/user/" + child.author.id}>{child.author.username}</a>{" "}
211 му реплицирал на {parentPostAuthorUsername}
212 </p>
213 <p style={{ marginBottom: "10px", maxWidth: "90%" }}>
214 {child.content}
215 </p>
216 {new Date(child.timePosted).setMilliseconds(0) === new Date(child.timeLastEdited).setMilliseconds(0) ? (
217 <OpinionCardContentTime>
218 {dateConverter(
219 new Date(child.timePosted).toString().slice(4, -43)
220 )} <span style={{fontStyle:"normal",color:"blue"}}>#{child.postId}</span>
221 </OpinionCardContentTime>
222 ) : (
223 <OpinionCardContentTime>
224 {dateConverter(
225 new Date(child.timeLastEdited).toString().slice(4, -43)
226 )}{" "} <span style={{fontStyle:"normal",color:"blue"}}>#{child.postId}</span>{" "}
227 (едитирано од модератор)
228 </OpinionCardContentTime>
229 )}
230
231 <div
232 style={{
233 display:
234 !auth || (auth && loadedUser && user.id !== child.author.id)
235 ? "block"
236 : "none",
237 }}
238 >
239 <StyledFontAwesomeIcon
240 icon={solid("thumbs-up")}
241 right={50 + "px"}
242 color={
243 auth && loadedUser && user
244 ? child.votes.some(
245 (e) => e.vote === "UPVOTE" && e.user.id === user.id
246 )
247 ? "green"
248 : "darkgrey"
249 : "darkgrey"
250 }
251 onClick={() => handleLike(child)}
252 />
253
254 <VoteCount right={50 + "px"}>
255 {child.votes.filter((v) => v.vote === "UPVOTE").length}
256 </VoteCount>
257
258 <StyledFontAwesomeIcon
259 icon={solid("thumbs-down")}
260 right={10 + "px"}
261 color={
262 auth && loadedUser && user
263 ? child.votes.some(
264 (e) => e.vote === "DOWNVOTE" && e.user.id === user.id
265 )
266 ? "indianred"
267 : "darkgrey"
268 : "darkgrey"
269 }
270 onClick={() => handleDislike(child)}
271 />
272
273 <VoteCount right={10 + "px"}>
274 {child.votes.filter((v) => v.vote === "DOWNVOTE").length}
275 </VoteCount>
276
277 <StyledFontAwesomeIcon
278 icon={solid("reply")}
279 right={90 + "px"}
280 color="darkgrey"
281 onClick={() => handleReply(child)}
282 />
283 </div>
284 </OpinionReplyCardContent>
285
286 {child.children.map((childOfChild) =>
287 displayChildPosts(
288 childOfChild,
289 child.author.username,
290 replyIndent + 30
291 )
292 )}
293 </OpinionReplyCard>
294 </div>
295 );
296 }
297
298 return loadedThread && thread.length !== 0 ? (
299 <>
300 <CurrentPageNav>
301 &#187;{" "}
302 <a
303 href={
304 "/university/" +
305 thread.targetSubject.studyProgramme.faculty.university.universityId
306 }
307 >
308 {
309 thread.targetSubject.studyProgramme.faculty.university
310 .universityName
311 }
312 </a>{" "}
313 &#187;{" "}
314 <a
315 href={
316 "/faculty/" + thread.targetSubject.studyProgramme.faculty.facultyId
317 }
318 >
319 {thread.targetSubject.studyProgramme.faculty.facultyName}
320 </a>{" "}
321 &#187;{" "}
322 <a href={"/subject/" + thread.targetSubject.subjectId}>
323 {thread.targetSubject.subjectName}
324 </a>
325 </CurrentPageNav>
326 <div style={{ height: "20px", marginBottom: "50px", marginTop: "50px" }}>
327 <h3 style={{ float: "left" }}>{thread.title} <span style={{opacity:"50%", fontSize:"16px"}}>#{thread.postId}</span></h3>
328 {auth && (
329 <AddOpinionButton onClick={handleAddOpinionButtonClick}>
330 Реплицирај
331 </AddOpinionButton>
332 )}
333 </div>
334 <Modal display={postModalDisplay}>
335 <ModalContent>
336 <ModalHeader>
337 <ModalClose onClick={handleModalCloseClick}>&times;</ModalClose>
338 <h3 style={{ marginTop: "5px" }}>
339 Реплика на темата {thread.title}
340 </h3>
341 </ModalHeader>
342 <form onSubmit={handlePostSubmit}>
343 <ModalBody>
344 <label htmlFor="content">
345 <b>Содржина</b>:
346 <ModalTextarea
347 id="content"
348 rows="8"
349 cols="100"
350 value={postContent}
351 onChange={handleContentChange}
352 spellCheck={false}
353 />
354 </label>
355 </ModalBody>
356 <p style={{ color: "red", marginLeft: "15px", marginTop: "10px" }}>
357 {errorMessage}
358 </p>
359 <ModalFooter type="submit">ОБЈАВИ</ModalFooter>
360 </form>
361 </ModalContent>
362 </Modal>
363 <OpinionCard>
364 <OpinionCardContent>
365 <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
366 <a href={"/user/" + thread.author.id}>{thread.author.username}</a>{" "}
367 напишал
368 </p>
369 <p style={{ marginBottom: "10px", maxWidth: "90%" }}>
370 {thread.content}
371 </p>
372 {new Date(thread.timePosted).setMilliseconds(0) === new Date(thread.timeLastEdited).setMilliseconds(0) ? (
373 <OpinionCardContentTime>
374 {dateConverter(
375 new Date(thread.timePosted).toString().slice(4, -43)
376 )} <span style={{fontStyle:"normal",color:"blue"}}>#{thread.postId}</span>
377 </OpinionCardContentTime>
378 ) : (
379 <OpinionCardContentTime>
380 {dateConverter(
381 new Date(thread.timeLastEdited).toString().slice(4, -43)
382 )}{" "} <span style={{fontStyle:"normal",color:"blue"}}>#{thread.postId}</span>{" "}
383 (едитирано од модератор)
384 </OpinionCardContentTime>
385 )}
386 <div
387 style={{
388 display:
389 !auth || (auth && loadedUser && user.id !== thread.author.id)
390 ? "block"
391 : "none",
392 }}
393 >
394 <StyledFontAwesomeIcon
395 icon={solid("thumbs-up")}
396 right={50 + "px"}
397 color={
398 auth && loadedUser && user
399 ? thread.votes.some(
400 (e) => e.vote === "UPVOTE" && e.user.id === user.id
401 )
402 ? "green"
403 : "darkgrey"
404 : "darkgrey"
405 }
406 onClick={() => handleLike(thread)}
407 />
408
409 <VoteCount right={50 + "px"}>
410 {thread.votes.filter((v) => v.vote === "UPVOTE").length}
411 </VoteCount>
412
413 <StyledFontAwesomeIcon
414 icon={solid("thumbs-down")}
415 right={10 + "px"}
416 color={
417 auth && loadedUser && user
418 ? thread.votes.some(
419 (e) => e.vote === "DOWNVOTE" && e.user.id === user.id
420 )
421 ? "indianred"
422 : "darkgrey"
423 : "darkgrey"
424 }
425 onClick={() => handleDislike(thread)}
426 />
427
428 <VoteCount right={10 + "px"}>
429 {thread.votes.filter((v) => v.vote === "DOWNVOTE").length}
430 </VoteCount>
431 </div>
432 </OpinionCardContent>
433 </OpinionCard>
434 {thread.children.map((directChild) => {
435 return (
436 <OpinionCard key={directChild.postId}>
437 <OpinionCardContent>
438 <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
439 <a href={"/user/" + directChild.author.id}>
440 {directChild.author.username}
441 </a>{" "}
442 напишал
443 </p>
444 <p style={{ marginBottom: "10px", maxWidth: "90%" }}>
445 {directChild.content}
446 </p>
447 {new Date(directChild.timePosted).setMilliseconds(0) === new Date(directChild.timeLastEdited).setMilliseconds(0) ? (
448 <OpinionCardContentTime>
449 {dateConverter(
450 new Date(directChild.timePosted).toString().slice(4, -43)
451 )} <span style={{fontStyle:"normal",color:"blue"}}>#{directChild.postId}</span>
452 </OpinionCardContentTime>
453 ) : (
454 <OpinionCardContentTime>
455 {dateConverter(
456 new Date(directChild.timeLastEdited)
457 .toString()
458 .slice(4, -43)
459 )}{" "} <span style={{fontStyle:"normal",color:"blue"}}>#{directChild.postId}</span>{" "}
460 (едитирано од модератор)
461 </OpinionCardContentTime>
462 )}
463 <div
464 style={{
465 display:
466 !auth ||
467 (auth && loadedUser && user.id !== directChild.author.id)
468 ? "block"
469 : "none",
470 }}
471 >
472 <StyledFontAwesomeIcon
473 icon={solid("thumbs-up")}
474 right={50 + "px"}
475 color={
476 auth && loadedUser && user
477 ? directChild.votes.some(
478 (e) => e.vote === "UPVOTE" && e.user.id === user.id
479 )
480 ? "green"
481 : "darkgrey"
482 : "darkgrey"
483 }
484 onClick={() => handleLike(directChild)}
485 />
486
487 <VoteCount right={50 + "px"}>
488 {directChild.votes.filter((v) => v.vote === "UPVOTE").length}
489 </VoteCount>
490
491 <StyledFontAwesomeIcon
492 icon={solid("thumbs-down")}
493 right={10 + "px"}
494 color={
495 auth && loadedUser && user
496 ? directChild.votes.some(
497 (e) => e.vote === "DOWNVOTE" && e.user.id === user.id
498 )
499 ? "indianred"
500 : "darkgrey"
501 : "darkgrey"
502 }
503 onClick={() => handleDislike(directChild)}
504 />
505
506 <VoteCount right={10 + "px"}>
507 {
508 directChild.votes.filter((v) => v.vote === "DOWNVOTE")
509 .length
510 }
511 </VoteCount>
512
513 <StyledFontAwesomeIcon
514 icon={solid("reply")}
515 right={90 + "px"}
516 color="darkgrey"
517 onClick={() => handleReply(directChild)}
518 />
519 </div>
520 </OpinionCardContent>
521 {directChild.children.map((child) =>
522 displayChildPosts(child, directChild.author.username, 30)
523 )}
524 </OpinionCard>
525 );
526 })}
527 {postForModal && (
528 <Modal display={replyModalDisplay}>
529 <ModalContent>
530 <ModalHeader>
531 <ModalClose onClick={handleModalCloseClick}>&times;</ModalClose>
532 <h3 style={{ marginTop: "5px" }}>
533 Реплика на {postForModal.author.username}
534 </h3>
535 </ModalHeader>
536 <form onSubmit={(e) => handleReplySubmit(e, postForModal.postId)}>
537 <ModalBody>
538 <label htmlFor="content">
539 <b>Содржина</b>:
540 <ModalTextarea
541 id="content"
542 rows="8"
543 cols="100"
544 value={replyContent}
545 onChange={handleReplyContentChange}
546 />
547 </label>
548 </ModalBody>
549 <p
550 style={{ color: "red", marginLeft: "15px", marginTop: "10px" }}
551 >
552 {errorMessage}
553 </p>
554 <ModalFooter type="submit">РЕПЛИЦИРАЈ</ModalFooter>
555 </form>
556 </ModalContent>
557 </Modal>
558 )}
559 </>
560 ) : !fetchError && !loadedThread ? (
561 <div>
562 <LoadingSpinner style={{ marginTop: "140px" }}/>
563 <Outlet />
564 </div>
565 ) : (
566 <div style={{ marginTop: "140px" }}>
567 <h1 style={{ textAlign: "center" }}>Страницата не е пронајдена.</h1>
568 </div>
569 );
570};
571
572export default Topic;
Note: See TracBrowser for help on using the repository browser.