Index: reactapp/src/App.js
===================================================================
--- reactapp/src/App.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/App.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -12,30 +12,11 @@
 import AuthApi from "./api/AuthApi";
 import Cookies from "js-cookie";
-import axios from "./api/axios";
-import JSOG from "jsog";
 import NotFound from "./Pages/NotFound";
+import Topic from "./Pages/Topic";
 
 export default function App() {
   const [auth, setAuth] = useState(false);
-  const [user, setUser] = useState(null);
-  const [userLoaded, setUserLoaded] = useState(false);
   const variableAuth = useMemo(() => ({ auth, setAuth }), [auth]);
   const [authLoaded, setAuthLoaded] = useState(false);
-
-  const fetchUser = async () => {
-    try {
-      const response = await axios.get(
-        "http://192.168.0.17:8080/secure/currentUser",
-        { withCredentials: true }
-      );
-      var cyclicGraph = await response.data;
-      var jsogStructure = JSOG.encode(cyclicGraph);
-      cyclicGraph = JSOG.decode(jsogStructure);
-      setUser(cyclicGraph);
-      setUserLoaded(true);
-    } catch (error) {
-      console.log("Fetching error", error);
-    }
-  };
 
   const readCookie = async () => {
@@ -43,5 +24,4 @@
     if (session) {
       setAuth(true);
-      fetchUser();
     } else {
       setAuth(false);
@@ -51,4 +31,5 @@
 
   useEffect(() => {
+    document.title = "profesori.mk";
     readCookie();
   }, []);
@@ -69,26 +50,19 @@
       <BrowserRouter>
         <Routes>
-          <Route
-            path="/"
-            element={<Home user={user} userLoaded={userLoaded} />}
-          >
+          <Route path="/" element={<Home />}>
             <Route path="login" element={<Login />}></Route>
             <Route path="registration" element={<Registration />}></Route>
             <Route path="professor">
-              <Route
-                path=":professorId"
-                element={<Professor user={user} userLoaded={userLoaded} />}
-              />
+              <Route path=":professorId" element={<Professor />} />
             </Route>
             <Route path="university/:universityId" element={<University />} />
             <Route path="faculty/:facultyId" element={<Faculty />} />
             <Route path="subject/:subjectId" element={<Subject />} />
+            <Route path="topic/:topicId" element={<Topic />} />
             <Route path="search" element={<SearchResults />}></Route>
             <Route
               path="user_dashboard"
               element={
-                <ProtectedRoute auth={auth}>
-                  {<UserDashboard user={user} userLoaded={userLoaded} />}
-                </ProtectedRoute>
+                <ProtectedRoute auth={auth}>{<UserDashboard />}</ProtectedRoute>
               }
             ></Route>
Index: reactapp/src/Components/OpinionTree.js
===================================================================
--- reactapp/src/Components/OpinionTree.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Components/OpinionTree.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -3,5 +3,4 @@
   OpinionCardContent,
   OpinionCardContentTime,
-  OpinionCardContentTitle,
   OpinionReplyCard,
   OpinionReplyCardContent,
@@ -14,5 +13,6 @@
 import AuthApi from "../api/AuthApi";
 import { useNavigate } from "react-router-dom";
-import { useContext, useState } from "react";
+import { useContext, useState, useEffect } from "react";
+import JSOG from "jsog";
 import {
   Modal,
@@ -26,56 +26,82 @@
 import axios from "../api/axios";
 
-function OpinionTree({ professor, user, userLoaded }) {
+function OpinionTree({ professor }) {
   var renderedOpinionIds = [];
   var postCount; // za da ne go pokazuva ispod postot
 
+  const { auth, setAuth } = useContext(AuthApi);
   let navigate = useNavigate();
-  const { auth, setAuth } = useContext(AuthApi);
-
-  let [replyModalDisplay, setReplyModalDisplay] = useState("none");
+
+  const [replyModalDisplay, setReplyModalDisplay] = useState("none");
   const [replyContent, setReplyContent] = useState("");
-
   const [postForModal, setPostForModal] = useState(null);
+  const [user, setUser] = useState(null);
+  const [loadedUser, setLoadedUser] = useState(false);
+  const [fetchError, setFetchError] = useState(false);
+  const [errorMessage, setErrorMessage] = useState("");
+
+  useEffect(() => {
+    const url = `http://192.168.0.17:8080/secure/currentUser`;
+
+    const fetchUser = async () => {
+      try {
+        const response = await axios.get(url, { withCredentials: true });
+        var cyclicGraph = await response.data;
+        var jsogStructure = JSOG.encode(cyclicGraph);
+        cyclicGraph = JSOG.decode(jsogStructure);
+        setUser(cyclicGraph);
+        setLoadedUser(true);
+      } catch (error) {
+        setFetchError(true);
+      }
+    };
+
+    if (auth) fetchUser();
+  }, []);
 
   const handleLike = async (post) => {
-    if (
-      auth &&
-      userLoaded &&
-      !post.likes.some((e) => e.id === user.user.id) &&
-      !post.dislikes.some((e) => e.id === user.user.id)
-    ) {
-      const response = await axios(
-        `http://192.168.0.17:8080/secure/professor/${professor.professorId}/upvoteOpinion/${post.postId}`,
-        {
-          method: "get",
-          withCredentials: true,
-        }
-      );
-
-      window.location.reload(false);
+    if (auth) {
+      if (
+        loadedUser &&
+        user &&
+        !post.votes.some((e) => e.user.id === user.id)
+      ) {
+        const response = await axios(
+          `http://192.168.0.17:8080/secure/upvoteOpinion/${post.postId}`,
+          {
+            method: "get",
+            withCredentials: true,
+          }
+        );
+        window.location.reload(false);
+      } else {
+        return;
+      }
     } else {
-      return;
+      navigate("/login");
     }
   };
 
   const handleDislike = async (post) => {
-    if (
-      auth &&
-      auth &&
-      userLoaded &&
-      !post.likes.some((e) => e.id === user.user.id) &&
-      !post.dislikes.some((e) => e.id === user.user.id)
-    ) {
-      const response = await axios(
-        `http://192.168.0.17:8080/secure/professor/${professor.professorId}/downvoteOpinion/${post.postId}`,
-        {
-          method: "get",
-          withCredentials: true,
-        }
-      );
-
-      window.location.reload(false);
+    if (auth) {
+      if (
+        loadedUser &&
+        user &&
+        !post.votes.some((e) => e.user.id === user.id)
+      ) {
+        const response = await axios(
+          `http://192.168.0.17:8080/secure/downvoteOpinion/${post.postId}`,
+          {
+            method: "get",
+            withCredentials: true,
+          }
+        );
+
+        window.location.reload(false);
+      } else {
+        return;
+      }
     } else {
-      return;
+      navigate("/login");
     }
   };
@@ -101,17 +127,20 @@
     e.preventDefault();
 
-    const response = await axios(
-      `http://192.168.0.17:8080/secure/professor/${professor.professorId}/replyToOpinion/${postId}`,
-      {
-        method: "post",
-        body: {
-          content: replyContent,
-        },
-        withCredentials: true,
-      }
-    );
-
-    window.location.reload(false);
-    //console.log(response);
+    if (!replyContent.length < 1) {
+      const response = await axios(
+        `http://192.168.0.17:8080/secure/professor/${professor.professorId}/replyToOpinion/${postId}`,
+        {
+          method: "post",
+          data: {
+            content: replyContent,
+          },
+          withCredentials: true,
+        }
+      );
+      setErrorMessage("");
+      window.location.reload(false);
+    } else {
+      setErrorMessage("Полето за содржина не смее да биде празно");
+    }
   };
 
@@ -123,9 +152,11 @@
         <OpinionReplyCard indent={replyIndent + "px"}>
           <OpinionReplyCardContent>
-            <p>
-              <a href="#">{child.author.username}</a> му реплицирал на{" "}
-              {parentPostAuthorUsername}
+            <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
+              <a href={"/user/" + child.author.id}>{child.author.username}</a>{" "}
+              му реплицирал на {parentPostAuthorUsername}
             </p>
-            <p>{child.content}</p>
+            <p style={{ marginBottom: "10px", maxWidth: "90%" }}>
+              {child.content}
+            </p>
             <OpinionReplyCardContentTime>
               {dateConverter(
@@ -133,36 +164,58 @@
               )}
             </OpinionReplyCardContentTime>
-            {auth && userLoaded && user.user.id !== child.author.id && (
-              <>
-                <StyledFontAwesomeIcon
-                  icon={solid("thumbs-up")}
-                  right={50 + "px"}
-                  color={
-                    child.likes.some((e) => e.id === user.user.id)
+
+            <div
+              style={{
+                display:
+                  !auth || (auth && loadedUser && user.id !== child.author.id)
+                    ? "block"
+                    : "none",
+              }}
+            >
+              <StyledFontAwesomeIcon
+                icon={solid("thumbs-up")}
+                right={50 + "px"}
+                color={
+                  auth && loadedUser && user
+                    ? child.votes.some(
+                        (e) => e.vote === "UPVOTE" && e.user.id === user.id
+                      )
                       ? "greenyellow"
                       : "darkgrey"
-                  }
-                  onClick={() => handleLike(child)}
-                />
-                <VoteCount right={50 + "px"}>{child.likes.length}</VoteCount>
-                <StyledFontAwesomeIcon
-                  icon={solid("thumbs-down")}
-                  right={10 + "px"}
-                  color={
-                    child.dislikes.some((e) => e.id === user.user.id)
+                    : "darkgrey"
+                }
+                onClick={() => handleLike(child)}
+              />
+
+              <VoteCount right={50 + "px"}>
+                {child.votes.filter((v) => v.vote === "UPVOTE").length}
+              </VoteCount>
+
+              <StyledFontAwesomeIcon
+                icon={solid("thumbs-down")}
+                right={10 + "px"}
+                color={
+                  auth && loadedUser && user
+                    ? child.votes.some(
+                        (e) => e.vote === "DOWNVOTE" && e.user.id === user.id
+                      )
                       ? "indianred"
                       : "darkgrey"
-                  }
-                  onClick={() => handleDislike(child)}
-                />
-                <VoteCount right={10 + "px"}>{child.dislikes.length}</VoteCount>
-                <StyledFontAwesomeIcon
-                  icon={solid("reply")}
-                  right={90 + "px"}
-                  color="darkgrey"
-                  onClick={() => handleReply(child)}
-                />
-              </>
-            )}
+                    : "darkgrey"
+                }
+                onClick={() => handleDislike(child)}
+              />
+
+              <VoteCount right={10 + "px"}>
+                {child.votes.filter((v) => v.vote === "DOWNVOTE").length}
+              </VoteCount>
+
+              <StyledFontAwesomeIcon
+                icon={solid("reply")}
+                right={90 + "px"}
+                color="darkgrey"
+                onClick={() => handleReply(child)}
+              />
+            </div>
           </OpinionReplyCardContent>
           {child.children.map((childOfChild) =>
@@ -187,11 +240,13 @@
               <OpinionCard>
                 <OpinionCardContent>
-                  <p>
-                    <a href="#">{opinion.author.username}</a> напишал
+                  <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
+                    <a href={"/user/" + opinion.author.id}>
+                      {opinion.author.username}
+                    </a>{" "}
+                    напишал
                   </p>
-                  <OpinionCardContentTitle>
-                    {opinion.title}
-                  </OpinionCardContentTitle>
-                  <p>{opinion.content}</p>
+                  <p style={{ marginBottom: "10px", maxWidth: "90%" }}>
+                    {opinion.content}
+                  </p>
                   <OpinionCardContentTime>
                     {dateConverter(
@@ -199,40 +254,64 @@
                     )}
                   </OpinionCardContentTime>
-                  {auth && userLoaded && user.user.id !== opinion.author.id && (
-                    <>
-                      <StyledFontAwesomeIcon
-                        icon={solid("thumbs-up")}
-                        right={50 + "px"}
-                        color={
-                          opinion.likes.some((e) => e.id === user.user.id)
+
+                  <div
+                    style={{
+                      display:
+                        !auth ||
+                        (auth && loadedUser && user.id !== opinion.author.id)
+                          ? "block"
+                          : "none",
+                    }}
+                  >
+                    <StyledFontAwesomeIcon
+                      icon={solid("thumbs-up")}
+                      right={50 + "px"}
+                      color={
+                        auth && loadedUser && user
+                          ? opinion.votes.some(
+                              (e) =>
+                                e.vote === "UPVOTE" && e.user.id === user.id
+                            )
                             ? "greenyellow"
                             : "darkgrey"
-                        }
-                        onClick={() => handleLike(opinion)}
-                      />
-                      <VoteCount right={50 + "px"}>
-                        {opinion.likes.length}
-                      </VoteCount>
-                      <StyledFontAwesomeIcon
-                        icon={solid("thumbs-down")}
-                        right={10 + "px"}
-                        color={
-                          opinion.dislikes.some((e) => e.id === user.user.id)
+                          : "darkgrey"
+                      }
+                      onClick={() => handleLike(opinion)}
+                    />
+
+                    <VoteCount right={50 + "px"}>
+                      {opinion.votes.filter((v) => v.vote === "UPVOTE").length}
+                    </VoteCount>
+
+                    <StyledFontAwesomeIcon
+                      icon={solid("thumbs-down")}
+                      right={10 + "px"}
+                      color={
+                        auth && loadedUser && user
+                          ? opinion.votes.some(
+                              (e) =>
+                                e.vote === "DOWNVOTE" && e.user.id === user.id
+                            )
                             ? "indianred"
                             : "darkgrey"
-                        }
-                        onClick={() => handleDislike(opinion)}
-                      />
-                      <VoteCount right={10 + "px"}>
-                        {opinion.dislikes.length}
-                      </VoteCount>
-                      <StyledFontAwesomeIcon
-                        icon={solid("reply")}
-                        right={90 + "px"}
-                        color="darkgrey"
-                        onClick={() => handleReply(opinion)}
-                      />
-                    </>
-                  )}
+                          : "darkgrey"
+                      }
+                      onClick={() => handleDislike(opinion)}
+                    />
+
+                    <VoteCount right={10 + "px"}>
+                      {
+                        opinion.votes.filter((v) => v.vote === "DOWNVOTE")
+                          .length
+                      }
+                    </VoteCount>
+
+                    <StyledFontAwesomeIcon
+                      icon={solid("reply")}
+                      right={90 + "px"}
+                      color="darkgrey"
+                      onClick={() => handleReply(opinion)}
+                    />
+                  </div>
                 </OpinionCardContent>
                 {opinion.children.map((child) =>
@@ -243,4 +322,5 @@
           );
         }
+        return null;
       })}
       {postForModal && (
@@ -266,4 +346,9 @@
                 </label>
               </ModalBody>
+              <p
+                style={{ color: "red", marginLeft: "15px", marginTop: "10px" }}
+              >
+                {errorMessage}
+              </p>
               <ModalFooter type="submit">РЕПЛИЦИРАЈ</ModalFooter>
             </form>
Index: reactapp/src/Components/Styled/EntityList.style.js
===================================================================
--- reactapp/src/Components/Styled/EntityList.style.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Components/Styled/EntityList.style.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -18,5 +18,5 @@
 export const EntityParam = styled.p`
   position: absolute;
-  right: 30px;
+  right: ${(props) => props.right};
   top: 10px;
   font-style: italic;
Index: reactapp/src/Components/Styled/Modal.style.js
===================================================================
--- reactapp/src/Components/Styled/Modal.style.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Components/Styled/Modal.style.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -18,4 +18,5 @@
   float: right;
   font-weight: bold;
+  box-shadow: 2px 1px 10px #aaaaaa;
 `;
 
Index: reactapp/src/Components/Styled/OpinionCard.style.js
===================================================================
--- reactapp/src/Components/Styled/OpinionCard.style.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Components/Styled/OpinionCard.style.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -18,4 +18,5 @@
   }
   position: relative;
+  overflow-wrap: break-word;
 `;
 
@@ -55,4 +56,5 @@
   display: block;
   position: absolute;
+  height: 20px;
   top: 50%;
   transform: translateY(-50%);
@@ -60,5 +62,5 @@
   transition: 0.5s;
   &:hover {
-    color: ${(props) => props.color};
+    opacity: 0.5;
     cursor: pointer;
   }
Index: reactapp/src/Components/SubjectsAccordion.js
===================================================================
--- reactapp/src/Components/SubjectsAccordion.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Components/SubjectsAccordion.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -27,56 +27,57 @@
       <SubjectsAccordionDiv height={height} opacity={opacity}>
         <EntityUl>
-          {props.content.map((item) => {
-            let totalPosts = item.threads.length;
-            return (
-              <EntityLi key={item.subjectName} bgcolor="cornsilk">
-                <a href={"/subject/" + item.subjectId}>{item.subjectName}</a>
-                <EntityParam>
-                  {totalPosts !== 1 ? (
-                    totalPosts !== 0 ? (
-                      <span
-                        style={{
-                          fontWeight: "normal",
-                          opacity: totalPosts === 0 ? "0.5" : "1",
-                        }}
-                      >
+          {props.content &&
+            props.content.map((item) => {
+              let totalPosts = item.threads.length;
+              return (
+                <EntityLi key={item.subjectName} bgcolor="cornsilk">
+                  <a href={"/subject/" + item.subjectId}>{item.subjectName}</a>
+                  <EntityParam right="30px">
+                    {totalPosts !== 1 ? (
+                      totalPosts !== 0 ? (
                         <span
                           style={{
-                            fontWeight: "bold",
+                            fontWeight: "normal",
                             opacity: totalPosts === 0 ? "0.5" : "1",
                           }}
                         >
-                          {totalPosts}
-                        </span>{" "}
-                        мислења
-                      </span>
-                    ) : (
-                      <span
-                        style={{
-                          fontWeight: "normal",
-                          opacity: totalPosts === 0 ? "0.5" : "1",
-                        }}
-                      >
+                          <span
+                            style={{
+                              fontWeight: "bold",
+                              opacity: totalPosts === 0 ? "0.5" : "1",
+                            }}
+                          >
+                            {totalPosts}
+                          </span>{" "}
+                          мислења
+                        </span>
+                      ) : (
                         <span
                           style={{
-                            fontWeight: "bold",
+                            fontWeight: "normal",
                             opacity: totalPosts === 0 ? "0.5" : "1",
                           }}
                         >
-                          {totalPosts}
-                        </span>{" "}
-                        мислења
+                          <span
+                            style={{
+                              fontWeight: "bold",
+                              opacity: totalPosts === 0 ? "0.5" : "1",
+                            }}
+                          >
+                            {totalPosts}
+                          </span>{" "}
+                          мислења
+                        </span>
+                      )
+                    ) : (
+                      <span style={{ fontWeight: "normal" }}>
+                        <span style={{ fontWeight: "bold" }}>{totalPosts}</span>{" "}
+                        мислење
                       </span>
-                    )
-                  ) : (
-                    <span style={{ fontWeight: "normal" }}>
-                      <span style={{ fontWeight: "bold" }}>{totalPosts}</span>{" "}
-                      мислење
-                    </span>
-                  )}
-                </EntityParam>
-              </EntityLi>
-            );
-          })}
+                    )}
+                  </EntityParam>
+                </EntityLi>
+              );
+            })}
         </EntityUl>
       </SubjectsAccordionDiv>
Index: reactapp/src/Components/UserHeader.js
===================================================================
--- reactapp/src/Components/UserHeader.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Components/UserHeader.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -4,6 +4,29 @@
 import Logout from "./Logout";
 
-function UserHeader({ user, userLoaded }) {
-  return userLoaded ? (
+function UserHeader({}) {
+  const [user, setUser] = useState(null);
+  const [loadedUser, setLoadedUser] = useState(false);
+  const [fetchError, setFetchError] = useState(false);
+
+  useEffect(() => {
+    const url = `http://192.168.0.17:8080/secure/currentUser`;
+
+    const fetchUser = async () => {
+      try {
+        const response = await axios.get(url, { withCredentials: true });
+        var cyclicGraph = await response.data;
+        var jsogStructure = JSOG.encode(cyclicGraph);
+        cyclicGraph = JSOG.decode(jsogStructure);
+        setUser(cyclicGraph);
+        setLoadedUser(true);
+      } catch (error) {
+        setFetchError(true);
+      }
+    };
+
+    fetchUser();
+  }, []);
+
+  return loadedUser ? (
     <div style={{ float: "left", marginTop: 20, marginLeft: 40 }}>
       Најавен/а: <a href="/user_dashboard">{user.username}</a> <Logout />{" "}
Index: reactapp/src/Pages/Faculty.js
===================================================================
--- reactapp/src/Pages/Faculty.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Pages/Faculty.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -60,5 +60,5 @@
   }, [params.facultyId]);
 
-  return loadedProfessors ? (
+  return loadedProfessors && professors.length != 0 ? (
     entityType === 0 ? (
       <>
@@ -114,5 +114,5 @@
                     {professor.professorName}
                   </a>
-                  <EntityParam>
+                  <EntityParam right="30px">
                     {totalPosts !== 1 ? (
                       totalPosts !== 0 ? (
@@ -168,4 +168,5 @@
         <>
           <CurrentPageNav>
+            &#187;{" "}
             <a
               href={
@@ -175,5 +176,5 @@
               {professors[0].faculty.university.universityName}
             </a>{" "}
-            / <a href="#">{professors[0].faculty.facultyName}</a>
+            &#187; <a href="#">{professors[0].faculty.facultyName}</a>
           </CurrentPageNav>
           <ProfessorCard>
@@ -221,5 +222,5 @@
       )
     )
-  ) : !fetchError ? (
+  ) : !fetchError && !loadedProfessors ? (
     <div>
       <p style={{ marginTop: "140px" }}>се вчитува...</p>
Index: reactapp/src/Pages/Home.js
===================================================================
--- reactapp/src/Pages/Home.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Pages/Home.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -24,5 +24,5 @@
       </a>{" "}
       <Search />
-      {auth && <UserHeader user={user} userLoaded={userLoaded} />}
+      {auth && <UserHeader />}
       <div style={{ marginTop: "140px" }}></div>
       <Outlet />
Index: reactapp/src/Pages/Login.js
===================================================================
--- reactapp/src/Pages/Login.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Pages/Login.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -35,5 +35,8 @@
     if (!response.request.responseURL.includes("error")) {
       // ako NE redirektira na /login?error
-      Cookies.set("JSESSIONID", response.data.sessionId);
+      var in30Minutes = 1 / 48;
+      Cookies.set("JSESSIONID", response.data.sessionId, {
+        expires: in30Minutes,
+      });
       setAuth(true);
       setErrMsg("");
Index: reactapp/src/Pages/Professor.js
===================================================================
--- reactapp/src/Pages/Professor.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Pages/Professor.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -25,20 +25,21 @@
 import { CurrentPageNav } from "../Components/Styled/Main.style";
 
-function Professor(user, userLoaded) {
+function Professor() {
   let params = useParams();
+  let navigate = useNavigate();
 
-  let [professor, setProfessor] = useState(null);
-  let [loaded, setLoaded] = useState(null);
-  let [postModalDisplay, setPostModalDisplay] = useState("none");
-  let navigate = useNavigate();
+  const [professor, setProfessor] = useState(null);
+  const [loadedProfessor, setLoadedProfessor] = useState(false);
+
+  const [postModalDisplay, setPostModalDisplay] = useState("none");
   const { auth, setAuth } = useContext(AuthApi);
-  const [postTitle, setPostTitle] = useState("");
   const [postContent, setPostContent] = useState("");
   const [fetchError, setFetchError] = useState(false);
+  const [errorMessage, setErrorMessage] = useState("");
 
   useEffect(() => {
     const url = `http://192.168.0.17:8080/public/professor/${params.professorId}`;
 
-    const fetchData = async () => {
+    const fetchProfessor = async () => {
       try {
         const response = await fetch(url);
@@ -47,5 +48,5 @@
         cyclicGraph = JSOG.decode(jsogStructure);
         setProfessor(cyclicGraph);
-        setLoaded(true);
+        setLoadedProfessor(true);
       } catch (error) {
         setFetchError(true);
@@ -53,5 +54,5 @@
     };
 
-    fetchData();
+    fetchProfessor();
   }, [params.professorId]);
 
@@ -71,21 +72,20 @@
     e.preventDefault();
 
-    const response = await axios(
-      `http://192.168.0.17:8080/secure/professor/${professor.professorId}/addOpinion`,
-      {
-        method: "post",
-        data: {
-          title: postTitle,
-          content: postContent,
-        },
-        withCredentials: true,
-      }
-    );
-
-    window.location.reload(false);
-  };
-
-  const handleTitleChange = (e) => {
-    setPostTitle(e.target.value);
+    if (!postContent.length < 1) {
+      const response = await axios(
+        `http://192.168.0.17:8080/secure/professor/${params.professorId}/addOpinion`,
+        {
+          method: "post",
+          data: {
+            content: postContent,
+          },
+          withCredentials: true,
+        }
+      );
+      setErrorMessage("");
+      window.location.reload(false);
+    } else {
+      setErrorMessage("Полето за содржина не смее да биде празно");
+    }
   };
 
@@ -94,5 +94,5 @@
   };
 
-  if (loaded) {
+  if (loadedProfessor) {
     return (
       <div>
@@ -120,5 +120,5 @@
           </div>
         </ProfessorCard>
-        <div style={{ height: "20px", marginBottom: "30px" }}>
+        <div style={{ height: "20px", marginBottom: "50px" }}>
           <h3
             style={{
@@ -146,13 +146,4 @@
             <form onSubmit={handlePostSubmit}>
               <ModalBody>
-                <label htmlFor="title">
-                  <b>Наслов</b>:
-                  <ModalInput
-                    id="title"
-                    type="text"
-                    value={postTitle}
-                    onChange={handleTitleChange}
-                  />
-                </label>
                 <label htmlFor="content">
                   <b>Содржина</b>:
@@ -166,4 +157,9 @@
                 </label>
               </ModalBody>
+              <p
+                style={{ color: "red", marginLeft: "15px", marginTop: "10px" }}
+              >
+                {errorMessage}
+              </p>
               <ModalFooter type="submit">ОБЈАВИ</ModalFooter>
             </form>
@@ -172,9 +168,5 @@
 
         <div className="opinionTree">
-          <OpinionTree
-            professor={professor}
-            user={user}
-            userLoaded={userLoaded}
-          />
+          <OpinionTree professor={professor} />
         </div>
         <Outlet />
Index: reactapp/src/Pages/Registration.js
===================================================================
--- reactapp/src/Pages/Registration.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Pages/Registration.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -1,3 +1,3 @@
-import React, { useRef, useState, useEffect } from "react";
+import React, { useRef, useState, useEffect, useContext } from "react";
 import axios from "../api/axios";
 import {
@@ -7,7 +7,10 @@
   RequiredAsterisk,
 } from "../Components/Styled/Login.style";
+import { Navigate } from "react-router-dom";
+import AuthApi from "../api/AuthApi";
 const REGISTRATION_URL = "/registration";
 
 const Registration = () => {
+  const { auth, setAuth } = useContext(AuthApi);
   const userRef = useRef();
   const errRef = useRef();
@@ -50,5 +53,7 @@
     useState(0);
 
-  return registrationSuccessful === false ? (
+  return auth ? (
+    <Navigate to="/user_dashboard" />
+  ) : registrationSuccessful === false ? (
     <div
       style={{
Index: reactapp/src/Pages/Subject.js
===================================================================
--- reactapp/src/Pages/Subject.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Pages/Subject.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -1,8 +1,275 @@
-import React from "react";
-import { useParams } from "react-router-dom";
+import React, { useState, useEffect, useContext } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import JSOG from "jsog";
+import { Outlet } from "react-router-dom";
+import { CurrentPageNav } from "../Components/Styled/Main.style";
+import {
+  ProfessorCard,
+  ProfessorCardDetails,
+  ProfessorCardName,
+  ProfessorCardSeparator,
+} from "../Components/Styled/ProfessorCard.style";
+import AuthApi from "../api/AuthApi";
+import { AddOpinionButton } from "../Components/Styled/Modal.style";
+import {
+  EntityLi,
+  EntityUl,
+  EntityParam,
+} from "../Components/Styled/EntityList.style";
+import {
+  Modal,
+  ModalBody,
+  ModalClose,
+  ModalContent,
+  ModalFooter,
+  ModalHeader,
+  ModalInput,
+  ModalTextarea,
+} from "../Components/Styled/Modal.style";
+import axios from "../api/axios";
 
 const Subject = () => {
   let params = useParams();
-  return <div>Subject {params.subjectId}</div>;
+  let navigate = useNavigate();
+
+  const { auth, setAuth } = useContext(AuthApi);
+  const [subject, setSubject] = useState(null);
+  const [loaded, setLoaded] = useState(false);
+  const [fetchError, setFetchError] = useState(false);
+  var totalTopics = 0;
+  var topics = [];
+
+  const [topicModalDisplay, setTopicModalDisplay] = useState("none");
+  const [topicTitle, setTopicTitle] = useState("");
+  const [topicContent, setTopicContent] = useState("");
+  const [errorMessage, setErrorMessage] = useState("");
+
+  useEffect(() => {
+    const url = `http://192.168.0.17:8080/public/subject/${params.subjectId}`;
+
+    const fetchData = async () => {
+      try {
+        const response = await fetch(url);
+        let cyclicGraph = await response.json();
+        let jsogStructure = JSOG.encode(cyclicGraph);
+        cyclicGraph = JSOG.decode(jsogStructure);
+        setSubject(cyclicGraph);
+        setLoaded(true);
+      } catch (error) {
+        setFetchError(true);
+      }
+    };
+
+    fetchData();
+  }, [params.subjectId]);
+
+  const handleAddTopicButtonClick = () => {
+    if (auth) {
+      setTopicModalDisplay("block");
+    } else {
+      navigate("/login");
+    }
+  };
+
+  const handleModalCloseClick = () => {
+    setTopicModalDisplay("none");
+  };
+
+  const handleTopicSubmit = async (e) => {
+    e.preventDefault();
+
+    if (!topicTitle.length < 1 && !topicContent.length < 1) {
+      const response = await axios(
+        `http://192.168.0.17:8080/secure/subject/${params.subjectId}/addThread`,
+        {
+          method: "post",
+          data: {
+            title: topicTitle,
+            content: topicContent,
+          },
+          withCredentials: true,
+        }
+      );
+      setErrorMessage("");
+      window.location.reload(false);
+    } else {
+      setErrorMessage("Полињата за наслов и содржина не смеат да бидат празни");
+    }
+  };
+
+  const handleContentChange = (e) => {
+    setTopicContent(e.target.value);
+  };
+
+  const handleTitleChange = (e) => {
+    setTopicTitle(e.target.value);
+  };
+
+  return loaded ? (
+    <>
+      <CurrentPageNav>
+        &#187;{" "}
+        <a
+          href={
+            "/university/" +
+            subject.studyProgramme.faculty.university.universityId
+          }
+        >
+          {subject.studyProgramme.faculty.university.universityName}
+        </a>{" "}
+        &#187;{" "}
+        <a href={"/faculty/" + subject.studyProgramme.faculty.facultyId}>
+          {subject.studyProgramme.faculty.facultyName}
+        </a>{" "}
+        &#187; <a href="#">{subject.subjectName}</a>
+      </CurrentPageNav>
+      <ProfessorCard>
+        <ProfessorCardName>{subject.subjectName}</ProfessorCardName>
+        <ProfessorCardSeparator />
+        <div style={{ marginTop: "10px" }}>
+          <ProfessorCardDetails fontSize="20px">
+            {subject.studyProgramme.studyProgrammeName} (
+            {subject.studyProgramme.cycle}
+            {"."}
+            {"циклус"})
+          </ProfessorCardDetails>
+          <ProfessorCardDetails fontSize="20px">
+            {subject.studyProgramme.faculty.facultyName}
+          </ProfessorCardDetails>
+          <ProfessorCardDetails fontSize="15px">
+            {subject.studyProgramme.faculty.university.universityName}
+          </ProfessorCardDetails>
+        </div>
+      </ProfessorCard>
+      <div style={{ height: "20px", marginBottom: "50px" }}>
+        <h3
+          style={{
+            float: "left",
+          }}
+        >
+          {subject.threads.map((thread) => {
+            if (thread.parent === null) {
+              totalTopics++;
+              topics.push(thread);
+            }
+          })}
+          {totalTopics} {totalTopics !== 1 ? "теми" : "тема"}
+        </h3>
+        {auth && (
+          <AddOpinionButton onClick={handleAddTopicButtonClick}>
+            Отвори тема
+          </AddOpinionButton>
+        )}
+      </div>
+      <Modal display={topicModalDisplay}>
+        <ModalContent>
+          <ModalHeader>
+            <ModalClose onClick={handleModalCloseClick}>&times;</ModalClose>
+            <h3 style={{ marginTop: "5px" }}>
+              Тема во врска со {subject.subjectName}
+            </h3>
+          </ModalHeader>
+          <form onSubmit={handleTopicSubmit}>
+            <ModalBody>
+              <label htmlFor="title">
+                <b>Наслов</b>:
+                <ModalInput
+                  id="title"
+                  value={topicTitle}
+                  onChange={handleTitleChange}
+                />
+              </label>
+              <label htmlFor="content">
+                <b>Содржина</b>:
+                <ModalTextarea
+                  id="content"
+                  rows="8"
+                  cols="100"
+                  value={topicContent}
+                  onChange={handleContentChange}
+                />
+              </label>
+            </ModalBody>
+            <p style={{ color: "red", marginLeft: "15px", marginTop: "10px" }}>
+              {errorMessage}
+            </p>
+            <ModalFooter type="submit">ОБЈАВИ</ModalFooter>
+          </form>
+        </ModalContent>
+      </Modal>
+      <div key={subject.subjectId}>
+        {topics.map((topic) => {
+          var numReplies = topic.children.length;
+          return (
+            <EntityUl key={topic.postId}>
+              <EntityLi bgcolor="cornsilk">
+                <a href={"/topic/" + topic.postId}>{topic.title}</a>
+                <EntityParam right="30px">
+                  <span style={{ fontWeight: "normal" }}>
+                    отворил:{" "}
+                    <a href={"/user/" + topic.author.id}>
+                      {topic.author.username}
+                    </a>
+                  </span>
+                  <span style={{ fontStyle: "normal" }}>,</span>{" "}
+                  {numReplies !== 1 ? (
+                    numReplies !== 0 ? (
+                      <span
+                        style={{
+                          fontWeight: "normal",
+                          opacity: numReplies === 0 ? "0.5" : "1",
+                        }}
+                      >
+                        <span
+                          style={{
+                            fontWeight: "bold",
+                            opacity: numReplies === 0 ? "0.5" : "1",
+                          }}
+                        >
+                          {numReplies}
+                        </span>{" "}
+                        реплики
+                      </span>
+                    ) : (
+                      <span
+                        style={{
+                          fontWeight: "normal",
+                          opacity: numReplies === 0 ? "0.5" : "1",
+                        }}
+                      >
+                        <span
+                          style={{
+                            fontWeight: "bold",
+                            opacity: numReplies === 0 ? "0.5" : "1",
+                          }}
+                        >
+                          {numReplies}
+                        </span>{" "}
+                        реплики
+                      </span>
+                    )
+                  ) : (
+                    <span style={{ fontWeight: "normal" }}>
+                      <span style={{ fontWeight: "bold" }}>{numReplies}</span>{" "}
+                      реплика
+                    </span>
+                  )}
+                </EntityParam>
+              </EntityLi>
+            </EntityUl>
+          );
+        })}
+      </div>
+    </>
+  ) : !fetchError ? (
+    <div>
+      <p style={{ marginTop: "140px" }}>се вчитува...</p>
+      <Outlet />
+    </div>
+  ) : (
+    <div style={{ marginTop: "140px" }}>
+      <h1 style={{ textAlign: "center" }}>Страницата не е пронајдена.</h1>
+    </div>
+  );
 };
 
Index: reactapp/src/Pages/Topic.js
===================================================================
--- reactapp/src/Pages/Topic.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
+++ reactapp/src/Pages/Topic.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -0,0 +1,538 @@
+import React, { useState, useEffect, useContext } from "react";
+import { useParams, Outlet } from "react-router-dom";
+import JSOG from "jsog";
+import { CurrentPageNav } from "../Components/Styled/Main.style";
+import AuthApi from "../api/AuthApi";
+import { useNavigate } from "react-router-dom";
+import {
+  OpinionCard,
+  OpinionCardContent,
+  OpinionCardContentTime,
+  OpinionReplyCard,
+  OpinionReplyCardContent,
+  OpinionReplyCardContentTime,
+  VoteCount,
+} from "../Components/Styled/OpinionCard.style";
+import { dateConverter } from "../Util/dateConverter";
+import { StyledFontAwesomeIcon } from "../Components/Styled/OpinionCard.style";
+import { solid } from "@fortawesome/fontawesome-svg-core/import.macro";
+import {
+  AddOpinionButton,
+  Modal,
+  ModalContent,
+  ModalHeader,
+  ModalClose,
+  ModalBody,
+  ModalTextarea,
+  ModalFooter,
+} from "../Components/Styled/Modal.style";
+import axios from "../api/axios";
+
+const Topic = () => {
+  let params = useParams();
+  let navigate = useNavigate();
+  const { auth, setAuth } = useContext(AuthApi);
+
+  const [thread, setThread] = useState(null);
+  const [loadedThread, setLoadedThread] = useState(false);
+  const [user, setUser] = useState(null);
+  const [loadedUser, setLoadedUser] = useState(false);
+  const [fetchError, setFetchError] = useState(false);
+
+  const [postModalDisplay, setPostModalDisplay] = useState("none");
+  const [postContent, setPostContent] = useState("");
+  const [replyModalDisplay, setReplyModalDisplay] = useState("none");
+  const [replyContent, setReplyContent] = useState("");
+  const [postForModal, setPostForModal] = useState(null);
+  const [errorMessage, setErrorMessage] = useState("");
+
+  useEffect(() => {
+    const url1 = `http://192.168.0.17:8080/public/thread/${params.topicId}`;
+    const url2 = `http://192.168.0.17:8080/secure/currentUser`;
+
+    const fetchTopic = async () => {
+      try {
+        const response = await fetch(url1);
+        let cyclicGraph = await response.json();
+        let jsogStructure = JSOG.encode(cyclicGraph);
+        cyclicGraph = JSOG.decode(jsogStructure);
+        setThread(cyclicGraph);
+        setLoadedThread(true);
+      } catch (error) {
+        setFetchError(true);
+      }
+    };
+
+    const fetchUser = async () => {
+      try {
+        const response = await axios.get(url2, { withCredentials: true });
+        var cyclicGraph = await response.data;
+        var jsogStructure = JSOG.encode(cyclicGraph);
+        cyclicGraph = JSOG.decode(jsogStructure);
+        setUser(cyclicGraph);
+        setLoadedUser(true);
+      } catch (error) {
+        setFetchError(true);
+      }
+    };
+
+    fetchTopic().then(fetchUser);
+  }, []);
+
+  const handleReply = (post) => {
+    if (auth) {
+      setReplyModalDisplay("block");
+      setPostForModal(post);
+    } else {
+      navigate("/login");
+    }
+  };
+
+  const handleReplyContentChange = (e) => {
+    setReplyContent(e.target.value);
+  };
+
+  const handleReplySubmit = async (e, postId) => {
+    e.preventDefault();
+
+    if (!replyContent.length < 1) {
+      const response = await axios(
+        `http://192.168.0.17:8080/secure/subject/${thread.targetSubject.subjectId}/replyToThread/${postId}`,
+        {
+          method: "post",
+          data: {
+            content: replyContent,
+          },
+          withCredentials: true,
+        }
+      );
+      setErrorMessage("");
+      window.location.reload(false);
+    } else {
+      setErrorMessage("Полето за содржина не смее да биде празно");
+    }
+  };
+
+  const handleAddOpinionButtonClick = () => {
+    if (auth) {
+      setPostModalDisplay("block");
+    } else {
+      navigate("/login");
+    }
+  };
+
+  const handlePostSubmit = async (e) => {
+    e.preventDefault();
+    if (!postContent.length < 1) {
+      const response = await axios(
+        `http://192.168.0.17:8080/secure/subject/${thread.targetSubject.subjectId}/replyToThread/${params.topicId}`,
+        {
+          method: "post",
+          data: {
+            content: postContent,
+          },
+          withCredentials: true,
+        }
+      );
+      setErrorMessage("");
+      window.location.reload(false);
+    } else {
+      setErrorMessage("Полето за содржина не смее да биде празно");
+    }
+  };
+  const handleModalCloseClick = () => {
+    setPostModalDisplay("none");
+    setReplyModalDisplay("none");
+  };
+  const handleContentChange = (e) => {
+    setPostContent(e.target.value);
+  };
+
+  const handleLike = async (post) => {
+    if (auth) {
+      if (
+        loadedUser &&
+        user &&
+        !post.votes.some((e) => e.user.id === user.id)
+      ) {
+        const response = await axios(
+          `http://192.168.0.17:8080/secure/upvoteThread/${post.postId}`,
+          {
+            method: "get",
+            withCredentials: true,
+          }
+        );
+        window.location.reload(false);
+      } else {
+        return;
+      }
+    } else {
+      navigate("/login");
+    }
+  };
+
+  const handleDislike = async (post) => {
+    if (auth) {
+      if (
+        loadedUser &&
+        user &&
+        !post.votes.some((e) => e.user.id === user.id)
+      ) {
+        const response = await axios(
+          `http://192.168.0.17:8080/secure/downvoteThread/${post.postId}`,
+          {
+            method: "get",
+            withCredentials: true,
+          }
+        );
+
+        window.location.reload(false);
+      } else {
+        return;
+      }
+    } else {
+      navigate("/login");
+    }
+  };
+
+  function displayChildPosts(child, parentPostAuthorUsername, replyIndent) {
+    if (child == null) return;
+    //postCount = renderedOpinionIds.push(child.postId);
+    return (
+      <div key={child.postId}>
+        <OpinionReplyCard indent={replyIndent + "px"}>
+          <OpinionReplyCardContent>
+            <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
+              <a href={"/user/" + child.author.id}>{child.author.username}</a>{" "}
+              му реплицирал на {parentPostAuthorUsername}
+            </p>
+            <p style={{ marginBottom: "10px", maxWidth: "90%" }}>
+              {child.content}
+            </p>
+            <OpinionReplyCardContentTime>
+              {dateConverter(
+                new Date(child.timePosted).toString().slice(4, -43)
+              )}
+            </OpinionReplyCardContentTime>
+
+            <div
+              style={{
+                display:
+                  !auth || (auth && loadedUser && user.id !== child.author.id)
+                    ? "block"
+                    : "none",
+              }}
+            >
+              <StyledFontAwesomeIcon
+                icon={solid("thumbs-up")}
+                right={50 + "px"}
+                color={
+                  auth && loadedUser && user
+                    ? child.votes.some(
+                        (e) => e.vote === "UPVOTE" && e.user.id === user.id
+                      )
+                      ? "greenyellow"
+                      : "darkgrey"
+                    : "darkgrey"
+                }
+                onClick={() => handleLike(child)}
+              />
+
+              <VoteCount right={50 + "px"}>
+                {child.votes.filter((v) => v.vote === "UPVOTE").length}
+              </VoteCount>
+
+              <StyledFontAwesomeIcon
+                icon={solid("thumbs-down")}
+                right={10 + "px"}
+                color={
+                  auth && loadedUser && user
+                    ? child.votes.some(
+                        (e) => e.vote === "DOWNVOTE" && e.user.id === user.id
+                      )
+                      ? "indianred"
+                      : "darkgrey"
+                    : "darkgrey"
+                }
+                onClick={() => handleDislike(child)}
+              />
+
+              <VoteCount right={50 + "px"}>
+                {child.votes.filter((v) => v.vote === "DOWNVOTE").length}
+              </VoteCount>
+
+              <StyledFontAwesomeIcon
+                icon={solid("reply")}
+                right={90 + "px"}
+                color="darkgrey"
+                onClick={() => handleReply(child)}
+              />
+            </div>
+          </OpinionReplyCardContent>
+
+          {child.children.map((childOfChild) =>
+            displayChildPosts(
+              childOfChild,
+              child.author.username,
+              replyIndent + 30
+            )
+          )}
+        </OpinionReplyCard>
+      </div>
+    );
+  }
+
+  return loadedThread && thread.length !== 0 ? (
+    <>
+      <CurrentPageNav>
+        &#187;{" "}
+        <a
+          href={
+            "/university/" +
+            thread.targetSubject.studyProgramme.faculty.university.universityId
+          }
+        >
+          {
+            thread.targetSubject.studyProgramme.faculty.university
+              .universityName
+          }
+        </a>{" "}
+        &#187;{" "}
+        <a
+          href={
+            "/faculty/" + thread.targetSubject.studyProgramme.faculty.facultyId
+          }
+        >
+          {thread.targetSubject.studyProgramme.faculty.facultyName}
+        </a>{" "}
+        &#187;{" "}
+        <a href={"/subject/" + thread.targetSubject.subjectId}>
+          {thread.targetSubject.subjectName}
+        </a>
+      </CurrentPageNav>
+      <div style={{ height: "20px", marginBottom: "50px", marginTop: "50px" }}>
+        <h3 style={{ float: "left" }}>{thread.title}</h3>
+        {auth && (
+          <AddOpinionButton onClick={handleAddOpinionButtonClick}>
+            Реплицирај
+          </AddOpinionButton>
+        )}
+      </div>
+      <Modal display={postModalDisplay}>
+        <ModalContent>
+          <ModalHeader>
+            <ModalClose onClick={handleModalCloseClick}>&times;</ModalClose>
+            <h3 style={{ marginTop: "5px" }}>
+              Реплика на темата {thread.title}
+            </h3>
+          </ModalHeader>
+          <form onSubmit={handlePostSubmit}>
+            <ModalBody>
+              <label htmlFor="content">
+                <b>Содржина</b>:
+                <ModalTextarea
+                  id="content"
+                  rows="8"
+                  cols="100"
+                  value={postContent}
+                  onChange={handleContentChange}
+                />
+              </label>
+            </ModalBody>
+            <p style={{ color: "red", marginLeft: "15px", marginTop: "10px" }}>
+              {errorMessage}
+            </p>
+            <ModalFooter type="submit">ОБЈАВИ</ModalFooter>
+          </form>
+        </ModalContent>
+      </Modal>
+      <OpinionCard>
+        <OpinionCardContent>
+          <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
+            <a href={"/user/" + thread.author.id}>{thread.author.username}</a>{" "}
+            напишал
+          </p>
+          <p style={{ marginBottom: "10px", maxWidth: "90%" }}>
+            {thread.content}
+          </p>
+          <OpinionCardContentTime>
+            {dateConverter(
+              new Date(thread.timePosted).toString().slice(4, -43)
+            )}
+          </OpinionCardContentTime>
+          <div
+            style={{
+              display:
+                !auth || (auth && loadedUser && user.id !== thread.author.id)
+                  ? "block"
+                  : "none",
+            }}
+          >
+            <StyledFontAwesomeIcon
+              icon={solid("thumbs-up")}
+              right={50 + "px"}
+              color={
+                auth && loadedUser && user
+                  ? thread.votes.some(
+                      (e) => e.vote === "UPVOTE" && e.user.id === user.id
+                    )
+                    ? "greenyellow"
+                    : "darkgrey"
+                  : "darkgrey"
+              }
+              onClick={() => handleLike(thread)}
+            />
+
+            <VoteCount right={50 + "px"}>
+              {thread.votes.filter((v) => v.vote === "UPVOTE").length}
+            </VoteCount>
+
+            <StyledFontAwesomeIcon
+              icon={solid("thumbs-down")}
+              right={10 + "px"}
+              color={
+                auth && loadedUser && user
+                  ? thread.votes.some(
+                      (e) => e.vote === "DOWNVOTE" && e.user.id === user.id
+                    )
+                    ? "indianred"
+                    : "darkgrey"
+                  : "darkgrey"
+              }
+              onClick={() => handleDislike(thread)}
+            />
+
+            <VoteCount right={10 + "px"}>
+              {thread.votes.filter((v) => v.vote === "DOWNVOTE").length}
+            </VoteCount>
+          </div>
+        </OpinionCardContent>
+      </OpinionCard>
+      {thread.children.map((directChild) => {
+        return (
+          <OpinionCard key={directChild.postId}>
+            <OpinionCardContent>
+              <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
+                <a href={"/user/" + directChild.author.id}>
+                  {directChild.author.username}
+                </a>{" "}
+                напишал
+              </p>
+              <p style={{ marginBottom: "10px", maxWidth: "90%" }}>
+                {directChild.content}
+              </p>
+              <OpinionCardContentTime>
+                {dateConverter(
+                  new Date(directChild.timePosted).toString().slice(4, -43)
+                )}
+              </OpinionCardContentTime>
+              <div
+                style={{
+                  display:
+                    !auth ||
+                    (auth && loadedUser && user.id !== directChild.author.id)
+                      ? "block"
+                      : "none",
+                }}
+              >
+                <StyledFontAwesomeIcon
+                  icon={solid("thumbs-up")}
+                  right={50 + "px"}
+                  color={
+                    auth && loadedUser && user
+                      ? directChild.votes.some(
+                          (e) => e.vote === "UPVOTE" && e.user.id === user.id
+                        )
+                        ? "greenyellow"
+                        : "darkgrey"
+                      : "darkgrey"
+                  }
+                  onClick={() => handleLike(directChild)}
+                />
+
+                <VoteCount right={50 + "px"}>
+                  {directChild.votes.filter((v) => v.vote === "UPVOTE").length}
+                </VoteCount>
+
+                <StyledFontAwesomeIcon
+                  icon={solid("thumbs-down")}
+                  right={10 + "px"}
+                  color={
+                    auth && loadedUser && user
+                      ? directChild.votes.some(
+                          (e) => e.vote === "DOWNVOTE" && e.user.id === user.id
+                        )
+                        ? "indianred"
+                        : "darkgrey"
+                      : "darkgrey"
+                  }
+                  onClick={() => handleDislike(directChild)}
+                />
+
+                <VoteCount right={10 + "px"}>
+                  {
+                    directChild.votes.filter((v) => v.vote === "DOWNVOTE")
+                      .length
+                  }
+                </VoteCount>
+
+                <StyledFontAwesomeIcon
+                  icon={solid("reply")}
+                  right={90 + "px"}
+                  color="darkgrey"
+                  onClick={() => handleReply(directChild)}
+                />
+              </div>
+            </OpinionCardContent>
+            {directChild.children.map((child) =>
+              displayChildPosts(child, directChild.author.username, 30)
+            )}
+          </OpinionCard>
+        );
+      })}
+      {postForModal && (
+        <Modal display={replyModalDisplay}>
+          <ModalContent>
+            <ModalHeader>
+              <ModalClose onClick={handleModalCloseClick}>&times;</ModalClose>
+              <h3 style={{ marginTop: "5px" }}>
+                Реплика на {postForModal.author.username}
+              </h3>
+            </ModalHeader>
+            <form onSubmit={(e) => handleReplySubmit(e, postForModal.postId)}>
+              <ModalBody>
+                <label htmlFor="content">
+                  <b>Содржина</b>:
+                  <ModalTextarea
+                    id="content"
+                    rows="8"
+                    cols="100"
+                    value={replyContent}
+                    onChange={handleReplyContentChange}
+                  />
+                </label>
+              </ModalBody>
+              <p
+                style={{ color: "red", marginLeft: "15px", marginTop: "10px" }}
+              >
+                {errorMessage}
+              </p>
+              <ModalFooter type="submit">РЕПЛИЦИРАЈ</ModalFooter>
+            </form>
+          </ModalContent>
+        </Modal>
+      )}
+    </>
+  ) : !fetchError && !loadedThread ? (
+    <div>
+      <p style={{ marginTop: "140px" }}>се вчитува...</p>
+      <Outlet />
+    </div>
+  ) : (
+    <div style={{ marginTop: "140px" }}>
+      <h1 style={{ textAlign: "center" }}>Страницата не е пронајдена.</h1>
+    </div>
+  );
+};
+
+export default Topic;
Index: reactapp/src/Pages/University.js
===================================================================
--- reactapp/src/Pages/University.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Pages/University.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -18,5 +18,5 @@
   let params = useParams();
   const [loaded, setLoaded] = useState(false);
-  const [faculties, setFaculties] = useState(false);
+  const [faculties, setFaculties] = useState(null);
   const [fetchError, setFetchError] = useState(false);
 
@@ -39,5 +39,5 @@
   }, [params.universityId]);
 
-  return loaded ? (
+  return loaded && !fetchError && faculties.length !== 0 ? (
     <>
       <CurrentPageNav>
@@ -74,5 +74,5 @@
                   {faculty.facultyName}
                 </a>
-                <EntityParam>
+                <EntityParam right="30px">
                   {totalSections}{" "}
                   {totalSections !== 1 ? (
@@ -103,5 +103,5 @@
       </div>
     </>
-  ) : !fetchError ? (
+  ) : !fetchError && !loaded ? (
     <div>
       <p style={{ marginTop: "140px" }}>се вчитува...</p>
Index: reactapp/src/Pages/UserDashboard.js
===================================================================
--- reactapp/src/Pages/UserDashboard.js	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ reactapp/src/Pages/UserDashboard.js	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -1,8 +1,7 @@
-import React, { useEffect } from "react";
+import React, { useEffect, useState, useContext } from "react";
 import {
   OpinionCard,
   OpinionCardContent,
   OpinionCardContentTime,
-  OpinionCardContentTitle,
 } from "../Components/Styled/OpinionCard.style";
 import {
@@ -11,14 +10,47 @@
 } from "../Components/Styled/UserDetails.style";
 import { dateConverter } from "../Util/dateConverter";
+import axios from "../api/axios";
+import JSOG from "jsog";
+import AuthApi from "../api/AuthApi";
 
-function UserDashboard({ user, userLoaded }) {
+function UserDashboard() {
+  const { auth, setAuth } = useContext(AuthApi);
+
+  const [user, setUser] = useState(null);
+  const [loadedUser, setLoadedUser] = useState(false);
+  const [fetchError, setFetchError] = useState(false);
+
   useEffect(() => {
-    const timer = setTimeout(() => {
-      if (user === null) window.location.reload(false);
-    }, 3000);
-    return () => clearTimeout(timer);
+    const url = `http://192.168.0.17:8080/secure/currentUser`;
+
+    const fetchUser = async () => {
+      try {
+        const response = await axios.get(url, { withCredentials: true });
+        var cyclicGraph = await response.data;
+        var jsogStructure = JSOG.encode(cyclicGraph);
+        cyclicGraph = JSOG.decode(jsogStructure);
+        setUser(cyclicGraph);
+        setLoadedUser(true);
+      } catch (error) {
+        setFetchError(true);
+      }
+    };
+
+    if (auth) fetchUser();
   }, []);
 
-  return userLoaded ? (
+  // useEffect(() => {
+  //   const timer = setTimeout(() => {
+  //     if (user === null) window.location.reload(false); <---- :-)
+  //   }, 3000);
+  //   return () => clearTimeout(timer);
+  // }, []);
+
+  function findParentThread(post) {
+    if (post.parent === null) return post;
+    return findParentThread(post.parent);
+  }
+
+  return loadedUser ? (
     <>
       <h3>Кориснички податоци:</h3>
@@ -49,12 +81,23 @@
             <OpinionCard>
               <OpinionCardContent>
-                <p>
-                  Во дискусија за{" "}
-                  <a href={"/professor/" + post.targetProfessor.professorId}>
-                    {post.targetProfessor.professorName}
-                  </a>
+                <p style={{ fontStyle: "italic", marginBottom: "10px" }}>
+                  во дискусија за{" "}
+                  {post.targetProfessor !== undefined ? (
+                    <a href={"/professor/" + post.targetProfessor.professorId}>
+                      {post.targetProfessor.professorName}
+                    </a>
+                  ) : (
+                    <a
+                      href={
+                        post.parent === null
+                          ? "/topic/" + post.postId
+                          : "/topic/" + findParentThread(post).postId
+                      }
+                    >
+                      {post.targetSubject.subjectName}
+                    </a>
+                  )}
                 </p>
-                <OpinionCardContentTitle>{post.title}</OpinionCardContentTitle>
-                <p>{post.content}</p>
+                <p style={{ marginBottom: "10px" }}>{post.content}</p>
                 <OpinionCardContentTime>
                   {dateConverter(
Index: springapp/pom.xml
===================================================================
--- springapp/pom.xml	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/pom.xml	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -30,5 +30,4 @@
 			<artifactId>spring-boot-starter-web</artifactId>
 		</dependency>
-
 		<dependency>
 			<groupId>org.postgresql</groupId>
@@ -63,4 +62,5 @@
 			<scope>compile</scope>
 		</dependency>
+		
 	</dependencies>
 
Index: springapp/src/main/java/mk/profesori/springapp/Controller/PublicController.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Controller/PublicController.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Controller/PublicController.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -18,10 +18,12 @@
 import mk.profesori.springapp.Model.Professor;
 import mk.profesori.springapp.Model.StudyProgramme;
+import mk.profesori.springapp.Model.Subject;
 import mk.profesori.springapp.Model.University;
+import mk.profesori.springapp.Model._Thread;
 import mk.profesori.springapp.Service.MainService;
 
 @RestController
 @RequestMapping("/public")
-@CrossOrigin(origins = { "http://192.168.0.17:3000", "http://192.168.0.28:3000" })
+@CrossOrigin(origins = { "http://192.168.0.17:3000", "http://192.168.0.39:3000" })
 public class PublicController {
 
@@ -98,4 +100,14 @@
     }
 
+    @RequestMapping(value = "/subject/{subjectId}", method = RequestMethod.GET)
+    public Subject getSubjectById(@PathVariable Long subjectId) {
+        return mainService.getSubjectById(subjectId); // vrakja predmet spored id
+    }
+
+    @RequestMapping(value = "/thread/{postId}", method = RequestMethod.GET)
+    public _Thread getThreadById(@PathVariable Long postId) {
+        return mainService.get_ThreadById(postId); // vrakja thread (tema) spored id
+    }
+
     @RequestMapping(value = "/loginSuccessRegular", method = RequestMethod.GET)
     public Map<String, String> loginSuccessRegular(@RequestParam String sessionId) {
Index: springapp/src/main/java/mk/profesori/springapp/Controller/SecureController.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Controller/SecureController.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Controller/SecureController.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -1,35 +1,27 @@
 package mk.profesori.springapp.Controller;
 
-import java.util.Collections;
-import java.util.Map;
-
-import org.springframework.beans.factory.annotation.Autowired;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import mk.profesori.springapp.Model.CustomUserDetails;
+import mk.profesori.springapp.Service.CustomUserDetailsService;
+import mk.profesori.springapp.Service.MainService;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.annotation.CurrentSecurityContext;
 import org.springframework.security.core.context.SecurityContext;
 import org.springframework.security.core.userdetails.UserDetails;
-import org.springframework.web.bind.annotation.CrossOrigin;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-import mk.profesori.springapp.Model.CustomUserDetails;
-import mk.profesori.springapp.Service.CustomUserDetailsService;
-import mk.profesori.springapp.Service.MainService;
+import org.springframework.web.bind.annotation.*;
 
 @RestController
 @RequestMapping("/secure")
-@CrossOrigin(origins = { "http://192.168.0.17:3000", "http://192.168.0.28:3000" })
+@CrossOrigin(origins = { "http://192.168.0.17:3000", "http://192.168.0.39:3000" })
 public class SecureController {
 
-    @Autowired
-    private MainService mainService;
-    @Autowired
+    private final MainService mainService;
+    final
     CustomUserDetailsService customUserDetailsService;
+
+    public SecureController(MainService mainService, CustomUserDetailsService customUserDetailsService) {
+        this.mainService = mainService;
+        this.customUserDetailsService = customUserDetailsService;
+    }
 
     @RequestMapping(value = "/professor/{professorId}/addOpinion", method = RequestMethod.POST)
@@ -39,9 +31,7 @@
         Authentication authentication = context.getAuthentication();
 
-        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) {
-            CustomUserDetails currentUser = (CustomUserDetails) authentication.getPrincipal();
-            String title = objectNode.get("title").asText();
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
             String content = objectNode.get("content").asText();
-            mainService.addOpinion(title, content, professorId, currentUser);
+            mainService.addOpinion(content, professorId, currentUser);
         }
     }
@@ -53,8 +43,32 @@
         Authentication authentication = context.getAuthentication();
 
-        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) {
-            CustomUserDetails currentUser = (CustomUserDetails) authentication.getPrincipal();
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
             String content = objectNode.get("content").asText();
             mainService.replyToOpinion(content, professorId, postId, currentUser);
+        }
+    }
+
+    @RequestMapping(value = "/subject/{subjectId}/addThread", method = RequestMethod.POST)
+    public void addThread(@RequestBody ObjectNode objectNode, @PathVariable Long subjectId,
+            @CurrentSecurityContext SecurityContext context) {
+
+        Authentication authentication = context.getAuthentication();
+
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
+            String title = objectNode.get("title").asText();
+            String content = objectNode.get("content").asText();
+            mainService.addThread(title, content, subjectId, currentUser);
+        }
+    }
+
+    @RequestMapping(value = "/subject/{subjectId}/replyToThread/{postId}", method = RequestMethod.POST)
+    public void replyToThread(@RequestBody ObjectNode objectNode, @PathVariable Long subjectId,
+            @PathVariable Long postId, @CurrentSecurityContext SecurityContext context) {
+
+        Authentication authentication = context.getAuthentication();
+
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
+            String content = objectNode.get("content").asText();
+            mainService.replyToThread(content, subjectId, postId, currentUser);
         }
     }
@@ -64,6 +78,5 @@
 
         Authentication authentication = context.getAuthentication();
-        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) {
-            CustomUserDetails currentUser = (CustomUserDetails) authentication.getPrincipal();
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
             return customUserDetailsService.loadUserByUsername(currentUser.getEmail());
         }
@@ -72,27 +85,43 @@
     }
 
-    @RequestMapping(value = "/professor/{professorId}/upvoteOpinion/{postId}", method = RequestMethod.GET)
-    public void upvoteOpinion(@PathVariable Long professorId,
-            @PathVariable Long postId, @CurrentSecurityContext SecurityContext context) {
+    @RequestMapping(value = "/upvoteOpinion/{postId}", method = RequestMethod.GET)
+    public void upvoteOpinion(@PathVariable Long postId, @CurrentSecurityContext SecurityContext context) {
 
         Authentication authentication = context.getAuthentication();
 
-        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) {
-            CustomUserDetails currentUser = (CustomUserDetails) authentication.getPrincipal();
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
             mainService.upvoteOpinion(postId, currentUser);
         }
     }
 
-    @RequestMapping(value = "/professor/{professorId}/downvoteOpinion/{postId}", method = RequestMethod.GET)
-    public void downvoteOpinion(@PathVariable Long professorId,
-            @PathVariable Long postId, @CurrentSecurityContext SecurityContext context) {
+    @RequestMapping(value = "/downvoteOpinion/{postId}", method = RequestMethod.GET)
+    public void downvoteOpinion(@PathVariable Long postId, @CurrentSecurityContext SecurityContext context) {
 
         Authentication authentication = context.getAuthentication();
 
-        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails) {
-            CustomUserDetails currentUser = (CustomUserDetails) authentication.getPrincipal();
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
             mainService.downvoteOpinion(postId, currentUser);
         }
     }
 
+    @RequestMapping(value = "/upvoteThread/{postId}", method = RequestMethod.GET)
+    public void upvoteThread(@PathVariable Long postId, @CurrentSecurityContext SecurityContext context) {
+
+        Authentication authentication = context.getAuthentication();
+
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
+            mainService.upvote_Thread(postId, currentUser);
+        }
+    }
+
+    @RequestMapping(value = "/downvoteThread/{postId}", method = RequestMethod.GET)
+    public void downvoteThread(@PathVariable Long postId, @CurrentSecurityContext SecurityContext context) {
+
+        Authentication authentication = context.getAuthentication();
+
+        if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails currentUser) {
+            mainService.downvote_Thread(postId, currentUser);
+        }
+    }
+
 }
Index: springapp/src/main/java/mk/profesori/springapp/Model/ConfirmationToken.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/ConfirmationToken.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Model/ConfirmationToken.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -12,4 +12,7 @@
 import javax.persistence.SequenceGenerator;
 
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.voodoodyne.jackson.jsog.JSOGGenerator;
+
 import lombok.Getter;
 import lombok.NoArgsConstructor;
@@ -20,6 +23,7 @@
 @NoArgsConstructor
 @Entity
+@JsonIdentityInfo(generator = JSOGGenerator.class)
 public class ConfirmationToken {
-    
+
     @Id
     @SequenceGenerator(name = "confirmation_token_sequence", sequenceName = "confirmation_token_sequence", allocationSize = 1)
@@ -42,7 +46,7 @@
     private CustomUserDetails customUserDetails;
 
-    public ConfirmationToken(String token, LocalDateTime createdAt, LocalDateTime expiredAt, 
-    CustomUserDetails customUserDetails) {
-        
+    public ConfirmationToken(String token, LocalDateTime createdAt, LocalDateTime expiredAt,
+            CustomUserDetails customUserDetails) {
+
         this.token = token;
         this.createdAt = createdAt;
Index: springapp/src/main/java/mk/profesori/springapp/Model/CustomUserDetails.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/CustomUserDetails.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Model/CustomUserDetails.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -1,36 +1,19 @@
 package mk.profesori.springapp.Model;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import javax.persistence.CascadeType;
-import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
-import javax.persistence.FetchType;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinTable;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToMany;
-import javax.persistence.OneToMany;
-import javax.persistence.SequenceGenerator;
-
+import com.fasterxml.jackson.annotation.JsonIdentityInfo;
+import com.voodoodyne.jackson.jsog.JSOGGenerator;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
 import org.springframework.security.core.GrantedAuthority;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 
-import com.fasterxml.jackson.annotation.JsonIdentityInfo;
-import com.voodoodyne.jackson.jsog.JSOGGenerator;
-
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
+import javax.persistence.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
 
 @Getter
@@ -59,10 +42,15 @@
     private Set<Post> authoredPosts = new HashSet<>();
     private Integer karma = 0;
-    @ManyToMany(fetch = FetchType.EAGER)
-    @JoinTable(name = "post_like", joinColumns = @JoinColumn(name = "custom_user_details_id"), inverseJoinColumns = @JoinColumn(name = "post_id"))
-    Set<Post> likedPosts;
-    @ManyToMany(fetch = FetchType.EAGER)
-    @JoinTable(name = "post_dislike", joinColumns = @JoinColumn(name = "custom_user_details_id"), inverseJoinColumns = @JoinColumn(name = "post_id"))
-    Set<Post> dislikedPosts;
+
+    public Set<PostVote> getVotes() {
+        return votes;
+    }
+
+    public void setVotes(Set<PostVote> votes) {
+        this.votes = votes;
+    }
+
+    @OneToMany(mappedBy = "user")
+    private Set<PostVote> votes = new HashSet<>();
 
     public CustomUserDetails(String fullName, String username, String email, String password, UserRole userRole) {
@@ -122,18 +110,8 @@
     }
 
-    public Set<Post> getLikedPosts() {
-        return this.likedPosts;
+    @Override
+    public String toString() {
+        return this.id.toString();
     }
 
-    public void setLikedPosts(Set<Post> likedPosts) {
-        this.likedPosts = likedPosts;
-    }
-
-    public Set<Post> getDislikedPosts() {
-        return this.dislikedPosts;
-    }
-
-    public void setDislikedPosts(Set<Post> dislikedPosts) {
-        this.likedPosts = dislikedPosts;
-    }
 }
Index: springapp/src/main/java/mk/profesori/springapp/Model/Post.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/Post.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Model/Post.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -3,4 +3,5 @@
 import java.time.LocalDateTime;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -55,15 +56,20 @@
 
     @ManyToOne
-    @JoinColumn(name = "parent_post_id", nullable = true)
+    @JoinColumn(name = "parent_post_id")
     private Post parent;
+
+    @OneToMany(mappedBy = "post")
+    private Set<PostVote> votes = new HashSet<>();
+
+    public Set<PostVote> getVotes() {
+        return votes;
+    }
+
+    public void setVotes(Set<PostVote> votes) {
+        this.votes = votes;
+    }
 
     @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
     private List<Post> children = new ArrayList<>();
-
-    @ManyToMany(mappedBy = "likedPosts")
-    Set<CustomUserDetails> likes;
-
-    @ManyToMany(mappedBy = "dislikedPosts")
-    Set<CustomUserDetails> dislikes;
 
     // getters and setters
@@ -132,20 +138,4 @@
     }
 
-    public Set<CustomUserDetails> getLikes() {
-        return likes;
-    }
-
-    public void setLikes(Set<CustomUserDetails> likes) {
-        this.likes = likes;
-    }
-
-    public Set<CustomUserDetails> getDislikes() {
-        return dislikes;
-    }
-
-    public void setDislikes(Set<CustomUserDetails> dislikes) {
-        this.dislikes = dislikes;
-    }
-
     // konstruktor so parent (koga e reply)
     public Post(String title, String content, CustomUserDetails author, LocalDateTime timePosted,
@@ -174,3 +164,8 @@
     }
 
+    @Override
+    public String toString() {
+        return this.postId.toString();
+    }
+
 }
Index: springapp/src/main/java/mk/profesori/springapp/Model/PostVote.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/PostVote.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
+++ springapp/src/main/java/mk/profesori/springapp/Model/PostVote.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -0,0 +1,76 @@
+package mk.profesori.springapp.Model;
+
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@Entity
+@NoArgsConstructor
+public class PostVote {
+
+    @Id
+    @SequenceGenerator(name = "post_vote_sequence", sequenceName = "post_vote_sequence", allocationSize = 1)
+    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_vote_sequence")
+    private Long postVoteId;
+
+    @ManyToOne
+    @JoinColumn(name = "custom_user_details_id")
+    private CustomUserDetails user;
+
+    @ManyToOne
+    @JoinColumn(name = "post_id")
+    private Post post;
+
+    LocalDateTime time;
+
+    @Enumerated(EnumType.STRING)
+    private VoteType vote;
+
+    public PostVote(CustomUserDetails user, Post post, VoteType vote) {
+        this.user = user;
+        this.post = post;
+        this.time = LocalDateTime.now();
+        this.vote = vote;
+    }
+
+    public Long getPostVoteId() {
+        return postVoteId;
+    }
+
+    public void setPostVoteId(Long id) {
+        this.postVoteId = id;
+    }
+
+    public CustomUserDetails getUser() {
+        return user;
+    }
+
+    public void setUser(CustomUserDetails user) {
+        this.user = user;
+    }
+
+    public Post getPost() {
+        return post;
+    }
+
+    public void setPost(Post post) {
+        this.post = post;
+    }
+
+    public LocalDateTime getTime() {
+        return time;
+    }
+
+    public void setTime(LocalDateTime time) {
+        this.time = time;
+    }
+
+    public VoteType getVote() {
+        return vote;
+    }
+
+    public void setVote(VoteType vote) {
+        this.vote = vote;
+    }
+}
Index: ringapp/src/main/java/mk/profesori/springapp/Model/Section.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/Section.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ 	(revision )
@@ -1,69 +1,0 @@
-package mk.profesori.springapp.Model;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import javax.persistence.CascadeType;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.GeneratedValue;
-import javax.persistence.GenerationType;
-import javax.persistence.Id;
-import javax.persistence.JoinColumn;
-import javax.persistence.OneToMany;
-import javax.persistence.OneToOne;
-
-import com.fasterxml.jackson.annotation.JsonIdentityInfo;
-import com.fasterxml.jackson.annotation.ObjectIdGenerators;
-
-@Entity
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "sectionId")
-public class Section {
-
-    @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    @Column(name = "id")
-    private Long sectionId;
-
-    @Column(name = "name")
-    private String name;
-
-    @OneToMany(mappedBy = "parentSection", cascade = CascadeType.ALL)
-    private List<_Thread> threads = new ArrayList<>();
-
-    @OneToOne
-    @JoinColumn(name = "study_programme_id", referencedColumnName = "id")
-    private StudyProgramme relatedStudyProgramme;
-
-    // getters and setters
-    public Long getSectionId() {
-        return sectionId;
-    }
-
-    public void setSectionId(Long sectionId) {
-        this.sectionId = sectionId;
-    }
-
-    public List<_Thread> getThreads() {
-        return threads;
-    }
-
-    public void setThreads(List<_Thread> threads) {
-        this.threads = threads;
-    }
-
-    public StudyProgramme getRelatedStudyProgramme() {
-        return relatedStudyProgramme;
-    }
-
-    public void setRelatedStudyProgramme(StudyProgramme relatedStudyProgramme) {
-        this.relatedStudyProgramme = relatedStudyProgramme;
-    }
-
-    public Section(Long sectionId, List<_Thread> threads, StudyProgramme relatedStudyProgramme) {
-        this.sectionId = sectionId;
-        this.threads = threads;
-        this.relatedStudyProgramme = relatedStudyProgramme;
-    }
-
-}
Index: springapp/src/main/java/mk/profesori/springapp/Model/StudyProgramme.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/StudyProgramme.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Model/StudyProgramme.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -12,5 +12,4 @@
 import javax.persistence.ManyToOne;
 import javax.persistence.OneToMany;
-import javax.persistence.OneToOne;
 import javax.persistence.Table;
 
@@ -41,7 +40,4 @@
     private Set<Subject> subjects = new HashSet<>();
 
-    @OneToOne(mappedBy = "relatedStudyProgramme")
-    private Section relatedSection;
-
     // getters
     public Long getStudyProgrammeId() {
@@ -65,6 +61,3 @@
     }
 
-    public Section getRelatedSection() {
-        return relatedSection;
-    }
 }
Index: springapp/src/main/java/mk/profesori/springapp/Model/Subject.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/Subject.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Model/Subject.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -15,9 +15,9 @@
 
 import com.fasterxml.jackson.annotation.JsonIdentityInfo;
-import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+import com.voodoodyne.jackson.jsog.JSOGGenerator;
 
 @Entity
 @Table(name = "subject")
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "subjectId")
+@JsonIdentityInfo(generator = JSOGGenerator.class)
 public class Subject {
 
Index: springapp/src/main/java/mk/profesori/springapp/Model/University.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/University.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Model/University.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -15,9 +15,9 @@
 
 import com.fasterxml.jackson.annotation.JsonIdentityInfo;
-import com.fasterxml.jackson.annotation.ObjectIdGenerators;
+import com.voodoodyne.jackson.jsog.JSOGGenerator;
 
 @Entity
 @Table(name = "university")
-@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "universityId")
+@JsonIdentityInfo(generator = JSOGGenerator.class)
 public class University {
 
Index: springapp/src/main/java/mk/profesori/springapp/Model/VoteType.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/VoteType.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
+++ springapp/src/main/java/mk/profesori/springapp/Model/VoteType.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -0,0 +1,6 @@
+package mk.profesori.springapp.Model;
+
+public enum VoteType {
+    UPVOTE,
+    DOWNVOTE
+}
Index: springapp/src/main/java/mk/profesori/springapp/Model/_Thread.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Model/_Thread.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Model/_Thread.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -2,25 +2,19 @@
 
 import java.time.LocalDateTime;
-import java.util.ArrayList;
+
 import java.util.List;
 
-import javax.persistence.Column;
 import javax.persistence.DiscriminatorValue;
-import javax.persistence.ElementCollection;
+
 import javax.persistence.Entity;
 import javax.persistence.JoinColumn;
 import javax.persistence.ManyToOne;
 
+import lombok.NoArgsConstructor;
+
 @Entity
 @DiscriminatorValue("thread")
+@NoArgsConstructor
 public class _Thread extends Post {
-
-    @Column(name = "tags") // unused
-    @ElementCollection
-    private List<String> tags = new ArrayList<>();
-
-    @ManyToOne
-    @JoinColumn(name = "section_id")
-    private Section parentSection;
 
     @ManyToOne
@@ -31,7 +25,6 @@
     public _Thread(String title, String content, CustomUserDetails author, LocalDateTime timePosted,
             LocalDateTime timeLastEdited,
-            Post parent, List<Post> children, Section parentSection, Subject targetSubject) {
+            Post parent, List<Post> children, Subject targetSubject) {
         super(title, content, author, timePosted, timeLastEdited, parent, children);
-        this.parentSection = parentSection;
         this.targetSubject = targetSubject;
     }
@@ -39,17 +32,7 @@
     // konstruktor bez parent (koga NE e reply)
     public _Thread(String title, String content, CustomUserDetails author, LocalDateTime timePosted,
-            LocalDateTime timeLastEdited, List<Post> children, Section parentSection, Subject targetSubject) {
+            LocalDateTime timeLastEdited, List<Post> children, Subject targetSubject) {
         super(title, content, author, timePosted, timeLastEdited, children);
-        this.parentSection = parentSection;
         this.targetSubject = targetSubject;
-    }
-
-    // getters
-    public List<String> getTags() {
-        return tags;
-    }
-
-    public Section getParentSection() {
-        return parentSection;
     }
 
Index: springapp/src/main/java/mk/profesori/springapp/Repository/OpinionRepository.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Repository/OpinionRepository.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Repository/OpinionRepository.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -1,11 +1,10 @@
 package mk.profesori.springapp.Repository;
 
+import mk.profesori.springapp.Model.Opinion;
 import org.springframework.data.repository.CrudRepository;
 import org.springframework.stereotype.Repository;
 
-import mk.profesori.springapp.Model.Opinion;
-
 @Repository
-public interface OpinionRepository extends CrudRepository<Opinion, Long> {
-    public Opinion findByPostId(Long id);
+public interface OpinionRepository extends CrudRepository<Opinion,Long> {
+    Opinion findByPostId(Long postId);
 }
Index: springapp/src/main/java/mk/profesori/springapp/Repository/PostVoteRepository.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Repository/PostVoteRepository.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
+++ springapp/src/main/java/mk/profesori/springapp/Repository/PostVoteRepository.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -0,0 +1,10 @@
+package mk.profesori.springapp.Repository;
+
+import mk.profesori.springapp.Model.PostVote;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface PostVoteRepository extends CrudRepository<PostVote,Long> {
+    PostVote findByPostVoteId(Long id);
+}
Index: springapp/src/main/java/mk/profesori/springapp/Repository/SubjectRepository.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Repository/SubjectRepository.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
+++ springapp/src/main/java/mk/profesori/springapp/Repository/SubjectRepository.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -0,0 +1,11 @@
+package mk.profesori.springapp.Repository;
+
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import mk.profesori.springapp.Model.Subject;
+
+@Repository
+public interface SubjectRepository extends CrudRepository<Subject, Long> {
+    public Subject findBySubjectId(Long id);
+}
Index: springapp/src/main/java/mk/profesori/springapp/Repository/_ThreadRepository.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Repository/_ThreadRepository.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
+++ springapp/src/main/java/mk/profesori/springapp/Repository/_ThreadRepository.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -0,0 +1,10 @@
+package mk.profesori.springapp.Repository;
+
+import mk.profesori.springapp.Model._Thread;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface _ThreadRepository extends CrudRepository<_Thread,Long> {
+    _Thread findByPostId(Long postId);
+}
Index: springapp/src/main/java/mk/profesori/springapp/Security/SecurityConfiguration.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Security/SecurityConfiguration.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Security/SecurityConfiguration.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -37,5 +37,5 @@
             @Override
             public void addCorsMappings(CorsRegistry registry) {
-                registry.addMapping("/**").allowedOrigins("http://192.168.0.17:3000", "http://192.168.0.28:3000")
+                registry.addMapping("/**").allowedOrigins("http://192.168.0.17:3000", "http://192.168.0.39:3000")
                         .allowCredentials(true);
             }
Index: springapp/src/main/java/mk/profesori/springapp/Service/EmailValidator.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Service/EmailValidator.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Service/EmailValidator.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -13,5 +13,5 @@
     public boolean test(String email) {
         
-        String regex = "^(.+)@(.+)$";
+        String regex = "^(.+)@(.+)$"; //TODO bilosto@@ ->> illegal address exception od mailsender
 
         Pattern pattern = Pattern.compile(regex);
Index: springapp/src/main/java/mk/profesori/springapp/Service/MainService.java
===================================================================
--- springapp/src/main/java/mk/profesori/springapp/Service/MainService.java	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/java/mk/profesori/springapp/Service/MainService.java	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -1,48 +1,41 @@
 package mk.profesori.springapp.Service;
+
+import mk.profesori.springapp.Model.*;
+import mk.profesori.springapp.Repository.*;
+import org.springframework.stereotype.Service;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import mk.profesori.springapp.Repository.CityRepository;
-import mk.profesori.springapp.Repository.FacultyRepository;
-import mk.profesori.springapp.Repository.OpinionRepository;
-import mk.profesori.springapp.Repository.ProfessorRepository;
-import mk.profesori.springapp.Repository.StudyProgrammeRepository;
-import mk.profesori.springapp.Repository.UniversityRepository;
-import mk.profesori.springapp.Repository.UserRepository;
-import mk.profesori.springapp.Model.City;
-import mk.profesori.springapp.Model.CustomUserDetails;
-import mk.profesori.springapp.Model.Faculty;
-import mk.profesori.springapp.Model.Opinion;
-import mk.profesori.springapp.Model.Professor;
-import mk.profesori.springapp.Model.StudyProgramme;
-import mk.profesori.springapp.Model.University;
-
 @Service
 public class MainService {
 
-    @Autowired
-    private ProfessorRepository professorRepository;
-    @Autowired
-    private StudyProgrammeRepository studyProgrammeRepository;
-    @Autowired
-    private FacultyRepository facultyRepository;
-    @Autowired
-    private UniversityRepository universityRepository;
-    @Autowired
-    private CityRepository cityRepository;
-    @Autowired
-    private OpinionRepository opinionRepository;
-    @Autowired
-    private UserRepository userRepository;
+    private final ProfessorRepository professorRepository;
+    private final StudyProgrammeRepository studyProgrammeRepository;
+    private final FacultyRepository facultyRepository;
+    private final UniversityRepository universityRepository;
+    private final CityRepository cityRepository;
+    private final OpinionRepository opinionRepository;
+    private final _ThreadRepository _threadRepository;
+    private final SubjectRepository subjectRepository;
+    private final PostVoteRepository postVoteRepository;
+    private final UserRepository userRepository;
+
+    public MainService(ProfessorRepository professorRepository, StudyProgrammeRepository studyProgrammeRepository, FacultyRepository facultyRepository, UniversityRepository universityRepository, CityRepository cityRepository, OpinionRepository opinionRepository, _ThreadRepository _threadRepository, SubjectRepository subjectRepository, PostVoteRepository postVoteRepository, UserRepository userRepository) {
+        this.professorRepository = professorRepository;
+        this.studyProgrammeRepository = studyProgrammeRepository;
+        this.facultyRepository = facultyRepository;
+        this.universityRepository = universityRepository;
+        this.cityRepository = cityRepository;
+        this.opinionRepository = opinionRepository;
+        this._threadRepository = _threadRepository;
+        this.subjectRepository = subjectRepository;
+        this.postVoteRepository = postVoteRepository;
+        this.userRepository = userRepository;
+    }
 
     public List<Professor> getAllProfessors() {
 
-        List<Professor> list = new ArrayList<>();
-        professorRepository.findAll().forEach(list::add);
-        return list;
+        return new ArrayList<>(professorRepository.findAll());
     }
 
@@ -56,20 +49,14 @@
         Faculty faculty = facultyRepository.findByFacultyId(facultyId);
 
-        List<Professor> list = new ArrayList<>();
-        professorRepository.findByFaculty(faculty).forEach(list::add);
-        return list;
+        return new ArrayList<>(professorRepository.findByFaculty(faculty));
     }
 
     public List<Professor> getProfessorsByNameContains(String contained) {
-        List<Professor> list = new ArrayList<>();
-        professorRepository.findByProfessorNameContainingIgnoreCase(contained).forEach(list::add);
-        return list;
+        return new ArrayList<>(professorRepository.findByProfessorNameContainingIgnoreCase(contained));
     }
 
     public List<StudyProgramme> getAllStudyProgrammes() {
 
-        List<StudyProgramme> list = new ArrayList<>();
-        studyProgrammeRepository.findAll().forEach(list::add);
-        return list;
+        return new ArrayList<>(studyProgrammeRepository.findAll());
     }
 
@@ -83,13 +70,9 @@
         Faculty faculty = facultyRepository.findByFacultyId(facultyId);
 
-        List<StudyProgramme> list = new ArrayList<>();
-        studyProgrammeRepository.findByFaculty(faculty).forEach(list::add);
-        return list;
+        return new ArrayList<>(studyProgrammeRepository.findByFaculty(faculty));
     }
 
     public List<Faculty> getAllFaculties() {
-        List<Faculty> list = new ArrayList<>();
-        facultyRepository.findAll().forEach(list::add);
-        return list;
+        return new ArrayList<>(facultyRepository.findAll());
     }
 
@@ -102,13 +85,9 @@
         University university = universityRepository.findByUniversityId(universityId);
 
-        List<Faculty> list = new ArrayList<>();
-        facultyRepository.findByUniversity(university).forEach(list::add);
-        return list;
+        return new ArrayList<>(facultyRepository.findByUniversity(university));
     }
 
     public List<University> getAllUniversities() {
-        List<University> list = new ArrayList<>();
-        universityRepository.findAll().forEach(list::add);
-        return list;
+        return new ArrayList<>(universityRepository.findAll());
     }
 
@@ -121,13 +100,9 @@
         City city = cityRepository.findByCityId(cityId);
 
-        List<University> list = new ArrayList<>();
-        universityRepository.findByCity(city).forEach(list::add);
-        return list;
+        return new ArrayList<>(universityRepository.findByCity(city));
     }
 
     public List<City> getAllCities() {
-        List<City> list = new ArrayList<>();
-        cityRepository.findAll().forEach(list::add);
-        return list;
+        return new ArrayList<>(cityRepository.findAll());
     }
 
@@ -136,9 +111,9 @@
     }
 
-    public void addOpinion(String title, String content, Long professorId, CustomUserDetails currentUser) {
+    public void addOpinion(String content, Long professorId, CustomUserDetails currentUser) {
 
         Professor targetProfessor = professorRepository.findByProfessorId(professorId);
 
-        Opinion opinionToAdd = new Opinion(title, content, currentUser, null, null,
+        Opinion opinionToAdd = new Opinion(null, content, currentUser, null, null,
                 null, targetProfessor);
 
@@ -155,38 +130,65 @@
         opinionRepository.save(opinionToAdd);
 
+        //mozda ne treba
         targetOpinion.getChildren().add(opinionToAdd);
         opinionRepository.save(targetOpinion);
     }
 
-    public void upvoteOpinion(Long postId, CustomUserDetails currentUser) {
-        Opinion targetOpinion = opinionRepository.findByPostId(postId);
+    public void addThread(String title, String content, Long subjectId, CustomUserDetails currentUser) {
+        Subject targetSubject = subjectRepository.findBySubjectId(subjectId);
 
-        if (!targetOpinion.getLikes().contains(currentUser)) {
-
-            targetOpinion.getLikes().add(currentUser);
-            // opinionRepository.save(targetOpinion);
-
-            targetOpinion.getAuthor().setKarma(targetOpinion.getAuthor().getKarma() + 1);
-            userRepository.save(targetOpinion.getAuthor());
-
-            currentUser.getLikedPosts().add(targetOpinion);
-            userRepository.save(currentUser);
-        }
+        _Thread _threadToAdd = new _Thread(title, content, currentUser, null, null, null, targetSubject);
+        _threadRepository.save(_threadToAdd);
     }
 
+    public void replyToThread(String content, Long subjectId, Long postId, CustomUserDetails currentUser) {
+        Subject targetSubject = subjectRepository.findBySubjectId(subjectId);
+        _Thread target_Thread = _threadRepository.findByPostId(postId);
+
+        _Thread _threadToAdd = new _Thread(null, content, currentUser, null, null, target_Thread, null, targetSubject);
+        _threadRepository.save(_threadToAdd);
+
+        //mozda ne treba
+        target_Thread.getChildren().add(_threadToAdd);
+        _threadRepository.save(target_Thread);
+    }
+
+    public Subject getSubjectById(Long subjectId) {
+        return subjectRepository.findBySubjectId(subjectId);
+    }
+
+    public _Thread get_ThreadById(Long postId) {
+        return _threadRepository.findByPostId(postId);
+    }
+
+    public void upvoteOpinion(Long postId, CustomUserDetails currentUser) {
+        Post targetPost = opinionRepository.findByPostId(postId);
+        PostVote voteToAdd = new PostVote(currentUser, targetPost, VoteType.UPVOTE);
+        postVoteRepository.save(voteToAdd);
+        targetPost.getAuthor().setKarma(targetPost.getAuthor().getKarma()+1);
+        userRepository.save(targetPost.getAuthor());
+    }
     public void downvoteOpinion(Long postId, CustomUserDetails currentUser) {
-        Opinion targetOpinion = opinionRepository.findByPostId(postId);
+        Post targetPost = opinionRepository.findByPostId(postId);
+        PostVote voteToAdd = new PostVote(currentUser, targetPost, VoteType.DOWNVOTE);
+        postVoteRepository.save(voteToAdd);
+        targetPost.getAuthor().setKarma(targetPost.getAuthor().getKarma()-1);
+        userRepository.save(targetPost.getAuthor());
+    }
 
-        if (!targetOpinion.getDislikes().contains(currentUser)) {
+    public void upvote_Thread(Long postId, CustomUserDetails currentUser) {
+        Post targetPost = _threadRepository.findByPostId(postId);
+        PostVote voteToAdd = new PostVote(currentUser, targetPost, VoteType.UPVOTE);
+        postVoteRepository.save(voteToAdd);
+        targetPost.getAuthor().setKarma(targetPost.getAuthor().getKarma()+1);
+        userRepository.save(targetPost.getAuthor());
 
-            targetOpinion.getDislikes().add(currentUser);
-            // opinionRepository.save(targetOpinion);
-
-            targetOpinion.getAuthor().setKarma(targetOpinion.getAuthor().getKarma() - 1);
-            userRepository.save(targetOpinion.getAuthor());
-
-            currentUser.getDislikedPosts().add(targetOpinion);
-            userRepository.save(currentUser);
-        }
+    }
+    public void downvote_Thread(Long postId, CustomUserDetails currentUser) {
+        Post targetPost = _threadRepository.findByPostId(postId);
+        PostVote voteToAdd = new PostVote(currentUser, targetPost, VoteType.DOWNVOTE);
+        postVoteRepository.save(voteToAdd);
+        targetPost.getAuthor().setKarma(targetPost.getAuthor().getKarma()-1);
+        userRepository.save(targetPost.getAuthor());
     }
 }
Index: springapp/src/main/resources/application.properties
===================================================================
--- springapp/src/main/resources/application.properties	(revision 8d831804abcee99d72c207c57a8f16e54b1329a2)
+++ springapp/src/main/resources/application.properties	(revision c68150fac8ab8e6f5dc5b8b2a9cd73d391dc1aa7)
@@ -4,9 +4,9 @@
 spring.datasource.password=1win7337
 spring.jpa.hibernate.ddl-auto=update
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.show-sql=false
-spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
 spring.jpa.properties.hibernate.format_sql=true
 server.address=192.168.0.17
-spring.mail.host=192.168.0.28
+spring.mail.host=192.168.0.39
 spring.mail.username=mailuser
 spring.mail.password=mailpass
