@@ -65,11 +81,7 @@ const Editor = forwardRef((props: TEditor) => {
value={value}
ref={editor}
modules={modules}
-
-
onChange={changeHandler}
-
-
theme="snow"
placeholder="Type your thoughts here..."
/>
diff --git a/enshi/src/Components/NavBar/NavBar.tsx b/enshi/src/Components/NavBar/NavBar.tsx
index bd60301..17472c6 100644
--- a/enshi/src/Components/NavBar/NavBar.tsx
+++ b/enshi/src/Components/NavBar/NavBar.tsx
@@ -1,20 +1,15 @@
-import {
- Container,
-} from "@radix-ui/themes";
-import SearchField from "./SearchField/SearchField";
-import UserButton from "./UserButton/UserButton";
import CustomNavigationMenu from "./NavigationMenu/NavigationMenu";
+import RightButtonBar from "./RightButtonBar/RightButtonBar";
+import SearchField from "./SearchField/SearchField";
export default function NavBar() {
return (
-
-
-
+
+
-
+
-
-
-
+
+
);
-}
\ No newline at end of file
+}
diff --git a/enshi/src/Components/NavBar/NavigationMenu/NavigationMenu.tsx b/enshi/src/Components/NavBar/NavigationMenu/NavigationMenu.tsx
index e82d496..56e8b9a 100644
--- a/enshi/src/Components/NavBar/NavigationMenu/NavigationMenu.tsx
+++ b/enshi/src/Components/NavBar/NavigationMenu/NavigationMenu.tsx
@@ -1,15 +1,19 @@
import * as NavigationMenu from "@radix-ui/react-navigation-menu";
-import { useThemeContext, Button, Heading } from "@radix-ui/themes";
-import { useNavigate, useLocation } from "react-router-dom";
+import { Button, Heading, useThemeContext } from "@radix-ui/themes";
+import { useTranslation } from "react-i18next";
+import { useLocation, useNavigate } from "react-router-dom";
export default function CustomNavigationMenu() {
+
+ const {t} = useTranslation()
+
return (
-
+
-
+
diff --git a/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx b/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx
new file mode 100644
index 0000000..eaaaaa6
--- /dev/null
+++ b/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx
@@ -0,0 +1,17 @@
+import { PlusIcon } from "@radix-ui/react-icons";
+import { Button, Text } from "@radix-ui/themes";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
+
+export default function CreatePostButton() {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t("createPost")}
+
+
+ );
+}
diff --git a/enshi/src/Components/NavBar/RightButtonBar/RightButtonBar.tsx b/enshi/src/Components/NavBar/RightButtonBar/RightButtonBar.tsx
new file mode 100644
index 0000000..3c1ff38
--- /dev/null
+++ b/enshi/src/Components/NavBar/RightButtonBar/RightButtonBar.tsx
@@ -0,0 +1,12 @@
+import CreatePostButton from "./CreatePostButton/CreatePostButton";
+import UserButton from "./UserButton/UserButton";
+
+
+export default function RightButtonBar() {
+ return (
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/enshi/src/Components/NavBar/UserButton/UserButton.tsx b/enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx
similarity index 66%
rename from enshi/src/Components/NavBar/UserButton/UserButton.tsx
rename to enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx
index c9c1868..ce9ca61 100644
--- a/enshi/src/Components/NavBar/UserButton/UserButton.tsx
+++ b/enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx
@@ -7,14 +7,16 @@ import {
import { DropdownMenu, Flex, IconButton, Text } from "@radix-ui/themes";
import { Icon } from "@radix-ui/themes/dist/esm/components/callout.js";
import { useAtomValue } from "jotai";
-import { userAtom } from "../../../AtomStore/AtomStore";
+import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
+import { userAtom } from "../../../../AtomStore/AtomStore";
export default function UserButton() {
const user = useAtomValue(userAtom);
+ const { t } = useTranslation();
return (
-
+
@@ -24,24 +26,26 @@ export default function UserButton() {
-
+
- Profile
+ {t("profile")}
-
-
-
-
- Your blogs
-
+
+
+
+
+
+ {t("yourBlogs")}
+
+
@@ -52,15 +56,17 @@ export default function UserButton() {
- Log out
+ {t("signOut")}
) : (
-
-
-
-
- Log in
-
+
+
+
+
+
+ {t("signIn")}
+
+
)}
diff --git a/enshi/src/Components/NavBar/SearchField/SearchField.tsx b/enshi/src/Components/NavBar/SearchField/SearchField.tsx
index 74cf6fb..02dbf95 100644
--- a/enshi/src/Components/NavBar/SearchField/SearchField.tsx
+++ b/enshi/src/Components/NavBar/SearchField/SearchField.tsx
@@ -6,9 +6,9 @@ export default function SearchField() {
const {t} = useTranslation()
return (
-
+
diff --git a/enshi/src/Components/ToastProvider/ToastProvider.tsx b/enshi/src/Components/ToastProvider/ToastProvider.tsx
new file mode 100644
index 0000000..51ec9a7
--- /dev/null
+++ b/enshi/src/Components/ToastProvider/ToastProvider.tsx
@@ -0,0 +1,44 @@
+import { Cross1Icon } from "@radix-ui/react-icons";
+import * as Toast from "@radix-ui/react-toast";
+import { Card, Text } from "@radix-ui/themes";
+import { useAtomValue } from "jotai";
+import React from "react";
+import { toastAtom } from "../../AtomStore/AtomStore";
+
+export default function ToastProvider(props: React.PropsWithChildren) {
+ const toastsToRender = useAtomValue(toastAtom);
+
+ return (
+
+ {props.children}
+
+ {toastsToRender.map((toast) => {
+ return (
+
+
+
+
+ {toast.title}
+
+
+
+ {toast.description}
+
+
+
+
+
+
+ );
+ })}
+
+
+
+ );
+}
diff --git a/enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx b/enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx
new file mode 100644
index 0000000..05091f5
--- /dev/null
+++ b/enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx
@@ -0,0 +1,33 @@
+import { Skeleton, Text } from "@radix-ui/themes";
+import { useQuery } from "@tanstack/react-query";
+import { Link } from "react-router-dom";
+import { axiosLocalhost } from "../../api/axios/axios";
+
+type TUserNicknameLink = {
+ userId: string;
+};
+
+export default function UserNicknameLink(props: TUserNicknameLink) {
+ const { data, isPending } = useQuery({
+ queryKey: [`userLink${props.userId}`],
+ queryFn: async () => {
+ const response = await axiosLocalhost.get(
+ `/user/${props.userId || 0}`
+ );
+ return response.data as string;
+ },
+ });
+
+ if (isPending)
+ return (
+
+ @Nickname
+
+ );
+
+ return (
+
+ @{data}
+
+ );
+}
diff --git a/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx b/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx
new file mode 100644
index 0000000..3418d83
--- /dev/null
+++ b/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx
@@ -0,0 +1,22 @@
+import { Container, Text } from "@radix-ui/themes";
+import { t } from "i18next";
+import { useAtomValue } from "jotai";
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { userAtom } from "../../AtomStore/AtomStore";
+
+export default function AuthPageWrapper(props: React.PropsWithChildren) {
+ const user = useAtomValue(userAtom);
+ const navigate = useNavigate();
+
+ if (!user) {
+ navigate("/login");
+ return (
+
+ {t("errors.unauthorized")}
+
+ );
+ }
+
+ return props.children;
+}
diff --git a/enshi/src/Pages/BlogPage/BlogPage.tsx b/enshi/src/Pages/BlogPage/BlogPage.tsx
new file mode 100644
index 0000000..fc1ae4a
--- /dev/null
+++ b/enshi/src/Pages/BlogPage/BlogPage.tsx
@@ -0,0 +1,13 @@
+import { Box } from '@radix-ui/themes'
+import { useParams } from 'react-router-dom'
+
+
+export default function BlogPage() {
+ const queryParams = useParams()
+
+ return (
+
+
+
+ )
+}
diff --git a/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx
new file mode 100644
index 0000000..2b1e3db
--- /dev/null
+++ b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx
@@ -0,0 +1,182 @@
+import * as Form from "@radix-ui/react-form";
+import { CrossCircledIcon } from "@radix-ui/react-icons";
+import { Button, Card, Heading, Text, TextField } from "@radix-ui/themes";
+import { useMutation } from "@tanstack/react-query";
+import { t } from "i18next";
+import { useAtom } from "jotai";
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { axiosLocalhost } from "../../../api/axios/axios";
+import { userAtom } from "../../../AtomStore/AtomStore";
+import UseCapsLock from "../../../hooks/useCapsLock";
+import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton";
+
+type TLoginData = {
+ username: string;
+ password: string;
+};
+
+export default function LoginPage() {
+ const [userAtomValue, setUserAtom] = useAtom(userAtom);
+ const [showPassword, setShowPassword] = useState(false);
+ const { isCapsLockOn } = UseCapsLock();
+ const [isError, setIsError] = useState(false);
+
+ const navigate = useNavigate();
+
+ const logInMutation = useMutation({
+ mutationFn: async (data: TLoginData) => {
+ let response = await axiosLocalhost.post(
+ "/login",
+ JSON.stringify(data)
+ );
+ setUserAtom({
+ username: response.data.username,
+ isAdmin: false,
+ id: response.data.id,
+ });
+ },
+
+ onError: (error, _variables, _context) => {
+ console.log(error);
+ setIsError(true);
+ },
+
+ onSuccess: () => {
+ let isAdminFunc = async () => {
+ let response = await axiosLocalhost.get("/admin/check");
+ if (response.status === 200) {
+ setUserAtom({
+ username: userAtomValue?.username || "",
+ isAdmin: true,
+ id: userAtomValue?.id,
+ });
+ }
+ };
+
+ isAdminFunc();
+
+ navigate("/");
+ },
+ });
+
+ return (
+
+
+ {t("loginForm")}
+
+ {
+ e.preventDefault();
+ let formData = new FormData(
+ document.querySelector("form") as HTMLFormElement
+ );
+
+ let loginData: TLoginData = {
+ password: (formData.get("password") as string) || "",
+ username: (formData.get("username") as string) || "",
+ };
+
+ logInMutation.mutate(loginData);
+ }}
+ >
+
+
+
+ {t("username")}
+
+
+ {t("errors.enterUsername")}
+
+
+
+
+
+ {(validity) => (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {t("password")}
+
+
+ {t("errors.enterPassword")}
+
+
+
+
+
+ {(validity) => (
+
+
+
+ )}
+
+
+
+
+ {t("capsLogWarning")}
+
+
+
+
+ {t("errors.invalidLoginData")}
+
+
+
+
+ {t("submit")}
+
+
+
+
+ {t("suggestRegister")}{" "}
+
+ {t("register")}
+ {" "}
+ {t("now")}
+
+
+
+ );
+}
diff --git a/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx b/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx
new file mode 100644
index 0000000..50d1e9b
--- /dev/null
+++ b/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx
@@ -0,0 +1,76 @@
+import { Box, Container, Flex, Spinner } from "@radix-ui/themes";
+import { useQuery } from "@tanstack/react-query";
+import { useState } from "react";
+import { useParams } from "react-router-dom";
+import { axiosLocalhost } from "../../../api/axios/axios";
+import Editor from "../../../Components/Editor/Editor";
+import SubmitChangesButton from "./SubmitChangesButton/SubmitChangesButton";
+
+export default function PostRedactor() {
+ const [contentValue, setContentValue] = useState("");
+ const [titleValue, setTitleValue] = useState("");
+
+ const queryParams = useParams();
+
+ const { isPending } = useQuery({
+ queryKey: ["changePostKey", queryParams.postId],
+ queryFn: async () => {
+ try {
+ const response = await axiosLocalhost.get(
+ `/posts/${queryParams.postId}`
+ );
+
+ setTitleValue(response.data["title"]);
+ setContentValue(response.data["content"]);
+
+ return response.data;
+ } catch (error) {
+ console.log(error);
+
+ return error;
+ }
+ },
+ gcTime: 0,
+ refetchOnMount: true
+ });
+
+ return (
+ <>
+
+
+
+ {
+ setTitleValue(e.target.value);
+ }}
+ value={titleValue}
+ />
+
+
+
+ {isPending ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx b/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx
new file mode 100644
index 0000000..070448d
--- /dev/null
+++ b/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx
@@ -0,0 +1,60 @@
+import { Button } from "@radix-ui/themes";
+import { useMutation } from "@tanstack/react-query";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate, useParams } from "react-router-dom";
+import { axiosLocalhost } from "../../../../api/axios/axios";
+import useToast from "../../../../hooks/useToast";
+
+type TSubmitChangesButton = {
+ className: string;
+ titleValue: string;
+ contentValue: string;
+};
+
+export default function SubmitChangesButton(props: TSubmitChangesButton) {
+ const { t } = useTranslation();
+
+ const [isDisabled, setIsDisabled] = useState(false);
+ const createToast = useToast();
+
+ const navigate = useNavigate();
+ const queryParams = useParams();
+
+ const postMutation = useMutation({
+ mutationFn: async () => {
+ if (!props.titleValue) throw new Error("no title provided");
+ if (!props.contentValue || props.contentValue === "
")
+ throw new Error("no content provided");
+
+ axiosLocalhost.put(`/posts/${queryParams["postId"]}`, {
+ title: props.titleValue,
+ content: props.contentValue,
+ });
+ },
+ onMutate: () => {
+ setIsDisabled(true);
+ },
+ onError: () => {
+ setIsDisabled(false);
+ },
+ onSuccess: () => {
+ createToast({title: "Post has been changed!"})
+ navigate("/");
+ },
+ });
+
+ return (
+ {
+ postMutation.mutate();
+ }}
+ className={props.className}
+ variant="soft"
+ size={"4"}
+ disabled={isDisabled}
+ >
+ {t("updatePost")}
+
+ );
+}
diff --git a/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx
new file mode 100644
index 0000000..4702221
--- /dev/null
+++ b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx
@@ -0,0 +1,265 @@
+import * as Form from "@radix-ui/react-form";
+import { CrossCircledIcon } from "@radix-ui/react-icons";
+import { Button, Card, Heading, Text, TextField } from "@radix-ui/themes";
+import { useMutation } from "@tanstack/react-query";
+import { useSetAtom } from "jotai";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Link, useNavigate } from "react-router-dom";
+import { axiosLocalhost } from "../../../api/axios/axios";
+import { userAtom } from "../../../AtomStore/AtomStore";
+import UseCapsLock from "../../../hooks/useCapsLock";
+import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton";
+
+type TRegisterData = {
+ username: string;
+ password: string;
+ email: string;
+};
+
+export default function RegisterPage() {
+ const setUserAtom = useSetAtom(userAtom)
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfPassword, setShowConfPassword] = useState(false);
+ const { isCapsLockOn } = UseCapsLock();
+
+ const { t } = useTranslation();
+
+ const [isError, setIsError] = useState(false);
+
+ const navigate = useNavigate();
+
+ const registerMutation = useMutation({
+ mutationFn: async (data: TRegisterData) => {
+ let response = await axiosLocalhost.post("/users", JSON.stringify(data));
+ setUserAtom({
+ username: response.data.username,
+ isAdmin: false,
+ id: response.data.id,
+ })
+ },
+
+ onError: (error, _variables, _context) => {
+ console.log(error);
+ setIsError(true);
+ },
+
+ onSuccess: () => {
+ navigate("/");
+ },
+ });
+
+ return (
+
+
+ {t("registerForm")}
+
+ {
+ e.preventDefault();
+ let formData = new FormData(
+ document.querySelector("form") as HTMLFormElement
+ );
+
+ let registerData: TRegisterData = {
+ password: (formData.get("password") as string) || "",
+ username: (formData.get("username") as string) || "",
+ email: (formData.get("email") as string) || "",
+ };
+
+ registerMutation.mutate(registerData);
+ }}
+ >
+
+
+
+ {t("username")}
+
+
+ {t("errors.enterUsername")}
+
+
+
+
+
+ {(validity) => (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {t("email")}
+
+
+ {t("errors.enterEmail")}
+
+
+ {t("errors.invalidEmail")}
+
+
+
+
+
+ {(validity) => (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+ {t("password")}
+
+
+ {t("errors.enterPassword")}
+
+
+
+
+
+ {(validity) => (
+
+
+
+ )}
+
+
+
+
+ {t("capsLogWarning")}
+
+
+
+
+
+
+ {t("confirmPassword")}
+
+
+ {t("errors.enterPassword")}
+
+
+ value !== formData.get("password")
+ }
+ >
+
+ {t("errors.passwordsMismatch")}
+
+
+
+
+
+
+ {(validity) => (
+
+
+
+ )}
+
+
+
+
+ {t("capsLogWarning")}
+
+
+
+
+ {t("errors.invalidRegisterData")}
+
+
+
+
+ {t("submit")}
+
+
+
+
+ {t("alreadyRegistered")}{" "}
+
+ {t("logIn")}
+ {" "}
+ {t("now")}
+
+
+
+ {t("byPressingTheButton")}{" "}
+
+ {t("termsOfService")} .
+
+
+
+
+ );
+}
diff --git a/enshi/src/Pages/LoginRegisterPage/ShowPasswordButton/ShowPasswordButton.tsx b/enshi/src/Pages/LoginRegisterPage/ShowPasswordButton/ShowPasswordButton.tsx
new file mode 100644
index 0000000..e7bd092
--- /dev/null
+++ b/enshi/src/Pages/LoginRegisterPage/ShowPasswordButton/ShowPasswordButton.tsx
@@ -0,0 +1,40 @@
+import { EyeClosedIcon, EyeOpenIcon } from "@radix-ui/react-icons";
+import { IconButton, Tooltip } from "@radix-ui/themes";
+import { Dispatch, SetStateAction } from "react";
+
+type TShowPasswordButton = {
+ isShown: boolean;
+ setIsShown: Dispatch>;
+};
+
+export default function ShowPasswordButton({ isShown, setIsShown }: TShowPasswordButton) {
+ return (
+
+
+ {isShown ? (
+ {
+ setIsShown(!isShown);
+ }}
+ size={"1"}
+ className="rounded-full"
+ variant="soft"
+ >
+
+
+ ) : (
+ setIsShown(!isShown)}
+ size={"1"}
+ className="rounded-full"
+ variant="soft"
+ >
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/enshi/src/Pages/MainPage/MainPage.tsx b/enshi/src/Pages/MainPage/MainPage.tsx
deleted file mode 100644
index 905549a..0000000
--- a/enshi/src/Pages/MainPage/MainPage.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react";
-import { Outlet } from "react-router-dom";
-import NavBar from "../../Components/NavBar/NavBar";
-import { axiosLocalhost } from "../../api/axios/axios";
-import { Container } from "@radix-ui/themes";
-
-export default function MainPage() {
- return (
- <>
-
-
- {
- let d = await axiosLocalhost.get("getCookie");
- console.log(d.data);
- }}
- >
- Click for cookie test
-
- >
- );
-}
diff --git a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx
new file mode 100644
index 0000000..eb677ba
--- /dev/null
+++ b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx
@@ -0,0 +1,43 @@
+import { Box, Container, Flex } from "@radix-ui/themes";
+import { useAtom, useSetAtom } from "jotai";
+import {
+ postCreationAtom,
+ postCreationTitleAtom
+} from "../../AtomStore/AtomStore";
+import Editor from "../../Components/Editor/Editor";
+import SubmitPostButton from "./SubmitPostButton/SubmitPostButton";
+
+export default function PostCreatorPage() {
+ const [titleValue, setTitleValue] = useAtom(postCreationTitleAtom);
+ const setContentValue = useSetAtom(postCreationAtom);
+
+ return (
+ <>
+
+
+
+
+ {
+ setTitleValue(e.target.value);
+ }}
+ value={titleValue}
+ />
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx b/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx
new file mode 100644
index 0000000..201d22d
--- /dev/null
+++ b/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx
@@ -0,0 +1,64 @@
+import { Button } from "@radix-ui/themes";
+import { useMutation } from "@tanstack/react-query";
+import { useAtom } from "jotai";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+import { axiosLocalhost } from "../../../api/axios/axios";
+import {
+ postCreationAtom,
+ postCreationTitleAtom,
+} from "../../../AtomStore/AtomStore";
+
+type TSubmitPostButton = {
+ className: string;
+};
+
+export default function SubmitPostButton(props: TSubmitPostButton) {
+ const { t } = useTranslation();
+
+ const [isDisabled, setIsDisabled] = useState(false);
+
+ const [contentValue, setContentValue] = useAtom(postCreationAtom);
+ const [titleValue, setTitleValue] = useAtom(postCreationTitleAtom);
+
+ const navigate = useNavigate();
+
+ const postMutation = useMutation({
+ mutationFn: async () => {
+ if (!titleValue) throw new Error("no title provided");
+ if (!contentValue || contentValue === "
")
+ throw new Error("no content provided");
+
+ axiosLocalhost.post("/posts", {
+ title: titleValue,
+ content: contentValue,
+ });
+ },
+ onMutate: () => {
+ setIsDisabled(true);
+ },
+ onError: () => {
+ setIsDisabled(false);
+ },
+ onSuccess: () => {
+ setContentValue("");
+ setTitleValue("");
+ navigate("/");
+ },
+ });
+
+ return (
+ {
+ postMutation.mutate();
+ }}
+ className={props.className}
+ variant="soft"
+ size={"4"}
+ disabled={isDisabled}
+ >
+ {t("submit")}
+
+ );
+}
diff --git a/enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx b/enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx
new file mode 100644
index 0000000..2b29f64
--- /dev/null
+++ b/enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx
@@ -0,0 +1,30 @@
+import { ImageIcon } from "@radix-ui/react-icons";
+import { Box, Card, Heading } from "@radix-ui/themes";
+import { useNavigate } from "react-router-dom";
+import { GetRandomPostsRow } from "../../../@types/PostTypes";
+
+type TPostCard = {
+ post: GetRandomPostsRow;
+};
+
+export default function PostCard({ post }: TPostCard) {
+ const navigate = useNavigate()
+
+ const clickHandler = () => {
+ navigate(`/posts/${post.post_id.toString()}`)
+ }
+
+ return (
+
+
+
+
+
+
+
+ {post.title}
+
+
+
+ );
+}
diff --git a/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx b/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx
new file mode 100644
index 0000000..605e04f
--- /dev/null
+++ b/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx
@@ -0,0 +1,67 @@
+import * as ScrollArea from "@radix-ui/react-scroll-area";
+import { Container, Flex, Heading, Separator } from "@radix-ui/themes";
+import { useQuery } from "@tanstack/react-query";
+import { useTranslation } from "react-i18next";
+import { GetRandomPostsRow } from "../../@types/PostTypes";
+import { axiosLocalhost } from "../../api/axios/axios";
+import PostCard from "./PostCard/PostCard";
+
+const LIMIT = 10;
+
+export default function RandomPostsPage() {
+ const {t} = useTranslation()
+
+ const { data, refetch } = useQuery({
+ queryKey: ["random_posts_key"],
+ queryFn: async () => {
+ try {
+ const response = await axiosLocalhost.get(
+ `/posts/random?limit=${LIMIT}`
+ );
+
+ return response.data as GetRandomPostsRow[];
+ } catch (error) {
+ console.log(`Something went wrong`);
+ }
+
+ return [];
+ },
+ });
+
+ return (
+ <>
+
+
+ {t("discover")}
+
+
+
+
+
+
+ {data?.map((post, i) => {
+ return (
+
+
+
+ );
+ })}
+
+
+
+
+ {/*
+
+ */}
+ {/* */}
+
+
+ >
+ );
+}
diff --git a/enshi/src/Pages/UserBlogsPage/SkeletonBoxes/SkeletonBoxes.tsx b/enshi/src/Pages/UserBlogsPage/SkeletonBoxes/SkeletonBoxes.tsx
new file mode 100644
index 0000000..2419e98
--- /dev/null
+++ b/enshi/src/Pages/UserBlogsPage/SkeletonBoxes/SkeletonBoxes.tsx
@@ -0,0 +1,17 @@
+import { Box, Skeleton } from "@radix-ui/themes";
+
+export default function SkeletonBoxes() {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx b/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx
new file mode 100644
index 0000000..599ae6d
--- /dev/null
+++ b/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx
@@ -0,0 +1,126 @@
+import * as Dialog from "@radix-ui/react-dialog";
+import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
+import {
+ Box,
+ Button,
+ Container,
+ Flex,
+ Separator,
+ Text,
+} from "@radix-ui/themes";
+import { useQuery } from "@tanstack/react-query";
+import { axiosLocalhost } from "../../api/axios/axios";
+import BlogBox from "../../Components/BlogBox/BlogBox";
+import { JSONWithInt64 } from "../../utils/idnex";
+import SkeletonBoxes from "./SkeletonBoxes/SkeletonBoxes";
+
+export default function UserBlogsPage() {
+ const { data, isPending, isFetching } = useQuery({
+ queryKey: ["userBlogs"],
+ queryFn: async () => {
+ const response = await axiosLocalhost.get("/user/blogs", {
+ transformResponse: [(data) => data],
+ });
+
+ let temp = JSONWithInt64(response.data);
+
+ return temp as any[];
+ },
+ });
+
+ if (isPending)
+ return (
+
+
+
+ );
+
+ return (
+
+
+
+
+ Your blogs
+
+
+
+
+ {data
+ ? data?.map((blog: any, b) => {
+ return (
+ <>
+
+ >
+ );
+ })
+ : null}
+
+
+
+ {}}>
+
+
+
+
+
+
+
+ Create blog
+
+
+ Create your new blog.
+
+
+
+ Blog title
+
+
+
+
+
+ Description
+
+
+
+
+
+
+ Create blog
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/enshi/src/api/axios/axios.ts b/enshi/src/api/axios/axios.ts
index 1a352da..858e9a0 100644
--- a/enshi/src/api/axios/axios.ts
+++ b/enshi/src/api/axios/axios.ts
@@ -2,7 +2,7 @@ import axios from "axios";
export const axiosLocalhost = axios.create(
{
- baseURL: `http://localhost:9876/`,
+ baseURL: `http://127.0.0.1:9876/`,
withCredentials: true,
headers: {
diff --git a/enshi/src/constants/textForSkeleton.ts b/enshi/src/constants/textForSkeleton.ts
new file mode 100644
index 0000000..b166637
--- /dev/null
+++ b/enshi/src/constants/textForSkeleton.ts
@@ -0,0 +1,12 @@
+export const pText = `The goal of typography is to relate font size, line
+ height, and line width in a proportional way that
+ maximizes beauty and makes reading easier and more
+ pleasant. The question is: What proportion(s) will give
+ us the best results? The golden ratio is often observed
+ in nature where beauty and utility intersect; perhaps we
+ can use this “divine” proportion to enhance these
+ attributes in our typography.`;
+
+export const headerLong = `THUS SHU SHU HDFQIUWKHFQWHF KJQWHqwfiqfquwdhqwjdk`;
+
+export const headerShort = `THUS SHU SHU HDFQIUWKHFQWHF`;
diff --git a/enshi/src/hooks/useCapsLock.tsx b/enshi/src/hooks/useCapsLock.tsx
new file mode 100644
index 0000000..04abe26
--- /dev/null
+++ b/enshi/src/hooks/useCapsLock.tsx
@@ -0,0 +1,27 @@
+import { useEffect, useState } from "react";
+
+export default function UseCapsLock() {
+
+ const [isCapsLockOn, setIsCapsLockOn] = useState(false);
+
+ useEffect(() => {
+ const f = (e: KeyboardEvent) => {
+ if (e.getModifierState("CapsLock")) {
+ setIsCapsLockOn(true);
+ } else {
+ setIsCapsLockOn(false);
+ }
+ };
+
+ document.addEventListener("keydown", f);
+
+ return () => {
+ document.removeEventListener("keydown", f);
+ };
+ }, []);
+
+
+ return {
+ isCapsLockOn
+ }
+}
\ No newline at end of file
diff --git a/enshi/src/hooks/useToast.tsx b/enshi/src/hooks/useToast.tsx
new file mode 100644
index 0000000..b4fd553
--- /dev/null
+++ b/enshi/src/hooks/useToast.tsx
@@ -0,0 +1,7 @@
+import { useSetAtom } from "jotai";
+import { setToastAtom } from "../AtomStore/AtomStore";
+
+export default function useToast() {
+ const createToast = useSetAtom(setToastAtom);
+ return createToast;
+}
diff --git a/enshi/src/index.css b/enshi/src/index.css
index 5238975..574086b 100644
--- a/enshi/src/index.css
+++ b/enshi/src/index.css
@@ -4,6 +4,11 @@
@tailwind components;
@tailwind utilities;
+@layer components {
+ .center-of-parent {
+ @apply absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%];
+ }
+}
.radix-themes {
--default-font-family: "Times New Roman"; ;
diff --git a/enshi/src/layout/MainPage/MainPage.tsx b/enshi/src/layout/MainPage/MainPage.tsx
new file mode 100644
index 0000000..fe64d01
--- /dev/null
+++ b/enshi/src/layout/MainPage/MainPage.tsx
@@ -0,0 +1,74 @@
+import { Box, Flex, Spinner } from "@radix-ui/themes";
+import { useQuery } from "@tanstack/react-query";
+import { useSetAtom } from "jotai";
+import { Outlet } from "react-router-dom";
+import { axiosLocalhost } from "../../api/axios/axios";
+import { userAtom } from "../../AtomStore/AtomStore";
+import NavBar from "../../Components/NavBar/NavBar";
+
+const REFETCH_INTERVAL_IN_MINUTES = 5;
+const RETRY_INTERVAL_IN_SECONDS = 1;
+
+const SECONDS_IN_MINUTE = 60;
+const MILLS_IN_SECOND = 1000;
+
+const TAGS = Array.from({ length: 50 }).map(
+ (_, i, a) => `v1.2.0-beta.${a.length - i}`
+);
+
+export default function MainPage() {
+ const setUserData = useSetAtom(userAtom);
+
+ const { isPending } = useQuery({
+ queryKey: ["authKey"],
+ queryFn: async () => {
+ try {
+ const response = await axiosLocalhost.get("/auth/check");
+
+ setUserData({
+ isAdmin: response.data["is_admin"],
+ username: response.data["username"],
+ id: response.data["id"],
+ });
+ return true;
+ } catch (error) {
+ setUserData(undefined);
+ return false;
+ }
+ },
+ refetchInterval:
+ REFETCH_INTERVAL_IN_MINUTES * SECONDS_IN_MINUTE * MILLS_IN_SECOND,
+ refetchOnWindowFocus: true,
+ refetchOnReconnect: true,
+ gcTime: 10,
+
+ retry: 3,
+ retryDelay: (attempt) =>
+ attempt * RETRY_INTERVAL_IN_SECONDS * MILLS_IN_SECOND,
+ });
+
+ return (
+ <>
+ {isPending ? (
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+ )}
+ >
+ );
+}
diff --git a/enshi/src/locale/en.ts b/enshi/src/locale/en.ts
index bd15612..caacecb 100644
--- a/enshi/src/locale/en.ts
+++ b/enshi/src/locale/en.ts
@@ -1,6 +1,53 @@
const en = {
hello: "hello!",
search: "Search...",
-}
+ username: "Username",
+ email: "Email",
+ password: "Password",
+ confirmPassword: "Confirm password",
+ submit: "Submit",
-export default en;
\ No newline at end of file
+ createPost: "Write post",
+
+ profile: "Profile",
+ yourBlogs: "Your blogs",
+
+ signIn: "Log in",
+ signOut: "Sign out",
+
+ capsLogWarning: "CapsLock is on",
+
+ registerForm: "Register",
+ loginForm: "Log in",
+
+ alreadyRegistered: "Already registered?",
+
+ suggestRegister: "Don't have an account?",
+ register: "Register",
+ now: "now!",
+
+ logIn: "Log in",
+
+ updatePost: "Update",
+
+ byPressingTheButton: "By pressing the submit button you agree with our",
+ termsOfService: "Terms Of Service",
+
+ discover: "Discover something new...",
+
+ home: "Home",
+ following: "Following",
+
+ errors: {
+ enterUsername: "Please enter your username",
+ enterEmail: "Please enter your email",
+ invalidEmail: "Please enter correct email",
+ enterPassword: "Please enter your password",
+ passwordsMismatch: "Passwords must be the same",
+ invalidLoginData: "Invalid username or password",
+ invalidRegisterData: "Invalid register data",
+ unauthorized: "You need to be authorized to do that",
+ },
+};
+
+export default en;
diff --git a/enshi/src/locale/ru.ts b/enshi/src/locale/ru.ts
index abba300..b50c34f 100644
--- a/enshi/src/locale/ru.ts
+++ b/enshi/src/locale/ru.ts
@@ -1,6 +1,56 @@
+
const ru = {
hello: "Привет!",
search: "Поиск...",
-}
+ username: "Имя пользователя",
+ email: "Электронная почта",
+ password: "Пароль",
+ confirmPassword: "Подтвердите пароль",
+ submit: "Подтвердить",
-export default ru;
\ No newline at end of file
+ createPost: "Написать пост",
+
+ profile: "Профиль",
+ yourBlogs: "Ваши блоги",
+
+ signIn: "Войти",
+ signOut: "Выйти",
+
+ capsLogWarning: "Включён CapsLock",
+
+ registerForm: "Регистрация",
+ loginForm: "Вход",
+
+ alreadyRegistered: "Уже есть аккаунт?",
+
+ suggestRegister: "Не зарегистрированы?",
+ register: "Создайте аккаунт",
+ now: "сейчас!",
+
+ logIn: "Войдите",
+
+ byPressingTheButton: "Нажимая `Подтвердить`, вы соглашаетесь с нашими",
+ termsOfService: "Условиями предоставления услуг.",
+
+ updatePost: "Изменить",
+
+ discover: "Найдите что-то новое",
+
+ home: "Главная",
+ following: "Отслеживаемые",
+
+
+ errors: {
+ enterUsername: "Это обязательное поле",
+ enterEmail: "Это обязательное поле",
+ invalidEmail: "Некорректный адрес электронной почты",
+ enterPassword: "Пожалуйста, введите пароль",
+ passwordsMismatch: "Пароли должны быть одинаковыми",
+ invalidLoginData: "Неверное имя пользователя или пароль",
+ invalidRegisterData:
+ "Пользователь с таким адресом электронной почты или именем пользователя уже существует",
+ unauthorized: "Вы должны быть авторизованы, чтобы сделать это",
+ },
+};
+
+export default ru;
diff --git a/enshi/src/routes/routes.tsx b/enshi/src/routes/routes.tsx
index 8331bbb..ef8fa3e 100644
--- a/enshi/src/routes/routes.tsx
+++ b/enshi/src/routes/routes.tsx
@@ -1,7 +1,20 @@
-import { createRoutesFromElements, Route, useRouteError } from "react-router-dom"
-import MainPage from "../Pages/MainPage/MainPage"
-import {Text} from "@radix-ui/themes";
-
+import { Text } from "@radix-ui/themes";
+import {
+ createRoutesFromElements,
+ Outlet,
+ Route,
+ useRouteError,
+} from "react-router-dom";
+import ArticleViewer from "../Components/ArticleViewer/ArticleViewer";
+import MainPage from "../layout/MainPage/MainPage";
+import AuthPageWrapper from "../Pages/AuthPageWrapper/AuthPageWrapper";
+import BlogPage from "../Pages/BlogPage/BlogPage";
+import LoginPage from "../Pages/LoginRegisterPage/LoginPage/LoginPage";
+import PostRedactor from "../Pages/LoginRegisterPage/PostRedactor/PostRedactor";
+import RegisterPage from "../Pages/LoginRegisterPage/RegisterPage/RegisterPage";
+import PostCreatorPage from "../Pages/PostCreatorPage/PostCreatorPage";
+import RandomPostsPage from "../Pages/RandomPostsPage/RandomPostsPage";
+import UserBlogsPage from "../Pages/UserBlogsPage/UserBlogsPage";
function ErrorBoundary() {
let error = useRouteError();
@@ -12,16 +25,54 @@ function ErrorBoundary() {
export const routes = createRoutesFromElements(
<>
- }
- element={ }
- >
- Cringer path} />
+ } element={ }>
+ } />
+
Cringer path, but this a}
- >
+ path="a?/c"
+ element={
+
+ This page is yet to be created
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+ } />
+
+ }>
+
+
+
+ }
+ />
+
+
+ } />
+ } />
+
+ }
+ element={ }
+ />
+
+ }
+ element={ }
+ />
>
-)
\ No newline at end of file
+);
diff --git a/enshi/src/utils/idnex.ts b/enshi/src/utils/idnex.ts
new file mode 100644
index 0000000..380864c
--- /dev/null
+++ b/enshi/src/utils/idnex.ts
@@ -0,0 +1,19 @@
+const isBigNumber = (num: any) => !Number.isSafeInteger(+num);
+
+const enquoteBigNumber = (jsonString: any, bigNumChecker: any) =>
+ jsonString.replaceAll(
+ /([:\s\[,]*)(\d+)([\s,\]]*)/g,
+ (matchingSubstr: any, prefix: any, bigNum: any, suffix: any) =>
+ bigNumChecker(bigNum)
+ ? `${prefix}"${bigNum}"${suffix}`
+ : matchingSubstr
+ );
+
+const parseWithBigInt = (jsonString: any, bigNumChecker: any) =>
+ JSON.parse(enquoteBigNumber(jsonString, bigNumChecker), (_key, value) =>
+ !isNaN(value) && bigNumChecker(value) ? BigInt(value).toString() : value
+ );
+
+export const JSONWithInt64 = (jsonString: any) => {
+ return parseWithBigInt(jsonString, isBigNumber);
+};
diff --git a/enshi/tailwind.config.js b/enshi/tailwind.config.js
index af5c3aa..3ae45b8 100644
--- a/enshi/tailwind.config.js
+++ b/enshi/tailwind.config.js
@@ -7,11 +7,32 @@ export default {
"primary-color": "var(--primary-color)",
"secondary-color": "var(--secondary-color)",
},
+ fontFamily: {
+ 'times': "Times New Roman"
+ },
animation: {
appear: "appear 0.25s",
widthOut: "widthOut cubic-bezier(0.4, 0, 0.6, 1) 0.4s",
+ slideFromRight: "slideFromRight cubic-bezier(0.4, 0, 0.6, 1) 0.2s",
+ fadeOut: "fadeOut 0.2s ease-in",
},
keyframes: {
+ fadeOut: {
+ from: {
+ opacity: "1",
+ },
+ to: {
+ opacity: "0",
+ }
+ },
+ slideFromRight: {
+ "0%": {
+ transform: "translateX(110%)"
+ },
+ "100%": {
+ transform: "translateX(0%)"
+ }
+ },
appear: {
"100%": { opacity: "1" },
},
diff --git a/enshi_back/ABAC/BookmarkPolicies/bookmarkPolicies.go b/enshi_back/ABAC/BookmarkPolicies/bookmarkPolicies.go
new file mode 100644
index 0000000..fbae00d
--- /dev/null
+++ b/enshi_back/ABAC/BookmarkPolicies/bookmarkPolicies.go
@@ -0,0 +1,36 @@
+package bookmarkspolicies
+
+import (
+ bookmarksrules "enshi/ABAC/BookmarkPolicies/bookmarkRules"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ DELETE_BOOKMARK = "delete_bookmark"
+ CREATE_BOOKMARK = "create_bookmark"
+ READ_BOOKMARK = "read_bookmark"
+)
+
+func BlogPolicies(c *gin.Context) (bool, []error) {
+ target, exists := c.Get("target")
+ if !exists {
+ return false, nil
+ }
+
+ // Permit if one permit
+ switch target {
+ case DELETE_BOOKMARK:
+ return rules.CheckRule(c, bookmarksrules.BookmarkDeleteRule)
+
+ case CREATE_BOOKMARK:
+ return rules.CheckRule(c, bookmarksrules.BookmarkCreateRule)
+
+ case READ_BOOKMARK:
+ return rules.CheckRule(c, bookmarksrules.BookmarkReadRule)
+
+ }
+
+ return false, nil
+}
diff --git a/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/createRule.go b/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/createRule.go
new file mode 100644
index 0000000..fdac869
--- /dev/null
+++ b/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/createRule.go
@@ -0,0 +1,22 @@
+package bookmarksrules
+
+import (
+ globalrules "enshi/ABAC/GlobalRules"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+func BookmarkCreateRule(c *gin.Context) (bool, []error) {
+ rulesToCheck := []rules.RuleFunction{
+ globalrules.AuthorizedRule,
+ }
+
+ isAllowed, errors := rules.CheckRules(
+ c,
+ rulesToCheck,
+ rules.ALL_RULES_MUST_BE_COMPLETED,
+ )
+
+ return isAllowed, errors
+}
diff --git a/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/deleteRule.go b/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/deleteRule.go
new file mode 100644
index 0000000..510b8ad
--- /dev/null
+++ b/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/deleteRule.go
@@ -0,0 +1,22 @@
+package bookmarksrules
+
+import (
+ globalrules "enshi/ABAC/GlobalRules"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+func BookmarkDeleteRule(c *gin.Context) (bool, []error) {
+ rulesToCheck := []rules.RuleFunction{
+ globalrules.AuthorizedRule,
+ }
+
+ isAllowed, errors := rules.CheckRules(
+ c,
+ rulesToCheck,
+ rules.ALL_RULES_MUST_BE_COMPLETED,
+ )
+
+ return isAllowed, errors
+}
diff --git a/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/readRule.go b/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/readRule.go
new file mode 100644
index 0000000..ad888b6
--- /dev/null
+++ b/enshi_back/ABAC/BookmarkPolicies/bookmarkRules/readRule.go
@@ -0,0 +1,22 @@
+package bookmarksrules
+
+import (
+ globalrules "enshi/ABAC/GlobalRules"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+func BookmarkReadRule(c *gin.Context) (bool, []error) {
+ rulesToCheck := []rules.RuleFunction{
+ globalrules.AuthorizedRule,
+ }
+
+ isAllowed, errors := rules.CheckRules(
+ c,
+ rulesToCheck,
+ rules.ALL_RULES_MUST_BE_COMPLETED,
+ )
+
+ return isAllowed, errors
+}
diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go b/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go
new file mode 100644
index 0000000..9e597bf
--- /dev/null
+++ b/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go
@@ -0,0 +1,38 @@
+package postvotespolicies
+
+import (
+ postvoterules "enshi/ABAC/PostVotesPolicies/PostVoteRules"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ DELETE_VOTE = "delete_vote"
+ CREATE_VOTE = "create_vote"
+ READ_VOTE = "read_vote"
+)
+
+func PostVotePolicies(c *gin.Context) (bool, []error) {
+ target, exists := c.Get("target")
+ if !exists {
+ return false, nil
+ }
+
+ // Permit if one permit
+ switch target {
+ case DELETE_VOTE:
+ return rules.CheckRule(c, postvoterules.PostVoteDeleteRule)
+
+ case CREATE_VOTE:
+ return rules.CheckRule(c, postvoterules.PostVoteCreateRule)
+
+ case READ_VOTE:
+ return rules.CheckRule(c, postvoterules.PostVoteReadRule)
+
+ default:
+ return rules.CheckRule(c, postvoterules.PostVotesReadRule)
+ }
+
+ return false, nil
+}
diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/createRule.go b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/createRule.go
new file mode 100644
index 0000000..cdde23a
--- /dev/null
+++ b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/createRule.go
@@ -0,0 +1,22 @@
+package postvoterules
+
+import (
+ globalrules "enshi/ABAC/GlobalRules"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+func PostVoteCreateRule(c *gin.Context) (bool, []error) {
+ rulesToCheck := []rules.RuleFunction{
+ globalrules.AuthorizedRule,
+ }
+
+ isAllowed, errors := rules.CheckRules(
+ c,
+ rulesToCheck,
+ rules.ALL_RULES_MUST_BE_COMPLETED,
+ )
+
+ return isAllowed, errors
+}
diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/deleteRule.go b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/deleteRule.go
new file mode 100644
index 0000000..b24ecc9
--- /dev/null
+++ b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/deleteRule.go
@@ -0,0 +1,22 @@
+package postvoterules
+
+import (
+ globalrules "enshi/ABAC/GlobalRules"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+func PostVoteDeleteRule(c *gin.Context) (bool, []error) {
+ rulesToCheck := []rules.RuleFunction{
+ globalrules.AuthorizedRule,
+ }
+
+ isAllowed, errors := rules.CheckRules(
+ c,
+ rulesToCheck,
+ rules.ALL_RULES_MUST_BE_COMPLETED,
+ )
+
+ return isAllowed, errors
+}
diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readRule.go b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readRule.go
new file mode 100644
index 0000000..0f707cc
--- /dev/null
+++ b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readRule.go
@@ -0,0 +1,22 @@
+package postvoterules
+
+import (
+ globalrules "enshi/ABAC/GlobalRules"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+func PostVoteReadRule(c *gin.Context) (bool, []error) {
+ rulesToCheck := []rules.RuleFunction{
+ globalrules.AuthorizedRule,
+ }
+
+ isAllowed, errors := rules.CheckRules(
+ c,
+ rulesToCheck,
+ rules.ALL_RULES_MUST_BE_COMPLETED,
+ )
+
+ return isAllowed, errors
+}
diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readVotesRule.go b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readVotesRule.go
new file mode 100644
index 0000000..736684e
--- /dev/null
+++ b/enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readVotesRule.go
@@ -0,0 +1,17 @@
+package postvoterules
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+func PostVotesReadRule(c *gin.Context) (bool, []error) {
+ // rulesToCheck := []rules.RuleFunction{}
+
+ // isAllowed, errors := rules.CheckRules(
+ // c,
+ // rulesToCheck,
+ // rules.ALL_RULES_MUST_BE_COMPLETED,
+ // )
+
+ return true, nil
+}
diff --git a/enshi_back/db/go_queries/post_votes_queries.sql.go b/enshi_back/db/go_queries/post_votes_queries.sql.go
index 171b179..05e957c 100644
--- a/enshi_back/db/go_queries/post_votes_queries.sql.go
+++ b/enshi_back/db/go_queries/post_votes_queries.sql.go
@@ -13,6 +13,9 @@ const createPostVote = `-- name: CreatePostVote :one
INSERT INTO public.post_votes
(post_id, user_id, vote)
VALUES($1, $2, $3)
+ON CONFLICT (user_id, post_id)
+DO UPDATE SET
+ vote = $3
RETURNING post_id, user_id, vote
`
@@ -62,6 +65,25 @@ func (q *Queries) GetPostVote(ctx context.Context, arg GetPostVoteParams) (bool,
return vote, err
}
+const getPostVotes = `-- name: GetPostVotes :one
+SELECT count (*) FILTER (WHERE vote = TRUE) as upvotes,
+count (*) FILTER (WHERE vote = FALSE) as downvotes
+FROM public.post_votes
+WHERE post_id = $1
+`
+
+type GetPostVotesRow struct {
+ Upvotes int64 `json:"upvotes"`
+ Downvotes int64 `json:"downvotes"`
+}
+
+func (q *Queries) GetPostVotes(ctx context.Context, postID int64) (GetPostVotesRow, error) {
+ row := q.db.QueryRow(ctx, getPostVotes, postID)
+ var i GetPostVotesRow
+ err := row.Scan(&i.Upvotes, &i.Downvotes)
+ return i, err
+}
+
const updateVote = `-- name: UpdateVote :one
UPDATE public.post_votes
SET vote=$1
diff --git a/enshi_back/db/go_queries/posts_queries.sql.go b/enshi_back/db/go_queries/posts_queries.sql.go
index 5dc478f..c778e15 100644
--- a/enshi_back/db/go_queries/posts_queries.sql.go
+++ b/enshi_back/db/go_queries/posts_queries.sql.go
@@ -146,6 +146,47 @@ func (q *Queries) GetPostsByUserId(ctx context.Context, userID int64) ([]Post, e
return items, nil
}
+const getRandomPosts = `-- name: GetRandomPosts :many
+SELECT post_id, blog_id, user_id, title, created_at
+FROM public.posts
+ORDER BY RANDOM()
+LIMIT $1
+`
+
+type GetRandomPostsRow struct {
+ PostID int64 `json:"post_id"`
+ BlogID pgtype.Int8 `json:"blog_id"`
+ UserID int64 `json:"user_id"`
+ Title pgtype.Text `json:"title"`
+ CreatedAt pgtype.Timestamp `json:"created_at"`
+}
+
+func (q *Queries) GetRandomPosts(ctx context.Context, limit int32) ([]GetRandomPostsRow, error) {
+ rows, err := q.db.Query(ctx, getRandomPosts, limit)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ var items []GetRandomPostsRow
+ for rows.Next() {
+ var i GetRandomPostsRow
+ if err := rows.Scan(
+ &i.PostID,
+ &i.BlogID,
+ &i.UserID,
+ &i.Title,
+ &i.CreatedAt,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
+
const updatePostBlogId = `-- name: UpdatePostBlogId :exec
UPDATE public.posts
SET blog_id=$2, updated_at=CURRENT_TIMESTAMP
diff --git a/enshi_back/db/go_queries/users_queries.sql.go b/enshi_back/db/go_queries/users_queries.sql.go
index de0e1d3..2660956 100644
--- a/enshi_back/db/go_queries/users_queries.sql.go
+++ b/enshi_back/db/go_queries/users_queries.sql.go
@@ -152,6 +152,17 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
return i, err
}
+const getUserUsernameById = `-- name: GetUserUsernameById :one
+SELECT username FROM users WHERE user_id = $1
+`
+
+func (q *Queries) GetUserUsernameById(ctx context.Context, userID int64) (string, error) {
+ row := q.db.QueryRow(ctx, getUserUsernameById, userID)
+ var username string
+ err := row.Scan(&username)
+ return username, err
+}
+
const updateUserPasswordHash = `-- name: UpdateUserPasswordHash :one
UPDATE public.users
SET "password"=$1
diff --git a/enshi_back/db/queries/post_votes_queries.sql b/enshi_back/db/queries/post_votes_queries.sql
index 74cbfd3..ea1443a 100644
--- a/enshi_back/db/queries/post_votes_queries.sql
+++ b/enshi_back/db/queries/post_votes_queries.sql
@@ -2,6 +2,9 @@
INSERT INTO public.post_votes
(post_id, user_id, vote)
VALUES($1, $2, $3)
+ON CONFLICT (user_id, post_id)
+DO UPDATE SET
+ vote = $3
RETURNING *;
-- name: DeletePostVote :exec
@@ -17,4 +20,10 @@ RETURNING *;
-- name: GetPostVote :one
SELECT vote
FROM public.post_votes p_v
-WHERE p_v.user_id = $1 and p_v.post_id = $2;
\ No newline at end of file
+WHERE p_v.user_id = $1 and p_v.post_id = $2;
+
+-- name: GetPostVotes :one
+SELECT count (*) FILTER (WHERE vote = TRUE) as upvotes,
+count (*) FILTER (WHERE vote = FALSE) as downvotes
+FROM public.post_votes
+WHERE post_id = $1;
diff --git a/enshi_back/db/queries/posts_queries.sql b/enshi_back/db/queries/posts_queries.sql
index bc260c1..9f22b8f 100644
--- a/enshi_back/db/queries/posts_queries.sql
+++ b/enshi_back/db/queries/posts_queries.sql
@@ -33,4 +33,10 @@ WHERE post_id=$1;
UPDATE public.posts
SET blog_id=$2, updated_at=CURRENT_TIMESTAMP
WHERE post_id = $1
-RETURNING *;
\ No newline at end of file
+RETURNING *;
+
+-- name: GetRandomPosts :many
+SELECT post_id, blog_id, user_id, title, created_at
+FROM public.posts
+ORDER BY RANDOM()
+LIMIT $1;
\ No newline at end of file
diff --git a/enshi_back/db/queries/users_queries.sql b/enshi_back/db/queries/users_queries.sql
index fcf2f59..1c84a51 100644
--- a/enshi_back/db/queries/users_queries.sql
+++ b/enshi_back/db/queries/users_queries.sql
@@ -4,6 +4,9 @@ SELECT * FROM users;
-- name: GetUserById :one
SELECT * FROM users WHERE user_id = $1;
+-- name: GetUserUsernameById :one
+SELECT username FROM users WHERE user_id = $1;
+
-- name: GetUserByUsername :one
SELECT * FROM users WHERE username = $1;
diff --git a/enshi_back/global/globalVars.go b/enshi_back/global/globalVars.go
index a581119..0116c2b 100644
--- a/enshi_back/global/globalVars.go
+++ b/enshi_back/global/globalVars.go
@@ -1,7 +1,7 @@
package global
const PathForCookies = "/"
-const DomainForCookies = "localhost"
+const DomainForCookies = "127.0.0.1"
const SecureForCookies = false
const HttpOnlyForCookies = false
diff --git a/enshi_back/middleware/authMiddleware.go b/enshi_back/middleware/authMiddleware.go
index 201f08f..e738ad3 100644
--- a/enshi_back/middleware/authMiddleware.go
+++ b/enshi_back/middleware/authMiddleware.go
@@ -1,8 +1,10 @@
package middleware
import (
+ rest_api_stuff "enshi/REST_API_stuff"
"enshi/auth"
"enshi/global"
+ "fmt"
"net/http"
"github.com/gin-gonic/gin"
@@ -11,9 +13,14 @@ import (
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
- // token := c.GetHeader("Authorization")
+ cookies := c.Request.CookiesNamed("auth_cookie")
+ if len(cookies) == 0 {
+ rest_api_stuff.UnauthorizedAnswer(c, fmt.Errorf("no token provided"))
+ c.Abort()
+ return
+ }
- tokenFromCookies := c.Request.CookiesNamed("auth_cookie")[0].Value
+ tokenFromCookies := cookies[0].Value
cookieClimes, err := auth.ValidateToken(tokenFromCookies)
if err != nil {
c.IndentedJSON(http.StatusUnauthorized, gin.H{"error auth": err.Error()})
@@ -21,14 +28,6 @@ func AuthMiddleware() gin.HandlerFunc {
return
}
- // claims, err := auth.ValidateToken(token)
- // if err != nil {
- // c.IndentedJSON(http.StatusUnauthorized, gin.H{"error auth": err.Error()})
- // c.Abort()
- // return
- // }
-
- // Claims -> data stored in token
c.Set(global.ContextUserId, cookieClimes["id"])
c.Set(global.ContextTokenData, cookieClimes)
c.Next()
diff --git a/enshi_back/middleware/bookmarksMiddleware.go b/enshi_back/middleware/bookmarksMiddleware.go
new file mode 100644
index 0000000..b2ca48a
--- /dev/null
+++ b/enshi_back/middleware/bookmarksMiddleware.go
@@ -0,0 +1,33 @@
+package middleware
+
+import (
+ bookmarkspolicies "enshi/ABAC/BookmarkPolicies"
+ "enshi/ABAC/rules"
+
+ "github.com/gin-gonic/gin"
+)
+
+func BookmarksMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+
+ switch c.Request.Method {
+ case "DELETE":
+ c.Set("target", bookmarkspolicies.DELETE_BOOKMARK)
+
+ case "POST":
+ c.Set("target", bookmarkspolicies.CREATE_BOOKMARK)
+
+ case "GET":
+ c.Set("target", bookmarkspolicies.READ_BOOKMARK)
+ }
+
+ isAllowed, errors := bookmarkspolicies.BlogPolicies(c)
+
+ if rules.ShouldAbortRequest(c, isAllowed, errors) {
+ c.Abort()
+ return
+ }
+
+ c.Next()
+ }
+}
diff --git a/enshi_back/middleware/corsMiddleware.go b/enshi_back/middleware/corsMiddleware.go
index ad20f2d..d0b2b8a 100644
--- a/enshi_back/middleware/corsMiddleware.go
+++ b/enshi_back/middleware/corsMiddleware.go
@@ -4,7 +4,7 @@ import "github.com/gin-gonic/gin"
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
- c.Writer.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
+ c.Writer.Header().Set("Access-Control-Allow-Origin", "http://127.0.0.1:5173")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set(
"Access-Control-Allow-Headers",
diff --git a/enshi_back/middleware/postVotesMiddleware.go b/enshi_back/middleware/postVotesMiddleware.go
new file mode 100644
index 0000000..0992fab
--- /dev/null
+++ b/enshi_back/middleware/postVotesMiddleware.go
@@ -0,0 +1,38 @@
+package middleware
+
+import (
+ postvotespolicies "enshi/ABAC/PostVotesPolicies"
+ "enshi/ABAC/rules"
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+func PostVotesMiddleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ a := strings.Split(c.Request.URL.Path, "/")[1]
+ switch c.Request.Method {
+ case "DELETE":
+ c.Set("target", postvotespolicies.DELETE_VOTE)
+
+ case "POST":
+ c.Set("target", postvotespolicies.CREATE_VOTE)
+
+ case "GET":
+ if a != "post-votes" {
+ c.Set("target", postvotespolicies.READ_VOTE)
+ } else {
+ c.Set("target", "")
+ }
+ }
+
+ isAllowed, errors := postvotespolicies.PostVotePolicies(c)
+
+ if rules.ShouldAbortRequest(c, isAllowed, errors) {
+ c.Abort()
+ return
+ }
+
+ c.Next()
+ }
+}
diff --git a/enshi_back/routes/authRoutes/login.go b/enshi_back/routes/authRoutes/login.go
index 212cb43..5371dd7 100644
--- a/enshi_back/routes/authRoutes/login.go
+++ b/enshi_back/routes/authRoutes/login.go
@@ -68,6 +68,6 @@ func Login(c *gin.Context) {
c.Header("Authorization", token)
c.SetCookie(cookieName, cookieValue, maxAge, path, domain, secure, httpOnly)
- c.IndentedJSON(http.StatusOK, gin.H{"token": token})
+ c.IndentedJSON(http.StatusOK, gin.H{"token": token, "username": user.Username, "id": user.UserID})
}
diff --git a/enshi_back/routes/authRoutes/registerUser.go b/enshi_back/routes/authRoutes/registerUser.go
index 79b994f..12fc962 100644
--- a/enshi_back/routes/authRoutes/registerUser.go
+++ b/enshi_back/routes/authRoutes/registerUser.go
@@ -10,6 +10,7 @@ import (
"enshi/global"
"enshi/hasher"
"fmt"
+ "net/http"
"time"
"github.com/gin-gonic/gin"
@@ -118,5 +119,5 @@ func RegisterUser(c *gin.Context) {
transaction.Commit(context.Background())
rest_api_stuff.SetCookie(c, cookieParams)
- rest_api_stuff.OkAnswer(c, "User has been created!")
+ c.IndentedJSON(http.StatusOK, gin.H{"status": "All good", "username": userParams.Username, "id": userParams.UserID})
}
diff --git a/enshi_back/routes/blogRoutes/getUserBlogs.go b/enshi_back/routes/blogRoutes/getUserBlogs.go
new file mode 100644
index 0000000..184d49e
--- /dev/null
+++ b/enshi_back/routes/blogRoutes/getUserBlogs.go
@@ -0,0 +1,29 @@
+package blogRoutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetUserBlogs(c *gin.Context) {
+ userId, err := getters.GetUserIdFromContext(c)
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+
+ blogData, err := db_repo.New(db_connection.Dbx).
+ GetBlogsByUserId(context.Background(), userId)
+ if err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ }
+
+ c.IndentedJSON(http.StatusOK, blogData)
+}
diff --git a/enshi_back/routes/bookmarksRoutes/createBookmark.go b/enshi_back/routes/bookmarksRoutes/createBookmark.go
new file mode 100644
index 0000000..d12734e
--- /dev/null
+++ b/enshi_back/routes/bookmarksRoutes/createBookmark.go
@@ -0,0 +1,35 @@
+package bookmarksroutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+
+ "github.com/gin-gonic/gin"
+)
+
+func CreateBookmark(c *gin.Context) {
+ var bookmarkParams db_repo.CreateBookmarkParams
+
+ if err := c.BindJSON(&bookmarkParams); err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+
+ userId, err := getters.GetUserIdFromContext(c)
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+ bookmarkParams.UserID = userId
+
+ query := db_repo.New(db_connection.Dbx)
+ if _, err := query.CreateBookmark(context.Background(), bookmarkParams); err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ }
+
+ rest_api_stuff.OkAnswer(c, "Bookmark has been created!")
+}
diff --git a/enshi_back/routes/bookmarksRoutes/deleteBookmark.go b/enshi_back/routes/bookmarksRoutes/deleteBookmark.go
new file mode 100644
index 0000000..b0d9432
--- /dev/null
+++ b/enshi_back/routes/bookmarksRoutes/deleteBookmark.go
@@ -0,0 +1,35 @@
+package bookmarksroutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+
+ "github.com/gin-gonic/gin"
+)
+
+func DeleteBookmark(c *gin.Context) {
+ var bookmarkParams db_repo.DeleteBookmarkParams
+
+ if err := c.BindJSON(&bookmarkParams); err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+
+ userId, err := getters.GetUserIdFromContext(c)
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+ bookmarkParams.UserID = userId
+
+ query := db_repo.New(db_connection.Dbx)
+ if err := query.DeleteBookmark(context.Background(), bookmarkParams); err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ }
+
+ rest_api_stuff.OkAnswer(c, "Bookmark has been deleted!")
+}
diff --git a/enshi_back/routes/bookmarksRoutes/getBookmark.go b/enshi_back/routes/bookmarksRoutes/getBookmark.go
new file mode 100644
index 0000000..c2630bc
--- /dev/null
+++ b/enshi_back/routes/bookmarksRoutes/getBookmark.go
@@ -0,0 +1,48 @@
+package bookmarksroutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetBookmark(c *gin.Context) {
+ var bookmarkParams db_repo.GetBookmarkTimestampParams
+
+ if err := c.BindJSON(&bookmarkParams); err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+
+ userId, err := getters.GetUserIdFromContext(c)
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+ bookmarkParams.UserID = userId
+
+ query := db_repo.New(db_connection.Dbx)
+ if timestamp, err := query.GetBookmarkTimestamp(context.Background(), bookmarkParams); err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ } else {
+ if timestamp.Valid {
+ c.IndentedJSON(http.StatusOK, gin.H{
+ "isBookmarked": timestamp.Valid,
+ "bookmarkedAt": timestamp.Time,
+ })
+ return
+ } else {
+ c.IndentedJSON(http.StatusOK, gin.H{
+ "isBookmarked": timestamp.Valid,
+ "bookmarkedAt": time.Unix(1<<63-1, 0).UTC(),
+ })
+ }
+ }
+}
diff --git a/enshi_back/routes/postsRoutes/getRandomPosts.go b/enshi_back/routes/postsRoutes/getRandomPosts.go
new file mode 100644
index 0000000..f8f2139
--- /dev/null
+++ b/enshi_back/routes/postsRoutes/getRandomPosts.go
@@ -0,0 +1,43 @@
+package postsRoutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "net/http"
+ "strconv"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetRandomPost(c *gin.Context) {
+ limit, err := strconv.Atoi(c.DefaultQuery("limit", "10"))
+
+ if err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ }
+
+ postsData, err :=
+ db_repo.New(db_connection.Dbx).
+ GetRandomPosts(context.Background(), int32(limit))
+
+ if err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ }
+
+ result := make([]any, 0)
+
+ for _, post := range postsData {
+ result = append(result, gin.H{
+ "post_id": strconv.Itoa(int(post.PostID)),
+ "title": post.Title,
+ "user_id": strconv.Itoa(int(post.UserID)),
+ })
+ }
+
+ c.IndentedJSON(http.StatusOK, result)
+
+}
diff --git a/enshi_back/routes/routesSetup.go b/enshi_back/routes/routesSetup.go
index 8978527..12afb8b 100644
--- a/enshi_back/routes/routesSetup.go
+++ b/enshi_back/routes/routesSetup.go
@@ -2,10 +2,14 @@ package routes
import (
"enshi/middleware"
+ "enshi/middleware/getters"
"enshi/routes/authRoutes"
"enshi/routes/blogRoutes"
+ bookmarksroutes "enshi/routes/bookmarksRoutes"
"enshi/routes/postsRoutes"
"enshi/routes/userProfileRoutes"
+ userroutes "enshi/routes/userRoutes"
+ voteroutes "enshi/routes/voteRoutes"
"net/http"
"strings"
@@ -21,6 +25,23 @@ func testAdmin(c *gin.Context) {
c.IndentedJSON(http.StatusOK, gin.H{"message": "you are an admin, congrats!"})
}
+func testAuth(c *gin.Context) {
+ userInfo, err := getters.GetClaimsFromContext(c)
+ if err != nil {
+ c.IndentedJSON(http.StatusUnauthorized, gin.H{"message": "you are not logged in"})
+
+ }
+ c.IndentedJSON(
+ http.StatusOK,
+ gin.H{
+ "message": "you are logged in, congrats!",
+ "username": userInfo.Username,
+ "is_admin": userInfo.IsAdmin,
+ "id": userInfo.Id,
+ },
+ )
+}
+
func SetupRotes(g *gin.Engine) error {
g.Use(middleware.CORSMiddleware())
@@ -45,6 +66,12 @@ func SetupRotes(g *gin.Engine) error {
"posts/:post-id",
postsRoutes.GetPost,
)
+
+ postsGroup.GET(
+ "posts/random",
+ postsRoutes.GetRandomPost,
+ )
+
postsGroup.PUT(
"posts/:post-id",
postsRoutes.UpdatePost,
@@ -97,11 +124,69 @@ func SetupRotes(g *gin.Engine) error {
userProfileRoutes.UpdateUserProfile,
)
+ bookmarksGroup := g.Group("/")
+ bookmarksGroup.Use(middleware.BookmarksMiddleware())
+
+ bookmarksGroup.POST(
+ "bookmarks/:post-id",
+ bookmarksroutes.CreateBookmark,
+ )
+
+ bookmarksGroup.DELETE(
+ "bookmarks/:post-id",
+ bookmarksroutes.DeleteBookmark,
+ )
+
+ bookmarksGroup.GET(
+ "bookmarks/:post-id",
+ bookmarksroutes.GetBookmark,
+ )
+
+ postVoteGroup := g.Group("/")
+ postVoteGroup.Use(middleware.PostVotesMiddleware())
+
+ postVoteGroup.POST(
+ "post-votes/:post-id",
+ voteroutes.CreateVote,
+ )
+
+ postVoteGroup.DELETE(
+ "post-votes/:post-id",
+ voteroutes.DeleteVote,
+ )
+
+ postVoteGroup.GET(
+ "post-vote/:post-id",
+ voteroutes.GetVote,
+ )
+
+ postVoteGroup.GET(
+ "post-votes/:post-id",
+ voteroutes.GetVotes,
+ )
+
// Admin group routes
adminGroup := g.Group("/admin/")
adminGroup.Use(middleware.AdminMiddleware())
- adminGroup.GET("testAdmin", testAdmin)
+ adminGroup.GET("check", testAdmin)
+
+ authGroup := g.Group("/auth/")
+ authGroup.Use(middleware.AuthMiddleware())
+ authGroup.GET("check", testAuth)
+
+ temporal := g.Group("/")
+ temporal.Use(middleware.AuthMiddleware())
+
+ temporal.GET(
+ "/user/blogs",
+ blogRoutes.GetUserBlogs,
+ )
+
+ freeGroup.GET(
+ "/user/:user-id",
+ userroutes.GetUserUsername,
+ )
return nil
}
diff --git a/enshi_back/routes/userRoutes/getUser.go b/enshi_back/routes/userRoutes/getUser.go
new file mode 100644
index 0000000..f12bf57
--- /dev/null
+++ b/enshi_back/routes/userRoutes/getUser.go
@@ -0,0 +1,30 @@
+package userroutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetUserUsername(c *gin.Context) {
+ userId, err := getters.GetInt64Param(c, "user-id")
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ }
+
+ userInfo, err := db_repo.New(db_connection.Dbx).GetUserUsernameById(
+ context.Background(),
+ userId,
+ )
+ if err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ }
+
+ c.IndentedJSON(http.StatusOK, userInfo)
+
+}
diff --git a/enshi_back/routes/voteRoutes/createVote.go b/enshi_back/routes/voteRoutes/createVote.go
new file mode 100644
index 0000000..037317c
--- /dev/null
+++ b/enshi_back/routes/voteRoutes/createVote.go
@@ -0,0 +1,42 @@
+package voteroutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+
+ "github.com/gin-gonic/gin"
+)
+
+func CreateVote(c *gin.Context) {
+ var postVoteParams db_repo.CreatePostVoteParams
+
+ if err := c.BindJSON(&postVoteParams); err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+
+ userId, err := getters.GetUserIdFromContext(c)
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+ postVoteParams.UserID = userId
+
+ postId, err := getters.GetInt64Param(c, "post-id")
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+ postVoteParams.PostID = postId
+
+ query := db_repo.New(db_connection.Dbx)
+ if _, err := query.CreatePostVote(context.Background(), postVoteParams); err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ }
+
+ rest_api_stuff.OkAnswer(c, "Vote has been created!")
+}
diff --git a/enshi_back/routes/voteRoutes/deleteVote.go b/enshi_back/routes/voteRoutes/deleteVote.go
new file mode 100644
index 0000000..baf1fdf
--- /dev/null
+++ b/enshi_back/routes/voteRoutes/deleteVote.go
@@ -0,0 +1,35 @@
+package voteroutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+
+ "github.com/gin-gonic/gin"
+)
+
+func DeleteVote(c *gin.Context) {
+ var postVoteParams db_repo.DeletePostVoteParams
+
+ if err := c.BindJSON(&postVoteParams); err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+
+ userId, err := getters.GetUserIdFromContext(c)
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+ postVoteParams.UserID = userId
+
+ query := db_repo.New(db_connection.Dbx)
+ if err := query.DeletePostVote(context.Background(), postVoteParams); err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ }
+
+ rest_api_stuff.OkAnswer(c, "Vote has been deleted!")
+}
diff --git a/enshi_back/routes/voteRoutes/getVote.go b/enshi_back/routes/voteRoutes/getVote.go
new file mode 100644
index 0000000..a949618
--- /dev/null
+++ b/enshi_back/routes/voteRoutes/getVote.go
@@ -0,0 +1,40 @@
+package voteroutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetVote(c *gin.Context) {
+ var postVoteParams db_repo.GetPostVoteParams
+
+ userId, err := getters.GetUserIdFromContext(c)
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+ postVoteParams.UserID = userId
+
+ postId, err := getters.GetInt64Param(c, "post-id")
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+ postVoteParams.PostID = postId
+
+ query := db_repo.New(db_connection.Dbx)
+ if voteData, err := query.GetPostVote(context.Background(), postVoteParams); err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ } else {
+ c.IndentedJSON(http.StatusOK, gin.H{
+ "vote": voteData,
+ })
+ }
+}
diff --git a/enshi_back/routes/voteRoutes/getVotes.go b/enshi_back/routes/voteRoutes/getVotes.go
new file mode 100644
index 0000000..a259e64
--- /dev/null
+++ b/enshi_back/routes/voteRoutes/getVotes.go
@@ -0,0 +1,29 @@
+package voteroutes
+
+import (
+ "context"
+ rest_api_stuff "enshi/REST_API_stuff"
+ db_repo "enshi/db/go_queries"
+ "enshi/db_connection"
+ "enshi/middleware/getters"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+func GetVotes(c *gin.Context) {
+ postId, err := getters.GetInt64Param(c, "post-id")
+
+ if err != nil {
+ rest_api_stuff.BadRequestAnswer(c, err)
+ return
+ }
+
+ query := db_repo.New(db_connection.Dbx)
+ if voteData, err := query.GetPostVotes(context.Background(), postId); err != nil {
+ rest_api_stuff.InternalErrorAnswer(c, err)
+ return
+ } else {
+ c.IndentedJSON(http.StatusOK, voteData)
+ }
+}
diff --git a/enshi_back/utils/cringe.go b/enshi_back/utils/cringe.go
new file mode 100644
index 0000000..a3e0952
--- /dev/null
+++ b/enshi_back/utils/cringe.go
@@ -0,0 +1,49 @@
+package utils
+
+import (
+ "fmt"
+ "reflect"
+)
+
+func ConvertInt64ToStringInStruct(input any) (any, error) {
+ origVal := reflect.ValueOf(input)
+
+ // Ensure input is a struct or pointer to a struct
+ if origVal.Kind() == reflect.Ptr {
+ origVal = origVal.Elem()
+ }
+ if origVal.Kind() != reflect.Struct {
+ return nil, fmt.Errorf("input must be a struct or a pointer to a struct")
+ }
+
+ // Create a new instance of the same type
+ newStruct := reflect.New(origVal.Type()).Elem()
+
+ // Iterate through fields
+ for i := 0; i < origVal.NumField(); i++ {
+ field := origVal.Field(i)
+ newField := newStruct.Field(i)
+
+ if !newField.CanSet() {
+ // Skip unexported fields
+ continue
+ }
+
+ switch field.Kind() {
+ case reflect.Int64:
+ // Convert int64 fields to string if the target is compatible
+ // if newField.Kind() == reflect.Int64 {
+ // fmt.Print("aqwrqfwq", field)
+ // newField.Set(strconv.FormatInt(field.Int(), 10))
+ // }
+ newField.SetString("asd")
+ default:
+ // Copy other fields directly
+ if newField.Type() == field.Type() {
+ newField.Set(field)
+ }
+ }
+ }
+
+ return newStruct.Interface(), nil
+}
diff --git a/package-lock.json b/package-lock.json
index bae962a..1477912 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,7 +5,113 @@
"packages": {
"": {
"dependencies": {
- "@radix-ui/react-icons": "^1.3.2"
+ "@radix-ui/react-icons": "^1.3.2",
+ "@radix-ui/react-toast": "^1.2.2",
+ "interweave": "^13.1.0"
+ }
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
+ "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-collection": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
+ "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
+ "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
+ "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
+ "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-escape-keydown": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
}
},
"node_modules/@radix-ui/react-icons": {
@@ -17,6 +123,240 @@
"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
}
},
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
+ "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
+ "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
+ "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-toast": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz",
+ "integrity": "sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.0",
+ "@radix-ui/react-collection": "1.1.0",
+ "@radix-ui/react-compose-refs": "1.1.0",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.1",
+ "@radix-ui/react-portal": "1.1.2",
+ "@radix-ui/react-presence": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.0",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-visually-hidden": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+ "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
+ "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
+ "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+ "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz",
+ "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/interweave": {
+ "version": "13.1.0",
+ "resolved": "https://registry.npmjs.org/interweave/-/interweave-13.1.0.tgz",
+ "integrity": "sha512-JIDq0+2NYg0cgL7AB26fBcV0yZdiJvPDBp+aF6k8gq6Cr1kH5Gd2/Xqn7j8z+TGb8jCWZn739jzalCz+nPYwcA==",
+ "license": "MIT",
+ "dependencies": {
+ "escape-html": "^1.0.3"
+ },
+ "funding": {
+ "type": "ko-fi",
+ "url": "https://ko-fi.com/milesjohnson"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -49,6 +389,30 @@
"engines": {
"node": ">=0.10.0"
}
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
}
}
}
diff --git a/package.json b/package.json
index cc5e205..5586dfe 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,7 @@
{
"dependencies": {
- "@radix-ui/react-icons": "^1.3.2"
+ "@radix-ui/react-icons": "^1.3.2",
+ "@radix-ui/react-toast": "^1.2.2",
+ "interweave": "^13.1.0"
}
}