From 184a47c4812ecfbb8d8c41ca022dcf5059a32ea5 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 19 Nov 2024 23:00:50 +0300 Subject: [PATCH 01/12] Start work o this --- .../LoginElement/LoginElement.tsx | 165 ++++++++++++++++++ .../LoginRegisterPage/LoginRegisterPage.tsx | 10 ++ .../ShowPasswordButton/ShowPasswordButton.tsx | 40 +++++ enshi/src/routes/routes.tsx | 15 +- 4 files changed, 227 insertions(+), 3 deletions(-) create mode 100644 enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx create mode 100644 enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx create mode 100644 enshi/src/Pages/LoginRegisterPage/ShowPasswordButton/ShowPasswordButton.tsx diff --git a/enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx b/enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx new file mode 100644 index 0000000..650fe85 --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx @@ -0,0 +1,165 @@ +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 { useEffect, useState } from "react"; +import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton"; + +export default function LoginElement() { + const [showPassword, setShowPassword] = useState(false); + 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 ( + + + Log in form + + + +
+ + Username + + + Please enter your username + +
+ + + + {(validity) => ( + + + + )} + + + +
+ + +
+ + Password + + + Please enter your password + +
+ + + + {(validity) => ( + + + + )} + + + + +
+ + {/* +
+ + Confirm password + + + Please enter your password + + + value !== formData.get("password") + } + > + Passwords must be the same + +
+ + + + {(validity) => ( + + + + )} + + + +
*/} + + + + +
+
+ ); +} diff --git a/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx b/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx new file mode 100644 index 0000000..ebe5a7a --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx @@ -0,0 +1,10 @@ +import { useState } from "react"; +import LoginElement from "./LoginElement/LoginElement"; + +export default function LoginRegisterPage() { + const [isRegister, setIsRegister] = useState(false) + + return ( + + ) +} \ No newline at end of file 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/routes/routes.tsx b/enshi/src/routes/routes.tsx index 8331bbb..06d4356 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -1,6 +1,7 @@ -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, Route, useRouteError } from "react-router-dom"; +import LoginRegisterPage from "../Pages/LoginRegisterPage/LoginRegisterPage"; +import MainPage from "../Pages/MainPage/MainPage"; function ErrorBoundary() { @@ -23,5 +24,13 @@ export const routes = createRoutesFromElements( element={Cringer path, but this a} > + + } + element={} + > + + ) \ No newline at end of file From 418947022b62aafb49404b041d9fc7191208c79f Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 20 Nov 2024 21:30:01 +0300 Subject: [PATCH 02/12] Localization | registration --- .../LoginElement/LoginElement.tsx | 128 +++++----- .../LoginRegisterPage/LoginRegisterPage.tsx | 12 +- .../RegisterElement/RegisterElement.tsx | 241 ++++++++++++++++++ enshi/src/api/axios/axios.ts | 2 +- enshi/src/hooks/useCapsLock.tsx | 27 ++ enshi/src/locale/en.ts | 24 +- enshi/src/locale/ru.ts | 25 +- enshi/src/routes/routes.tsx | 12 +- enshi_back/global/globalVars.go | 2 +- enshi_back/middleware/corsMiddleware.go | 2 +- 10 files changed, 390 insertions(+), 85 deletions(-) create mode 100644 enshi/src/Pages/LoginRegisterPage/RegisterElement/RegisterElement.tsx create mode 100644 enshi/src/hooks/useCapsLock.tsx diff --git a/enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx b/enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx index 650fe85..3b00098 100644 --- a/enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx +++ b/enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx @@ -1,28 +1,43 @@ 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 { useEffect, useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { t } from "i18next"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { axiosLocalhost } from "../../../api/axios/axios"; +import UseCapsLock from "../../../hooks/useCapsLock"; import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton"; +type TLoginData = { + username: string; + password: string; +}; + export default function LoginElement() { const [showPassword, setShowPassword] = useState(false); - const [isCapsLockOn, setIsCapsLockOn] = useState(false); + const { isCapsLockOn } = UseCapsLock(); + const [isError, setIsError] = useState(false); - useEffect(() => { - const f = (e: KeyboardEvent) => { - if (e.getModifierState("CapsLock")) { - setIsCapsLockOn(true); - } else { - setIsCapsLockOn(false); - } - }; + const navigate = useNavigate() - document.addEventListener("keydown", f); + const logInMutation = useMutation({ + mutationFn: async (data: TLoginData) => { + await axiosLocalhost.post( + "/login", + JSON.stringify(data) + ); + }, - return () => { - document.removeEventListener("keydown", f); - }; - }, []); + onError: (error, _variables, _context) => { + console.log(error); + setIsError(true); + }, + + onSuccess: () => { + navigate("/") + }, + }); return ( - Log in form + {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); + }} + >
- Username + {t("username")} - Please enter your username + {t("errors.enterUsername")}
@@ -70,10 +99,10 @@ export default function LoginElement() {
- Password + {t("password")} - Please enter your password + {t("errors.enterPassword")}
@@ -103,60 +132,17 @@ export default function LoginElement() {
- {/* -
- - Confirm password - - - Please enter your password - - - value !== formData.get("password") - } - > - Passwords must be the same - -
- - - - {(validity) => ( - - - - )} - - - -
*/} + - -
diff --git a/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx b/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx index ebe5a7a..ade733c 100644 --- a/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx +++ b/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx @@ -1,10 +1,16 @@ -import { useState } from "react"; import LoginElement from "./LoginElement/LoginElement"; +import RegisterElement from "./RegisterElement/RegisterElement"; -export default function LoginRegisterPage() { - const [isRegister, setIsRegister] = useState(false) +export function LoginPage() { return ( ) +} + +export function RegisterPage() { + + return ( + + ) } \ No newline at end of file diff --git a/enshi/src/Pages/LoginRegisterPage/RegisterElement/RegisterElement.tsx b/enshi/src/Pages/LoginRegisterPage/RegisterElement/RegisterElement.tsx new file mode 100644 index 0000000..21c3f62 --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/RegisterElement/RegisterElement.tsx @@ -0,0 +1,241 @@ +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 { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { axiosLocalhost } from "../../../api/axios/axios"; +import UseCapsLock from "../../../hooks/useCapsLock"; +import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton"; + +type TRegisterData = { + username: string; + password: string; + email: string; +}; + +export default function RegisterElement() { + 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) => { + await axiosLocalhost.post("/users", JSON.stringify(data)); + }, + + 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("confirmPassword")} + + + {t("errors.enterPassword")} + + + value !== formData.get("password") + } + > + + {t("errors.passwordsMismatch")} + + +
+ + + + {(validity) => ( + + + + )} + + + + +
+ + + + + + +
+
+ ); +} 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/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/locale/en.ts b/enshi/src/locale/en.ts index bd15612..c955807 100644 --- a/enshi/src/locale/en.ts +++ b/enshi/src/locale/en.ts @@ -1,6 +1,26 @@ 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 + capsLogWarning: "CapsLock is on", + + registerForm: "Register", + loginForm: "Log in", + + 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", + }, +}; + +export default en; diff --git a/enshi/src/locale/ru.ts b/enshi/src/locale/ru.ts index abba300..7d2cddc 100644 --- a/enshi/src/locale/ru.ts +++ b/enshi/src/locale/ru.ts @@ -1,6 +1,27 @@ const ru = { hello: "Привет!", search: "Поиск...", -} + username: "Имя пользователя", + email: "Электронная почта", + password: "Пароль", + confirmPassword: "Подтвердите пароль", + submit: "Подтвердить", -export default ru; \ No newline at end of file + capsLogWarning: "Включён CapsLock", + + registerForm: "Регистрация", + loginForm: "Вход", + + errors: { + enterUsername: "Пожалуйста, введите ваше имя пользователя", + enterEmail: "Пожалуйста, введите свой адрес электронной почты", + invalidEmail: "Пожалуйста, введите корректный адрес электронной почты", + enterPassword: "Пожалуйста, введите пароль", + passwordsMismatch: "Пароли должны быть одинаковыми", + invalidLoginData: "Неверное имя пользователя или пароль", + invalidRegisterData: + "Пользователь с таким адресом электронной почты или именем пользователя уже существует", + }, +}; + +export default ru; diff --git a/enshi/src/routes/routes.tsx b/enshi/src/routes/routes.tsx index 06d4356..db1bc25 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -1,6 +1,6 @@ import { Text } from "@radix-ui/themes"; import { createRoutesFromElements, Route, useRouteError } from "react-router-dom"; -import LoginRegisterPage from "../Pages/LoginRegisterPage/LoginRegisterPage"; +import { LoginPage, RegisterPage } from "../Pages/LoginRegisterPage/LoginRegisterPage"; import MainPage from "../Pages/MainPage/MainPage"; @@ -28,9 +28,13 @@ export const routes = createRoutesFromElements( } - element={} - > + element={} + /> - + } + element={} + /> ) \ No newline at end of file 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/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", From be99c53c6950e6477ec84a703d6873a2a1eb7320 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 21 Nov 2024 21:18:53 +0300 Subject: [PATCH 03/12] Localization | edit page --- enshi/package-lock.json | 42 ++++++++++-- enshi/package.json | 1 + enshi/src/App.tsx | 14 ++-- enshi/src/Components/Editor/Editor.tsx | 15 ++--- enshi/src/Components/NavBar/NavBar.tsx | 13 ++-- .../CreatePostButton/CreatePostButton.tsx | 14 ++++ .../NavBar/RightButtonBar/RightButtonBar.tsx | 12 ++++ .../UserButton/UserButton.tsx | 14 ++-- .../NavBar/SearchField/SearchField.tsx | 4 +- .../LoginPage.tsx} | 30 +++++++-- .../LoginRegisterPage/LoginRegisterPage.tsx | 16 ----- .../RegisterPage.tsx} | 11 +++- enshi/src/Pages/MainPage/MainPage.tsx | 64 +++++++++++++++---- .../Pages/PostCreatorPage/PostCreatorPage.tsx | 36 +++++++++++ enshi/src/locale/en.ts | 9 +++ enshi/src/locale/ru.ts | 10 +++ enshi/src/routes/routes.tsx | 30 ++++++--- enshi_back/middleware/authMiddleware.go | 10 --- enshi_back/routes/authRoutes/login.go | 2 +- enshi_back/routes/authRoutes/registerUser.go | 3 +- enshi_back/routes/routesSetup.go | 23 ++++++- 21 files changed, 275 insertions(+), 98 deletions(-) create mode 100644 enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx create mode 100644 enshi/src/Components/NavBar/RightButtonBar/RightButtonBar.tsx rename enshi/src/Components/NavBar/{ => RightButtonBar}/UserButton/UserButton.tsx (84%) rename enshi/src/Pages/LoginRegisterPage/{LoginElement/LoginElement.tsx => LoginPage/LoginPage.tsx} (87%) delete mode 100644 enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx rename enshi/src/Pages/LoginRegisterPage/{RegisterElement/RegisterElement.tsx => RegisterPage/RegisterPage.tsx} (96%) create mode 100644 enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx diff --git a/enshi/package-lock.json b/enshi/package-lock.json index 641fb3f..b0afd08 100644 --- a/enshi/package-lock.json +++ b/enshi/package-lock.json @@ -14,6 +14,7 @@ "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/themes": "^3.1.3", "@tanstack/react-query": "^5.55.0", + "@tanstack/react-query-devtools": "^5.61.0", "axios": "^1.7.7", "html-react-parser": "^5.1.16", "i18n": "^0.15.1", @@ -2808,9 +2809,19 @@ ] }, "node_modules/@tanstack/query-core": { - "version": "5.54.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.54.1.tgz", - "integrity": "sha512-hKS+WRpT5zBFip21pB6Jx1C0hranWQrbv5EJ7qPoiV5MYI3C8rTCqWC9DdBseiPT1JgQWh8Y55YthuYZNiw3Xw==", + "version": "5.60.6", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.6.tgz", + "integrity": "sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.59.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.59.20.tgz", + "integrity": "sha512-vxhuQ+8VV4YWQSFxQLsuM+dnEKRY7VeRzpNabFXdhEwsBYLrjXlF1pM38A8WyKNLqZy8JjyRO8oP4Wd/oKHwuQ==", "license": "MIT", "funding": { "type": "github", @@ -2818,12 +2829,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.55.0", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.55.0.tgz", - "integrity": "sha512-2uYuxEbRQD8TORUiTUacEOwt1e8aoSqUOJFGY5TUrh6rQ3U85zrMS2wvbNhBhXGh6Vj69QDCP2yv8tIY7joo6Q==", + "version": "5.61.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.61.0.tgz", + "integrity": "sha512-SBzV27XAeCRBOQ8QcC94w2H1Md0+LI0gTWwc3qRJoaGuewKn5FNW4LSqwPFJZVEItfhMfGT7RpZuSFXjTi12pQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.54.1" + "@tanstack/query-core": "5.60.6" }, "funding": { "type": "github", @@ -2833,6 +2844,23 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.61.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.61.0.tgz", + "integrity": "sha512-hd3yXl+KV+OGQmAw946qHAFp6DygcXcYN+1ai9idYddx6uEQyCwYk3jyIBOQEUw9uzN5DOGJLBsgd/QcimDQsA==", + "license": "MIT", + "dependencies": { + "@tanstack/query-devtools": "5.59.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.61.0", + "react": "^18 || ^19" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", diff --git a/enshi/package.json b/enshi/package.json index 9047b25..f50f2c9 100644 --- a/enshi/package.json +++ b/enshi/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/themes": "^3.1.3", "@tanstack/react-query": "^5.55.0", + "@tanstack/react-query-devtools": "^5.61.0", "axios": "^1.7.7", "html-react-parser": "^5.1.16", "i18n": "^0.15.1", diff --git a/enshi/src/App.tsx b/enshi/src/App.tsx index f840c5e..7d8c2ba 100644 --- a/enshi/src/App.tsx +++ b/enshi/src/App.tsx @@ -1,11 +1,12 @@ -import "./App.css"; +import { Theme } from "@radix-ui/themes"; import "@radix-ui/themes/styles.css"; -import { Theme, ThemePanel } from "@radix-ui/themes"; -import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { QueryClientProvider } from "@tanstack/react-query"; -import queryClient from "./api/QueryClient/QueryClient"; -import { routes } from "./routes/routes"; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import "axios"; +import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import queryClient from "./api/QueryClient/QueryClient"; +import "./App.css"; +import { routes } from "./routes/routes"; const router = createBrowserRouter(routes); @@ -14,7 +15,8 @@ export default function App() { - + {/* */} + ); diff --git a/enshi/src/Components/Editor/Editor.tsx b/enshi/src/Components/Editor/Editor.tsx index d9e01a7..d6be522 100644 --- a/enshi/src/Components/Editor/Editor.tsx +++ b/enshi/src/Components/Editor/Editor.tsx @@ -1,19 +1,18 @@ -import Quill, { Delta, } from "quill/core"; -import ReactQuill from "react-quill"; -import React, { +import Sources from "quill"; +import Quill, { Delta, } from "quill/core"; +import { forwardRef, useEffect, - useLayoutEffect, useRef, - useState, + useState } from "react"; -import Sources from "quill"; +import ReactQuill from "react-quill"; type TEditor = { readOnly?: boolean; defaultValue?: string | Delta; - onChange: (d: string) => void; // TODO: make type - onSelectionChange?: any; // TODO same as before + onChange: (d: string) => void; + onSelectionChange?: any; }; const modules = { diff --git a/enshi/src/Components/NavBar/NavBar.tsx b/enshi/src/Components/NavBar/NavBar.tsx index bd60301..4cca591 100644 --- a/enshi/src/Components/NavBar/NavBar.tsx +++ b/enshi/src/Components/NavBar/NavBar.tsx @@ -1,20 +1,17 @@ -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/RightButtonBar/CreatePostButton/CreatePostButton.tsx b/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx new file mode 100644 index 0000000..f9993bd --- /dev/null +++ b/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx @@ -0,0 +1,14 @@ +import { PlusIcon } from "@radix-ui/react-icons"; +import { Button, Text } from "@radix-ui/themes"; +import { useTranslation } from "react-i18next"; + +export default function CreatePostButton() { + const {t} = useTranslation() + + return ( + + ); +} 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 84% rename from enshi/src/Components/NavBar/UserButton/UserButton.tsx rename to enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx index c9c1868..3a8cbf1 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 ( -
+
@@ -30,7 +32,7 @@ export default function UserButton() { - Profile + {t("profile")} @@ -40,7 +42,7 @@ export default function UserButton() { - Your blogs + {t("yourBlogs")} @@ -52,14 +54,14 @@ 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/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx similarity index 87% rename from enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx rename to enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx index 3b00098..8d166d0 100644 --- a/enshi/src/Pages/LoginRegisterPage/LoginElement/LoginElement.tsx +++ b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx @@ -3,9 +3,11 @@ 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 { 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"; @@ -14,19 +16,21 @@ type TLoginData = { password: string; }; -export default function LoginElement() { +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 navigate = useNavigate(); const logInMutation = useMutation({ mutationFn: async (data: TLoginData) => { - await axiosLocalhost.post( - "/login", - JSON.stringify(data) - ); + let response = await axiosLocalhost.post("/login", JSON.stringify(data)); + setUserAtom({ + username: response.data.username, + isAdmin: false + }) }, onError: (error, _variables, _context) => { @@ -35,7 +39,19 @@ export default function LoginElement() { }, onSuccess: () => { - navigate("/") + let isAdminFunc = async () => { + let response = await axiosLocalhost.get("/admin/check"); + if (response.status === 200) { + setUserAtom({ + username: userAtomValue?.username || "", + isAdmin: true + }) + } + }; + + isAdminFunc(); + + navigate("/"); }, }); diff --git a/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx b/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx deleted file mode 100644 index ade733c..0000000 --- a/enshi/src/Pages/LoginRegisterPage/LoginRegisterPage.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import LoginElement from "./LoginElement/LoginElement"; -import RegisterElement from "./RegisterElement/RegisterElement"; - -export function LoginPage() { - - return ( - - ) -} - -export function RegisterPage() { - - return ( - - ) -} \ No newline at end of file diff --git a/enshi/src/Pages/LoginRegisterPage/RegisterElement/RegisterElement.tsx b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx similarity index 96% rename from enshi/src/Pages/LoginRegisterPage/RegisterElement/RegisterElement.tsx rename to enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx index 21c3f62..dcf8914 100644 --- a/enshi/src/Pages/LoginRegisterPage/RegisterElement/RegisterElement.tsx +++ b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx @@ -2,10 +2,12 @@ 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 { 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"; @@ -15,7 +17,8 @@ type TRegisterData = { email: string; }; -export default function RegisterElement() { +export default function RegisterPage() { + const setUserAtom = useSetAtom(userAtom) const [showPassword, setShowPassword] = useState(false); const [showConfPassword, setShowConfPassword] = useState(false); const { isCapsLockOn } = UseCapsLock(); @@ -28,7 +31,11 @@ export default function RegisterElement() { const registerMutation = useMutation({ mutationFn: async (data: TRegisterData) => { - await axiosLocalhost.post("/users", JSON.stringify(data)); + let response = await axiosLocalhost.post("/users", JSON.stringify(data)); + setUserAtom({ + username: response.data.username, + isAdmin: false + }) }, onError: (error, _variables, _context) => { diff --git a/enshi/src/Pages/MainPage/MainPage.tsx b/enshi/src/Pages/MainPage/MainPage.tsx index 905549a..b98c8a6 100644 --- a/enshi/src/Pages/MainPage/MainPage.tsx +++ b/enshi/src/Pages/MainPage/MainPage.tsx @@ -1,22 +1,60 @@ -import React from "react"; +import { Spinner } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { useSetAtom } from "jotai"; import { Outlet } from "react-router-dom"; -import NavBar from "../../Components/NavBar/NavBar"; import { axiosLocalhost } from "../../api/axios/axios"; -import { Container } from "@radix-ui/themes"; +import { userAtom } from "../../AtomStore/AtomStore"; +import NavBar from "../../Components/NavBar/NavBar"; + +const REFETCH_INTERVAL_IN_MINUTES = 5; +const RETRY_INTERVAL_IN_SECONDS = 3; + +const SECONDS_IN_MINUTE = 60; +const MILLS_IN_SECOND = 1000; export default function MainPage() { + const setUserData = useSetAtom(userAtom); + + const { isPending } = useQuery({ + queryKey: ["authKey"], + queryFn: async () => { + const response = await axiosLocalhost.get("/auth/check"); + if (response.status === 200) { + setUserData({ + isAdmin: response.data["is_admin"], + username: response.data["username"], + }); + return true; + } else { + setUserData(undefined); + return false; + } + }, + refetchInterval: + REFETCH_INTERVAL_IN_MINUTES * SECONDS_IN_MINUTE * MILLS_IN_SECOND, + refetchOnWindowFocus: true, + refetchOnReconnect: true, + + retry: 3, + retryDelay: (attempt) => + attempt * RETRY_INTERVAL_IN_SECONDS * MILLS_IN_SECOND, + }); + return ( <> - - - + {isPending ? ( +
+ +
+ ) : ( + <> + + + + )} ); } diff --git a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx new file mode 100644 index 0000000..f2aa2b5 --- /dev/null +++ b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx @@ -0,0 +1,36 @@ +import { Container, Text } from "@radix-ui/themes"; +import { useAtomValue } from "jotai"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { userAtom } from "../../AtomStore/AtomStore"; +import Editor from "../../Components/Editor/Editor"; + +export default function PostCreatorPage() { + const user = useAtomValue(userAtom); + + const [userInput, setUserInput] = useState(""); + + const { t } = useTranslation(); + + useEffect(() => { + console.log("asdasd", userInput); + + }, [userInput]) + + + if (!user) { + return ( + + {t("errors.unauthorized")} + + ); + } + + return ( + <> + + + + + ); +} diff --git a/enshi/src/locale/en.ts b/enshi/src/locale/en.ts index c955807..1a22750 100644 --- a/enshi/src/locale/en.ts +++ b/enshi/src/locale/en.ts @@ -7,6 +7,14 @@ const en = { confirmPassword: "Confirm password", submit: "Submit", + createPost: "Write post", + + profile: "Profile", + yourBlogs: "Your blogs", + + signIn: "Log in", + signOut: "Sign out", + capsLogWarning: "CapsLock is on", registerForm: "Register", @@ -20,6 +28,7 @@ const en = { passwordsMismatch: "Passwords must be the same", invalidLoginData: "Invalid username or password", invalidRegisterData: "Invalid register data", + unauthorized: "You need to be authorized to do that", }, }; diff --git a/enshi/src/locale/ru.ts b/enshi/src/locale/ru.ts index 7d2cddc..15b7f2e 100644 --- a/enshi/src/locale/ru.ts +++ b/enshi/src/locale/ru.ts @@ -7,10 +7,19 @@ const ru = { confirmPassword: "Подтвердите пароль", submit: "Подтвердить", + createPost: "Написать пост", + + profile: "Профиль", + yourBlogs: "Ваши блоги", + + signIn: "Войти", + signOut: "Выйти", + capsLogWarning: "Включён CapsLock", registerForm: "Регистрация", loginForm: "Вход", + errors: { enterUsername: "Пожалуйста, введите ваше имя пользователя", @@ -21,6 +30,7 @@ const ru = { invalidLoginData: "Неверное имя пользователя или пароль", invalidRegisterData: "Пользователь с таким адресом электронной почты или именем пользователя уже существует", + unauthorized: "Вы должны быть авторизованы, чтобы сделать это", }, }; diff --git a/enshi/src/routes/routes.tsx b/enshi/src/routes/routes.tsx index db1bc25..02bb0f2 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -1,8 +1,13 @@ import { Text } from "@radix-ui/themes"; -import { createRoutesFromElements, Route, useRouteError } from "react-router-dom"; -import { LoginPage, RegisterPage } from "../Pages/LoginRegisterPage/LoginRegisterPage"; +import { + createRoutesFromElements, + Route, + useRouteError, +} from "react-router-dom"; +import LoginPage from "../Pages/LoginRegisterPage/LoginPage/LoginPage"; +import RegisterPage from "../Pages/LoginRegisterPage/RegisterPage/RegisterPage"; import MainPage from "../Pages/MainPage/MainPage"; - +import PostCreatorPage from "../Pages/PostCreatorPage/PostCreatorPage"; function ErrorBoundary() { let error = useRouteError(); @@ -13,15 +18,20 @@ function ErrorBoundary() { export const routes = createRoutesFromElements( <> - } - element={} - > + } element={}> Cringer path} /> Cringer path, but this a} + element={ + Cringer path, but this a + } + > + + + } > @@ -37,4 +47,4 @@ export const routes = createRoutesFromElements( element={} /> -) \ No newline at end of file +); diff --git a/enshi_back/middleware/authMiddleware.go b/enshi_back/middleware/authMiddleware.go index 201f08f..c584354 100644 --- a/enshi_back/middleware/authMiddleware.go +++ b/enshi_back/middleware/authMiddleware.go @@ -11,8 +11,6 @@ import ( func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - // token := c.GetHeader("Authorization") - tokenFromCookies := c.Request.CookiesNamed("auth_cookie")[0].Value cookieClimes, err := auth.ValidateToken(tokenFromCookies) if err != nil { @@ -21,14 +19,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/routes/authRoutes/login.go b/enshi_back/routes/authRoutes/login.go index 212cb43..cec28b0 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}) } diff --git a/enshi_back/routes/authRoutes/registerUser.go b/enshi_back/routes/authRoutes/registerUser.go index 79b994f..0e75e75 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}) } diff --git a/enshi_back/routes/routesSetup.go b/enshi_back/routes/routesSetup.go index 8978527..9299bf4 100644 --- a/enshi_back/routes/routesSetup.go +++ b/enshi_back/routes/routesSetup.go @@ -2,6 +2,7 @@ package routes import ( "enshi/middleware" + "enshi/middleware/getters" "enshi/routes/authRoutes" "enshi/routes/blogRoutes" "enshi/routes/postsRoutes" @@ -21,6 +22,22 @@ 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, + }, + ) +} + func SetupRotes(g *gin.Engine) error { g.Use(middleware.CORSMiddleware()) @@ -101,7 +118,11 @@ func SetupRotes(g *gin.Engine) error { 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) return nil } From 7c38c6ca9bd4139bf1ed6165efbdfa9f9a54a050 Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 23 Nov 2024 20:49:12 +0300 Subject: [PATCH 04/12] Localization | edit page --- enshi/package-lock.json | 22 +++++++++++ enshi/package.json | 2 + enshi/src/App.css | 4 ++ .../ArticleViewer/ArticleViewer.tsx | 12 +++++- enshi/src/Components/Editor/Editor.tsx | 28 +++++++++++++- .../CreatePostButton/CreatePostButton.tsx | 13 ++++--- .../RightButtonBar/UserButton/UserButton.tsx | 32 +++++++++------- .../Pages/AuthPageWrapper/AuthPageWrapper.tsx | 22 +++++++++++ .../LoginRegisterPage/LoginPage/LoginPage.tsx | 37 +++++++++++++------ .../RegisterPage/RegisterPage.tsx | 13 ++++--- enshi/src/Pages/MainPage/MainPage.tsx | 10 +++-- .../Pages/PostCreatorPage/PostCreatorPage.tsx | 30 +++++---------- enshi/src/index.css | 5 +++ enshi/src/locale/en.ts | 4 ++ enshi/src/locale/ru.ts | 10 +++-- enshi/src/main.tsx | 2 +- enshi/src/routes/routes.tsx | 5 ++- enshi/tailwind.config.js | 3 ++ enshi_back/middleware/authMiddleware.go | 11 +++++- package-lock.json | 25 ++++++++++++- package.json | 3 +- 21 files changed, 220 insertions(+), 73 deletions(-) create mode 100644 enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx diff --git a/enshi/package-lock.json b/enshi/package-lock.json index b0afd08..5ae82a9 100644 --- a/enshi/package-lock.json +++ b/enshi/package-lock.json @@ -20,7 +20,9 @@ "i18n": "^0.15.1", "i18next": "^23.14.0", "i18next-browser-languagedetector": "^8.0.0", + "immer": "^10.1.1", "jotai": "^2.9.3", + "jotai-immer": "^0.4.1", "primereact": "^10.8.2", "quill": "^2.0.2", "react": "^18.3.1", @@ -4846,6 +4848,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -5071,6 +5083,16 @@ } } }, + "node_modules/jotai-immer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/jotai-immer/-/jotai-immer-0.4.1.tgz", + "integrity": "sha512-nQTt1HBKie/5OJDck1qLpV1PeBA6bjJLAczEYAx70PD8R4Mbu7gtexfBUCzJh6W6ecsOfwHksAYAesVth6SN9A==", + "license": "MIT", + "peerDependencies": { + "immer": ">=9.0.0", + "jotai": ">=2.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/enshi/package.json b/enshi/package.json index f50f2c9..8f48f0b 100644 --- a/enshi/package.json +++ b/enshi/package.json @@ -22,7 +22,9 @@ "i18n": "^0.15.1", "i18next": "^23.14.0", "i18next-browser-languagedetector": "^8.0.0", + "immer": "^10.1.1", "jotai": "^2.9.3", + "jotai-immer": "^0.4.1", "primereact": "^10.8.2", "quill": "^2.0.2", "react": "^18.3.1", diff --git a/enshi/src/App.css b/enshi/src/App.css index 5ca1f02..73de149 100644 --- a/enshi/src/App.css +++ b/enshi/src/App.css @@ -19,6 +19,10 @@ text-transform: uppercase; } + * { + font-family: "Times New Roman"; + } + /*! * Quill Editor v1.3.6 * https://quilljs.com/ diff --git a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx index ac30298..511968a 100644 --- a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx +++ b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx @@ -1,11 +1,19 @@ import { Container } from "@radix-ui/themes"; -import React from "react"; +import { Interweave } from "interweave"; +import { useParams } from "react-router-dom"; + +type TArticleViewer = { + htmlToParse?: string; +} + +export default function ArticleViewer(props: TArticleViewer) { + const queryPapms = useParams() -export default function ArticleViewer() { return ( <>
+
diff --git a/enshi/src/Components/Editor/Editor.tsx b/enshi/src/Components/Editor/Editor.tsx index d6be522..9c92d3c 100644 --- a/enshi/src/Components/Editor/Editor.tsx +++ b/enshi/src/Components/Editor/Editor.tsx @@ -1,5 +1,5 @@ import Sources from "quill"; -import Quill, { Delta, } from "quill/core"; +import Quill, { Delta } from "quill/core"; import { forwardRef, useEffect, @@ -18,7 +18,7 @@ type TEditor = { const modules = { toolbar: [ [{ header: [1, 2, 3, false] }], - ["bold", "italic", "underline", "strike", "blockquote"], + ["bold", "italic", "underline", "strike", "blockquote", "span-wrapper"], [ { list: "ordered" }, { list: "bullet" }, @@ -29,6 +29,9 @@ const modules = { ["clean"], [{ align: [] }], ], + clipboard: { + matchVisual: true, + }, }; /** @@ -50,6 +53,26 @@ const Editor = forwardRef((props: TEditor) => { }; }, [editor.current]); + useEffect(() => { + let Inline = Quill.import('attributors/style/size'); + console.log(Inline); + + // //@ts-ignore + // class BoldBlot extends Inline {} + // //@ts-ignore + // BoldBlot.blotName = 'bold1'; + // //@ts-ignore + // BoldBlot.tagName = 'strong'; + // console.log(BoldBlot, true); + + + Quill.register(Inline as any, true); + return () => { + + } + }, []) + + const changeHandler = (val: string, _changeDelta: Delta, _source: Sources, _editor: ReactQuill.UnprivilegedEditor) => { console.log(val); console.log(JSON.stringify(quill?.getContents().ops, null, 2)) @@ -64,6 +87,7 @@ const Editor = forwardRef((props: TEditor) => { value={value} ref={editor} modules={modules} + formats={['bold1']} onChange={changeHandler} diff --git a/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx b/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx index f9993bd..eaaaaa6 100644 --- a/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx +++ b/enshi/src/Components/NavBar/RightButtonBar/CreatePostButton/CreatePostButton.tsx @@ -1,14 +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() + const { t } = useTranslation(); return ( - + + + ); } diff --git a/enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx b/enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx index 3a8cbf1..ce9ca61 100644 --- a/enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx +++ b/enshi/src/Components/NavBar/RightButtonBar/UserButton/UserButton.tsx @@ -13,7 +13,7 @@ import { userAtom } from "../../../../AtomStore/AtomStore"; export default function UserButton() { const user = useAtomValue(userAtom); - const {t} = useTranslation() + const { t } = useTranslation(); return (
@@ -26,7 +26,7 @@ export default function UserButton() { - + @@ -38,12 +38,14 @@ export default function UserButton() { - - - - - {t("yourBlogs")} - + + + + + + {t("yourBlogs")} + + @@ -57,12 +59,14 @@ export default function UserButton() { {t("signOut")} ) : ( - - - - - {t("signIn")} - + + + + + + {t("signIn")} + + )} diff --git a/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx b/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx new file mode 100644 index 0000000..9f6ced6 --- /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/LoginRegisterPage/LoginPage/LoginPage.tsx b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx index 8d166d0..7835c87 100644 --- a/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx +++ b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx @@ -5,7 +5,7 @@ import { useMutation } from "@tanstack/react-query"; import { t } from "i18next"; import { useAtom } from "jotai"; import { useState } from "react"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { axiosLocalhost } from "../../../api/axios/axios"; import { userAtom } from "../../../AtomStore/AtomStore"; import UseCapsLock from "../../../hooks/useCapsLock"; @@ -17,7 +17,7 @@ type TLoginData = { }; export default function LoginPage() { - const [userAtomValue, setUserAtom] = useAtom(userAtom) + const [userAtomValue, setUserAtom] = useAtom(userAtom); const [showPassword, setShowPassword] = useState(false); const { isCapsLockOn } = UseCapsLock(); const [isError, setIsError] = useState(false); @@ -26,11 +26,14 @@ export default function LoginPage() { const logInMutation = useMutation({ mutationFn: async (data: TLoginData) => { - let response = await axiosLocalhost.post("/login", JSON.stringify(data)); + let response = await axiosLocalhost.post( + "/login", + JSON.stringify(data) + ); setUserAtom({ username: response.data.username, - isAdmin: false - }) + isAdmin: false, + }); }, onError: (error, _variables, _context) => { @@ -44,8 +47,8 @@ export default function LoginPage() { if (response.status === 200) { setUserAtom({ username: userAtomValue?.username || "", - isAdmin: true - }) + isAdmin: true, + }); } }; @@ -58,7 +61,7 @@ export default function LoginPage() { return ( @@ -66,6 +69,7 @@ export default function LoginPage() { {t("loginForm")} { e.preventDefault(); let formData = new FormData( @@ -80,10 +84,10 @@ export default function LoginPage() { logInMutation.mutate(loginData); }} > - +
- {t("username")} + {t("username")} {t("errors.enterUsername")} @@ -115,7 +119,7 @@ export default function LoginPage() {
- {t("password")} + {t("password")} {t("errors.enterPassword")} @@ -125,6 +129,7 @@ export default function LoginPage() { {(validity) => ( @@ -157,10 +162,18 @@ export default function LoginPage() { - + + + {t("suggestRegister")}{" "} + + {t("register")} + {" "} + {t("now")} + ); diff --git a/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx index dcf8914..bd488f4 100644 --- a/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx +++ b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx @@ -59,6 +59,7 @@ export default function RegisterPage() { {t("registerForm")} { e.preventDefault(); let formData = new FormData( @@ -74,7 +75,7 @@ export default function RegisterPage() { registerMutation.mutate(registerData); }} > - +
{t("username")} @@ -106,7 +107,7 @@ export default function RegisterPage() { - +
{t("email")} @@ -141,7 +142,7 @@ export default function RegisterPage() { - +
{t("password")} @@ -182,7 +183,7 @@ export default function RegisterPage() {
@@ -237,8 +238,8 @@ export default function RegisterPage() { {t("errors.invalidRegisterData")} - - diff --git a/enshi/src/Pages/MainPage/MainPage.tsx b/enshi/src/Pages/MainPage/MainPage.tsx index b98c8a6..06b1bf4 100644 --- a/enshi/src/Pages/MainPage/MainPage.tsx +++ b/enshi/src/Pages/MainPage/MainPage.tsx @@ -7,7 +7,7 @@ import { userAtom } from "../../AtomStore/AtomStore"; import NavBar from "../../Components/NavBar/NavBar"; const REFETCH_INTERVAL_IN_MINUTES = 5; -const RETRY_INTERVAL_IN_SECONDS = 3; +const RETRY_INTERVAL_IN_SECONDS = 1; const SECONDS_IN_MINUTE = 60; const MILLS_IN_SECOND = 1000; @@ -18,14 +18,15 @@ export default function MainPage() { const { isPending } = useQuery({ queryKey: ["authKey"], queryFn: async () => { - const response = await axiosLocalhost.get("/auth/check"); - if (response.status === 200) { + try { + const response = await axiosLocalhost.get("/auth/check"); + setUserData({ isAdmin: response.data["is_admin"], username: response.data["username"], }); return true; - } else { + } catch (error) { setUserData(undefined); return false; } @@ -34,6 +35,7 @@ export default function MainPage() { REFETCH_INTERVAL_IN_MINUTES * SECONDS_IN_MINUTE * MILLS_IN_SECOND, refetchOnWindowFocus: true, refetchOnReconnect: true, + gcTime: 10, retry: 3, retryDelay: (attempt) => diff --git a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx index f2aa2b5..97885fa 100644 --- a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx +++ b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx @@ -1,35 +1,25 @@ -import { Container, Text } from "@radix-ui/themes"; -import { useAtomValue } from "jotai"; -import { useEffect, useState } from "react"; +import { Container } from "@radix-ui/themes"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { userAtom } from "../../AtomStore/AtomStore"; +import ArticleViewer from "../../Components/ArticleViewer/ArticleViewer"; import Editor from "../../Components/Editor/Editor"; export default function PostCreatorPage() { - const user = useAtomValue(userAtom); - const [userInput, setUserInput] = useState(""); const { t } = useTranslation(); - useEffect(() => { - console.log("asdasd", userInput); - - }, [userInput]) - - - if (!user) { - return ( - - {t("errors.unauthorized")} - - ); - } - return ( <> + + ); 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/locale/en.ts b/enshi/src/locale/en.ts index 1a22750..846f98e 100644 --- a/enshi/src/locale/en.ts +++ b/enshi/src/locale/en.ts @@ -20,6 +20,10 @@ const en = { registerForm: "Register", loginForm: "Log in", + suggestRegister: "Don't have an account?", + register: "Register", + now: "now!", + errors: { enterUsername: "Please enter your username", enterEmail: "Please enter your email", diff --git a/enshi/src/locale/ru.ts b/enshi/src/locale/ru.ts index 15b7f2e..e5dfbf0 100644 --- a/enshi/src/locale/ru.ts +++ b/enshi/src/locale/ru.ts @@ -1,3 +1,4 @@ + const ru = { hello: "Привет!", search: "Поиск...", @@ -20,11 +21,14 @@ const ru = { registerForm: "Регистрация", loginForm: "Вход", + suggestRegister: "Не зарегистрированы?", + register: "Создайте аккаунт", + now: "сейчас!", errors: { - enterUsername: "Пожалуйста, введите ваше имя пользователя", - enterEmail: "Пожалуйста, введите свой адрес электронной почты", - invalidEmail: "Пожалуйста, введите корректный адрес электронной почты", + enterUsername: "Это обязательное поле", + enterEmail: "Это обязательное поле", + invalidEmail: "Некорректный адрес электронной почты", enterPassword: "Пожалуйста, введите пароль", passwordsMismatch: "Пароли должны быть одинаковыми", invalidLoginData: "Неверное имя пользователя или пароль", diff --git a/enshi/src/main.tsx b/enshi/src/main.tsx index 63f11dd..7b97595 100644 --- a/enshi/src/main.tsx +++ b/enshi/src/main.tsx @@ -4,7 +4,7 @@ import './index.css' import './locale/i18n.ts' import i18n from './locale/i18n.ts' -i18n.changeLanguage(navigator.language) +i18n.changeLanguage("ru") createRoot(document.getElementById('root')!).render( diff --git a/enshi/src/routes/routes.tsx b/enshi/src/routes/routes.tsx index 02bb0f2..4f74bfc 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -4,6 +4,7 @@ import { Route, useRouteError, } from "react-router-dom"; +import AuthPageWrapper from "../Pages/AuthPageWrapper/AuthPageWrapper"; import LoginPage from "../Pages/LoginRegisterPage/LoginPage/LoginPage"; import RegisterPage from "../Pages/LoginRegisterPage/RegisterPage/RegisterPage"; import MainPage from "../Pages/MainPage/MainPage"; @@ -30,7 +31,9 @@ export const routes = createRoutesFromElements( + + + } > diff --git a/enshi/tailwind.config.js b/enshi/tailwind.config.js index af5c3aa..fcdfd66 100644 --- a/enshi/tailwind.config.js +++ b/enshi/tailwind.config.js @@ -7,6 +7,9 @@ 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", diff --git a/enshi_back/middleware/authMiddleware.go b/enshi_back/middleware/authMiddleware.go index c584354..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,7 +13,14 @@ import ( func AuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { - tokenFromCookies := c.Request.CookiesNamed("auth_cookie")[0].Value + cookies := c.Request.CookiesNamed("auth_cookie") + if len(cookies) == 0 { + rest_api_stuff.UnauthorizedAnswer(c, fmt.Errorf("no token provided")) + c.Abort() + return + } + + tokenFromCookies := cookies[0].Value cookieClimes, err := auth.ValidateToken(tokenFromCookies) if err != nil { c.IndentedJSON(http.StatusUnauthorized, gin.H{"error auth": err.Error()}) diff --git a/package-lock.json b/package-lock.json index bae962a..483719a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "@radix-ui/react-icons": "^1.3.2" + "@radix-ui/react-icons": "^1.3.2", + "interweave": "^13.1.0" } }, "node_modules/@radix-ui/react-icons": { @@ -17,6 +18,28 @@ "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" } }, + "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", diff --git a/package.json b/package.json index cc5e205..dcd08fd 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "dependencies": { - "@radix-ui/react-icons": "^1.3.2" + "@radix-ui/react-icons": "^1.3.2", + "interweave": "^13.1.0" } } From 66178664f33740352d740cd251a98e4e331148d8 Mon Sep 17 00:00:00 2001 From: Max Date: Sun, 24 Nov 2024 20:14:55 +0300 Subject: [PATCH 05/12] Many things --- enshi/package-lock.json | 135 +++++++ enshi/package.json | 2 + enshi/src/{types => @types}/UserType.ts | 0 enshi/src/@types/index.d.ts | 11 + enshi/src/App.tsx | 15 +- enshi/src/AtomStore/AtomStore.ts | 50 ++- enshi/src/Components/Editor/Editor.tsx | 32 +- enshi/src/Components/NavBar/NavBar.tsx | 14 +- .../ToastProvider/ToastProvider.tsx | 44 +++ .../Pages/AuthPageWrapper/AuthPageWrapper.tsx | 2 +- enshi/src/Pages/MainPage/MainPage.tsx | 16 +- .../Pages/PostCreatorPage/PostCreatorPage.tsx | 52 ++- .../SubmitPostButton/SubmitPostButton.tsx | 57 +++ enshi/src/hooks/useToast.tsx | 7 + enshi/src/main.tsx | 2 +- enshi/src/routes/routes.tsx | 3 +- enshi/tailwind.config.js | 18 + package-lock.json | 341 ++++++++++++++++++ package.json | 1 + 19 files changed, 731 insertions(+), 71 deletions(-) rename enshi/src/{types => @types}/UserType.ts (100%) create mode 100644 enshi/src/@types/index.d.ts create mode 100644 enshi/src/Components/ToastProvider/ToastProvider.tsx create mode 100644 enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx create mode 100644 enshi/src/hooks/useToast.tsx diff --git a/enshi/package-lock.json b/enshi/package-lock.json index 5ae82a9..499cbe7 100644 --- a/enshi/package-lock.json +++ b/enshi/package-lock.json @@ -10,7 +10,9 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-form": "^0.1.0", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/themes": "^3.1.3", "@tanstack/react-query": "^5.55.0", @@ -1622,6 +1624,15 @@ } } }, + "node_modules/@radix-ui/react-icons": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz", + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==", + "license": "MIT", + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", @@ -2216,6 +2227,130 @@ } } }, + "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-toast/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-toast/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-toast/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-toast/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-toggle": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", diff --git a/enshi/package.json b/enshi/package.json index 8f48f0b..6effe72 100644 --- a/enshi/package.json +++ b/enshi/package.json @@ -12,7 +12,9 @@ "dependencies": { "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-form": "^0.1.0", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/themes": "^3.1.3", "@tanstack/react-query": "^5.55.0", diff --git a/enshi/src/types/UserType.ts b/enshi/src/@types/UserType.ts similarity index 100% rename from enshi/src/types/UserType.ts rename to enshi/src/@types/UserType.ts diff --git a/enshi/src/@types/index.d.ts b/enshi/src/@types/index.d.ts new file mode 100644 index 0000000..6b484a7 --- /dev/null +++ b/enshi/src/@types/index.d.ts @@ -0,0 +1,11 @@ +type TToast = { + title: string; + description?: string; + action?: React.Component; +}; + +type TExistingToast = TToast & { + id: number; + resetFunc: (arg0: boolean) => void; + open: boolean; +}; diff --git a/enshi/src/App.tsx b/enshi/src/App.tsx index 7d8c2ba..b67896c 100644 --- a/enshi/src/App.tsx +++ b/enshi/src/App.tsx @@ -1,11 +1,12 @@ import { Theme } from "@radix-ui/themes"; import "@radix-ui/themes/styles.css"; import { QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import "axios"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; import queryClient from "./api/QueryClient/QueryClient"; import "./App.css"; +import ToastProvider from "./Components/ToastProvider/ToastProvider"; import { routes } from "./routes/routes"; const router = createBrowserRouter(routes); @@ -13,11 +14,13 @@ const router = createBrowserRouter(routes); export default function App() { return ( - - - {/* */} - - + + + + {/* */} + + + ); } diff --git a/enshi/src/AtomStore/AtomStore.ts b/enshi/src/AtomStore/AtomStore.ts index 352eca9..ed53496 100644 --- a/enshi/src/AtomStore/AtomStore.ts +++ b/enshi/src/AtomStore/AtomStore.ts @@ -1,4 +1,50 @@ import { atom } from "jotai"; -import { TUser } from "../types/UserType"; +import { atomWithStorage } from "jotai/utils"; +import { TUser } from "../@types/UserType"; -export const userAtom = atom() \ No newline at end of file +export const userAtom = atom(); + +export const postCreationAtom = atom(); +export const postCreationTitleAtom = atom(); + +type TPostData = { + title: string; + content: string; +}; + +export const storagePostAtom = atomWithStorage( + "draft-post", + { title: "", content: "" }, + { + getItem: (key) => sessionStorage.getItem(key) as any, + setItem: (key, value) => sessionStorage.setItem(key, value as any), + removeItem: (key) => sessionStorage.removeItem(key), + }, + { getOnInit: true } +); + +export const toastAtom = atom([]); +export const setToastAtom = atom(null, (get, set, value: TToast) => { + let maxToastId = Math.max(...get(toastAtom).map((toast) => toast.id)); + maxToastId = maxToastId >= 0 ? maxToastId : 1; + let atomValueWithNewToast = get(toastAtom); + atomValueWithNewToast = [ + ...atomValueWithNewToast, + { + id: maxToastId + 1, + resetFunc: (_) => { + let currentToasts = get(toastAtom); + let afterRemoval = currentToasts.filter( + (toast) => toast.id != maxToastId + 1 + ); + set(toastAtom, afterRemoval); + }, + title: value.title, + action: value.action, + description: value.description, + open: true, + }, + ]; + + set(toastAtom, atomValueWithNewToast); +}); diff --git a/enshi/src/Components/Editor/Editor.tsx b/enshi/src/Components/Editor/Editor.tsx index 9c92d3c..a758d13 100644 --- a/enshi/src/Components/Editor/Editor.tsx +++ b/enshi/src/Components/Editor/Editor.tsx @@ -11,13 +11,13 @@ import ReactQuill from "react-quill"; type TEditor = { readOnly?: boolean; defaultValue?: string | Delta; - onChange: (d: string) => void; + onChange?: (d: string) => void; onSelectionChange?: any; }; const modules = { toolbar: [ - [{ header: [1, 2, 3, false] }], + [{ header: [1, 2, 3, 4, 5, false] }], ["bold", "italic", "underline", "strike", "blockquote", "span-wrapper"], [ { list: "ordered" }, @@ -28,10 +28,7 @@ const modules = { ["link", "image"], ["clean"], [{ align: [] }], - ], - clipboard: { - matchVisual: true, - }, + ] }; /** @@ -52,32 +49,13 @@ const Editor = forwardRef((props: TEditor) => { setQuill(null); }; }, [editor.current]); - - useEffect(() => { - let Inline = Quill.import('attributors/style/size'); - console.log(Inline); - - // //@ts-ignore - // class BoldBlot extends Inline {} - // //@ts-ignore - // BoldBlot.blotName = 'bold1'; - // //@ts-ignore - // BoldBlot.tagName = 'strong'; - // console.log(BoldBlot, true); - - - Quill.register(Inline as any, true); - return () => { - - } - }, []) const changeHandler = (val: string, _changeDelta: Delta, _source: Sources, _editor: ReactQuill.UnprivilegedEditor) => { console.log(val); console.log(JSON.stringify(quill?.getContents().ops, null, 2)) let fullDelta = quill?.getContents() - props.onChange(val || "") + if (props.onChange) props.onChange(val || "") setValue(fullDelta || new Delta()) } @@ -87,8 +65,6 @@ const Editor = forwardRef((props: TEditor) => { value={value} ref={editor} modules={modules} - formats={['bold1']} - onChange={changeHandler} diff --git a/enshi/src/Components/NavBar/NavBar.tsx b/enshi/src/Components/NavBar/NavBar.tsx index 4cca591..17472c6 100644 --- a/enshi/src/Components/NavBar/NavBar.tsx +++ b/enshi/src/Components/NavBar/NavBar.tsx @@ -4,14 +4,12 @@ import SearchField from "./SearchField/SearchField"; export default function NavBar() { return ( - // - ); -} \ No newline at end of file +} 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/Pages/AuthPageWrapper/AuthPageWrapper.tsx b/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx index 9f6ced6..3418d83 100644 --- a/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx +++ b/enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx @@ -10,7 +10,7 @@ export default function AuthPageWrapper(props: React.PropsWithChildren) { const navigate = useNavigate(); if (!user) { - navigate("/login") + navigate("/login"); return ( {t("errors.unauthorized")} diff --git a/enshi/src/Pages/MainPage/MainPage.tsx b/enshi/src/Pages/MainPage/MainPage.tsx index 06b1bf4..12bb82d 100644 --- a/enshi/src/Pages/MainPage/MainPage.tsx +++ b/enshi/src/Pages/MainPage/MainPage.tsx @@ -1,4 +1,4 @@ -import { Spinner } from "@radix-ui/themes"; +import { Box, Flex, Spinner } from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; import { useSetAtom } from "jotai"; import { Outlet } from "react-router-dom"; @@ -20,7 +20,7 @@ export default function MainPage() { queryFn: async () => { try { const response = await axiosLocalhost.get("/auth/check"); - + setUserData({ isAdmin: response.data["is_admin"], username: response.data["username"], @@ -52,10 +52,14 @@ export default function MainPage() {
) : ( - <> - - - + + + + + + + + )} ); diff --git a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx index 97885fa..3e3e6dd 100644 --- a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx +++ b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx @@ -1,26 +1,42 @@ -import { Container } from "@radix-ui/themes"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import ArticleViewer from "../../Components/ArticleViewer/ArticleViewer"; +import { Box, Container, Flex } from "@radix-ui/themes"; +import { 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 [userInput, setUserInput] = useState(""); - - const { t } = useTranslation(); - + const setTitleValue = useSetAtom(postCreationTitleAtom); + const setContentValue = useSetAtom(postCreationAtom); + return ( <> - - - - - + + + + + { + setTitleValue(e.target.value); + }} + /> + + + + + + + + + + + ); } diff --git a/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx b/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx new file mode 100644 index 0000000..c57f3c6 --- /dev/null +++ b/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx @@ -0,0 +1,57 @@ +import { Button } from "@radix-ui/themes"; +import { useMutation } from "@tanstack/react-query"; +import { useAtomValue } 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 contentValue = useAtomValue(postCreationAtom); + const titleValue = useAtomValue(postCreationTitleAtom); + + const navigate = useNavigate(); + + const [isDisabled, setIsDisabled] = useState(false); + + const postMutation = useMutation({ + mutationFn: async () => { + axiosLocalhost.post("/posts", { + title: titleValue, + content: contentValue, + }); + }, + onMutate: () => { + setIsDisabled(true); + }, + onError: () => { + setIsDisabled(false); + }, + onSuccess: () => { + navigate("/"); + }, + }); + + return ( + + ); +} 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/main.tsx b/enshi/src/main.tsx index 7b97595..63f11dd 100644 --- a/enshi/src/main.tsx +++ b/enshi/src/main.tsx @@ -4,7 +4,7 @@ import './index.css' import './locale/i18n.ts' import i18n from './locale/i18n.ts' -i18n.changeLanguage("ru") +i18n.changeLanguage(navigator.language) createRoot(document.getElementById('root')!).render( diff --git a/enshi/src/routes/routes.tsx b/enshi/src/routes/routes.tsx index 4f74bfc..a6f3544 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -21,12 +21,13 @@ export const routes = createRoutesFromElements( <> } element={}> Cringer path} /> + Cringer path, but this a } - > + /> =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 dcd08fd..5586dfe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-toast": "^1.2.2", "interweave": "^13.1.0" } } From 9aa5cbc12a16101ed56ecedb459f5cbfc3f8c4c6 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 25 Nov 2024 17:36:29 +0300 Subject: [PATCH 06/12] Bookmarks and layout --- .../ArticleViewer/ArticleViewer.tsx | 31 +++++++++++++--- .../SkeletonLoader/SkeletonLoader.tsx | 27 ++++++++++++++ .../Pages/PostCreatorPage/PostCreatorPage.tsx | 9 ++--- .../SubmitPostButton/SubmitPostButton.tsx | 17 ++++++--- enshi/src/constants/textForSkeleton.ts | 12 +++++++ enshi/src/routes/routes.tsx | 5 ++- .../ABAC/BookmarkPolicies/bookmarkPolicies.go | 36 +++++++++++++++++++ .../bookmarkRules/createRule.go | 22 ++++++++++++ .../bookmarkRules/deleteRule.go | 22 ++++++++++++ .../bookmarkRules/readRule.go | 22 ++++++++++++ enshi_back/middleware/bookmarksMiddleware.go | 33 +++++++++++++++++ .../routes/bookmarksRoutes/createBookmark.go | 35 ++++++++++++++++++ enshi_back/routes/routesSetup.go | 9 +++++ 13 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx create mode 100644 enshi/src/constants/textForSkeleton.ts create mode 100644 enshi_back/ABAC/BookmarkPolicies/bookmarkPolicies.go create mode 100644 enshi_back/ABAC/BookmarkPolicies/bookmarkRules/createRule.go create mode 100644 enshi_back/ABAC/BookmarkPolicies/bookmarkRules/deleteRule.go create mode 100644 enshi_back/ABAC/BookmarkPolicies/bookmarkRules/readRule.go create mode 100644 enshi_back/middleware/bookmarksMiddleware.go create mode 100644 enshi_back/routes/bookmarksRoutes/createBookmark.go diff --git a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx index 511968a..564e314 100644 --- a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx +++ b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx @@ -1,19 +1,40 @@ -import { Container } from "@radix-ui/themes"; +import { Container, Separator, Text } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; import { Interweave } from "interweave"; import { useParams } from "react-router-dom"; +import { axiosLocalhost } from "../../api/axios/axios"; +import SkeletonLoader from "./SkeletonLoader/SkeletonLoader"; type TArticleViewer = { htmlToParse?: string; -} +}; export default function ArticleViewer(props: TArticleViewer) { - const queryPapms = useParams() + let queryParams = useParams(); + + const { data, isPending } = useQuery({ + queryKey: [`post_${queryParams["postId"]}`], + queryFn: async () => { + const response = await axiosLocalhost.get( + `posts/${queryParams["postId"]}` + ); + + return response.data; + }, + }); + + if (isPending) + return ( + + ); return ( <>
- - + + {data.title} + +
diff --git a/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx b/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx new file mode 100644 index 0000000..2a9841b --- /dev/null +++ b/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx @@ -0,0 +1,27 @@ +import { Container, Skeleton, Text } from "@radix-ui/themes"; +import { + headerLong, + headerShort, + pText, +} from "../../../constants/textForSkeleton"; + +export default function SkeletonLoader() { + return ( + + + {headerLong} +
+ {headerShort} +
+
+ {pText} +
+
+ {pText} +
+
+ {pText} +
+
+ ); +} diff --git a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx index 3e3e6dd..eb677ba 100644 --- a/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx +++ b/enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx @@ -1,5 +1,5 @@ import { Box, Container, Flex } from "@radix-ui/themes"; -import { useSetAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import { postCreationAtom, postCreationTitleAtom @@ -8,7 +8,7 @@ import Editor from "../../Components/Editor/Editor"; import SubmitPostButton from "./SubmitPostButton/SubmitPostButton"; export default function PostCreatorPage() { - const setTitleValue = useSetAtom(postCreationTitleAtom); + const [titleValue, setTitleValue] = useAtom(postCreationTitleAtom); const setContentValue = useSetAtom(postCreationAtom); return ( @@ -20,11 +20,12 @@ export default function PostCreatorPage() { { setTitleValue(e.target.value); }} + value={titleValue} /> diff --git a/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx b/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx index c57f3c6..201d22d 100644 --- a/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx +++ b/enshi/src/Pages/PostCreatorPage/SubmitPostButton/SubmitPostButton.tsx @@ -1,6 +1,6 @@ import { Button } from "@radix-ui/themes"; import { useMutation } from "@tanstack/react-query"; -import { useAtomValue } from "jotai"; +import { useAtom } from "jotai"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -16,15 +16,20 @@ type TSubmitPostButton = { export default function SubmitPostButton(props: TSubmitPostButton) { const { t } = useTranslation(); - const contentValue = useAtomValue(postCreationAtom); - const titleValue = useAtomValue(postCreationTitleAtom); - - const navigate = useNavigate(); 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, @@ -37,6 +42,8 @@ export default function SubmitPostButton(props: TSubmitPostButton) { setIsDisabled(false); }, onSuccess: () => { + setContentValue(""); + setTitleValue(""); navigate("/"); }, }); 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/routes/routes.tsx b/enshi/src/routes/routes.tsx index a6f3544..fd1c62a 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -4,6 +4,7 @@ import { Route, useRouteError, } from "react-router-dom"; +import ArticleViewer from "../Components/ArticleViewer/ArticleViewer"; import AuthPageWrapper from "../Pages/AuthPageWrapper/AuthPageWrapper"; import LoginPage from "../Pages/LoginRegisterPage/LoginPage/LoginPage"; import RegisterPage from "../Pages/LoginRegisterPage/RegisterPage/RegisterPage"; @@ -36,7 +37,9 @@ export const routes = createRoutesFromElements( } - >
+ /> + + } /> Date: Tue, 26 Nov 2024 22:44:53 +0300 Subject: [PATCH 07/12] This is lazy push --- .../PostVotesPolicies/PostVotePolicies.go | 36 ++++++++++++++ .../PostVoteRules/createRule.go | 22 +++++++++ .../PostVoteRules/deleteRule.go | 22 +++++++++ .../PostVoteRules/readRule.go | 22 +++++++++ .../db/go_queries/post_votes_queries.sql.go | 3 ++ enshi_back/db/queries/post_votes_queries.sql | 3 ++ enshi_back/middleware/postVotesMiddleware.go | 33 +++++++++++++ .../routes/bookmarksRoutes/createBookmark.go | 2 +- .../routes/bookmarksRoutes/deleteBookmark.go | 35 ++++++++++++++ .../routes/bookmarksRoutes/getBookmark.go | 48 +++++++++++++++++++ enshi_back/routes/routesSetup.go | 29 +++++++++++ enshi_back/routes/voteRoutes/createVote.go | 35 ++++++++++++++ enshi_back/routes/voteRoutes/deleteVote.go | 35 ++++++++++++++ enshi_back/routes/voteRoutes/getVote.go | 38 +++++++++++++++ 14 files changed, 362 insertions(+), 1 deletion(-) create mode 100644 enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go create mode 100644 enshi_back/ABAC/PostVotesPolicies/PostVoteRules/createRule.go create mode 100644 enshi_back/ABAC/PostVotesPolicies/PostVoteRules/deleteRule.go create mode 100644 enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readRule.go create mode 100644 enshi_back/middleware/postVotesMiddleware.go create mode 100644 enshi_back/routes/bookmarksRoutes/deleteBookmark.go create mode 100644 enshi_back/routes/bookmarksRoutes/getBookmark.go create mode 100644 enshi_back/routes/voteRoutes/createVote.go create mode 100644 enshi_back/routes/voteRoutes/deleteVote.go create mode 100644 enshi_back/routes/voteRoutes/getVote.go diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go b/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go new file mode 100644 index 0000000..045a327 --- /dev/null +++ b/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go @@ -0,0 +1,36 @@ +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) + + } + + 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/db/go_queries/post_votes_queries.sql.go b/enshi_back/db/go_queries/post_votes_queries.sql.go index 171b179..688d23e 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 ` diff --git a/enshi_back/db/queries/post_votes_queries.sql b/enshi_back/db/queries/post_votes_queries.sql index 74cbfd3..ee6b651 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 diff --git a/enshi_back/middleware/postVotesMiddleware.go b/enshi_back/middleware/postVotesMiddleware.go new file mode 100644 index 0000000..f6805b9 --- /dev/null +++ b/enshi_back/middleware/postVotesMiddleware.go @@ -0,0 +1,33 @@ +package middleware + +import ( + postvotespolicies "enshi/ABAC/PostVotesPolicies" + "enshi/ABAC/rules" + + "github.com/gin-gonic/gin" +) + +func PostVotesMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + + switch c.Request.Method { + case "DELETE": + c.Set("target", postvotespolicies.DELETE_VOTE) + + case "POST": + c.Set("target", postvotespolicies.CREATE_VOTE) + + case "GET": + c.Set("target", postvotespolicies.READ_VOTE) + } + + isAllowed, errors := postvotespolicies.PostVotePolicies(c) + + if rules.ShouldAbortRequest(c, isAllowed, errors) { + c.Abort() + return + } + + c.Next() + } +} diff --git a/enshi_back/routes/bookmarksRoutes/createBookmark.go b/enshi_back/routes/bookmarksRoutes/createBookmark.go index 14fdae9..d12734e 100644 --- a/enshi_back/routes/bookmarksRoutes/createBookmark.go +++ b/enshi_back/routes/bookmarksRoutes/createBookmark.go @@ -10,7 +10,7 @@ import ( "github.com/gin-gonic/gin" ) -func CreatePost(c *gin.Context) { +func CreateBookmark(c *gin.Context) { var bookmarkParams db_repo.CreateBookmarkParams if err := c.BindJSON(&bookmarkParams); err != nil { 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/routesSetup.go b/enshi_back/routes/routesSetup.go index 0e6e3de..ebbbdb7 100644 --- a/enshi_back/routes/routesSetup.go +++ b/enshi_back/routes/routesSetup.go @@ -8,6 +8,7 @@ import ( bookmarksroutes "enshi/routes/bookmarksRoutes" "enshi/routes/postsRoutes" "enshi/routes/userProfileRoutes" + voteroutes "enshi/routes/voteRoutes" "net/http" "strings" @@ -123,6 +124,34 @@ func SetupRotes(g *gin.Engine) error { 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-votes/:post-id", + voteroutes.GetVote, + ) + // Admin group routes adminGroup := g.Group("/admin/") adminGroup.Use(middleware.AdminMiddleware()) diff --git a/enshi_back/routes/voteRoutes/createVote.go b/enshi_back/routes/voteRoutes/createVote.go new file mode 100644 index 0000000..58946dc --- /dev/null +++ b/enshi_back/routes/voteRoutes/createVote.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 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 + + 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..609e74d --- /dev/null +++ b/enshi_back/routes/voteRoutes/getVote.go @@ -0,0 +1,38 @@ +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 + + 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 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, + }) + } +} From 1ab8022b951cab5405f74a927008e0649a6a7e25 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 2 Dec 2024 22:06:55 +0300 Subject: [PATCH 08/12] Post update stuff --- enshi/src/@types/UserType.ts | 1 + .../ArticleViewer/ArticleViewer.tsx | 29 +++++-- .../ChangePostButton/ChangePostButton.tsx | 21 ++++++ .../SkeletonLoader/SkeletonLoader.tsx | 2 +- enshi/src/Components/Editor/Editor.tsx | 49 +++++++----- .../LoginRegisterPage/LoginPage/LoginPage.tsx | 2 + .../PostRedactor/PostRedactor.tsx | 75 +++++++++++++++++++ .../SubmitChangesButton.tsx | 57 ++++++++++++++ .../RegisterPage/RegisterPage.tsx | 3 +- enshi/src/Pages/MainPage/MainPage.tsx | 1 + enshi/src/routes/routes.tsx | 2 + enshi_back/routes/authRoutes/login.go | 2 +- enshi_back/routes/authRoutes/registerUser.go | 2 +- enshi_back/routes/routesSetup.go | 1 + 14 files changed, 217 insertions(+), 30 deletions(-) create mode 100644 enshi/src/Components/ArticleViewer/ChangePostButton/ChangePostButton.tsx create mode 100644 enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx create mode 100644 enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx diff --git a/enshi/src/@types/UserType.ts b/enshi/src/@types/UserType.ts index c48416c..0e20c4d 100644 --- a/enshi/src/@types/UserType.ts +++ b/enshi/src/@types/UserType.ts @@ -1,4 +1,5 @@ export type TUser = { username: string; isAdmin: boolean; + id?: string | number; } \ No newline at end of file diff --git a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx index 564e314..5e6cb85 100644 --- a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx +++ b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx @@ -1,9 +1,12 @@ -import { Container, Separator, Text } from "@radix-ui/themes"; +import { Container, Flex, Separator, Text } from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; import { Interweave } from "interweave"; +import { useAtomValue } from "jotai"; import { useParams } from "react-router-dom"; import { axiosLocalhost } from "../../api/axios/axios"; -import SkeletonLoader from "./SkeletonLoader/SkeletonLoader"; +import { userAtom } from "../../AtomStore/AtomStore"; +import ChangePostButton from "./ChangePostButton/ChangePostButton"; +import SkeletonPostLoader from "./SkeletonLoader/SkeletonLoader"; type TArticleViewer = { htmlToParse?: string; @@ -11,6 +14,7 @@ type TArticleViewer = { export default function ArticleViewer(props: TArticleViewer) { let queryParams = useParams(); + const user = useAtomValue(userAtom) const { data, isPending } = useQuery({ queryKey: [`post_${queryParams["postId"]}`], @@ -21,18 +25,27 @@ export default function ArticleViewer(props: TArticleViewer) { return response.data; }, - }); + }) + - if (isPending) - return ( - - ); + if (isPending) return ; return ( <>
- {data.title} + + + {data.title} + + + + + diff --git a/enshi/src/Components/ArticleViewer/ChangePostButton/ChangePostButton.tsx b/enshi/src/Components/ArticleViewer/ChangePostButton/ChangePostButton.tsx new file mode 100644 index 0000000..f76cc5e --- /dev/null +++ b/enshi/src/Components/ArticleViewer/ChangePostButton/ChangePostButton.tsx @@ -0,0 +1,21 @@ +import { Button } from "@radix-ui/themes"; +import { useNavigate } from "react-router-dom"; + +type TChangePostButton = { + postId: number | string; +}; + +export default function ChangePostButton(props: TChangePostButton) { + const navigate = useNavigate(); + + return ( + + ); +} diff --git a/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx b/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx index 2a9841b..efa6b31 100644 --- a/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx +++ b/enshi/src/Components/ArticleViewer/SkeletonLoader/SkeletonLoader.tsx @@ -5,7 +5,7 @@ import { pText, } from "../../../constants/textForSkeleton"; -export default function SkeletonLoader() { +export default function SkeletonPostLoader() { return ( diff --git a/enshi/src/Components/Editor/Editor.tsx b/enshi/src/Components/Editor/Editor.tsx index a758d13..dd5354d 100644 --- a/enshi/src/Components/Editor/Editor.tsx +++ b/enshi/src/Components/Editor/Editor.tsx @@ -1,11 +1,6 @@ import Sources from "quill"; import Quill, { Delta } from "quill/core"; -import { - forwardRef, - useEffect, - useRef, - useState -} from "react"; +import { forwardRef, useEffect, useRef, useState } from "react"; import ReactQuill from "react-quill"; type TEditor = { @@ -28,7 +23,7 @@ const modules = { ["link", "image"], ["clean"], [{ align: [] }], - ] + ], }; /** @@ -37,7 +32,9 @@ const modules = { const Editor = forwardRef((props: TEditor) => { const editor = useRef(null); const [quill, setQuill] = useState(null); - const [value, setValue] = useState(new Delta()) + const [value, setValue] = useState(new Delta()); + + const [loaded, setLoaded] = useState(false); useEffect(() => { if (editor.current) { @@ -49,15 +46,34 @@ const Editor = forwardRef((props: TEditor) => { setQuill(null); }; }, [editor.current]); - - const changeHandler = (val: string, _changeDelta: Delta, _source: Sources, _editor: ReactQuill.UnprivilegedEditor) => { + useEffect(() => { + const quill = new Quill(document.createElement("div")); + const t = quill.clipboard.convert({ + html: props.defaultValue as string, + }) as Delta; + + if (!loaded) { + setValue(t); + + console.log(t); + } + + setLoaded(true); + }, [props.defaultValue]); + + const changeHandler = ( + val: string, + _changeDelta: Delta, + _source: Sources, + _editor: ReactQuill.UnprivilegedEditor + ) => { console.log(val); - console.log(JSON.stringify(quill?.getContents().ops, null, 2)) - let fullDelta = quill?.getContents() - if (props.onChange) props.onChange(val || "") - setValue(fullDelta || new Delta()) - } + console.log(JSON.stringify(quill?.getContents().ops, null, 2)); + let fullDelta = quill?.getContents(); + if (props.onChange) props.onChange(val || ""); + if (loaded) setValue(fullDelta || new Delta()); + }; return (
@@ -65,10 +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/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx index 7835c87..2b1e3db 100644 --- a/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx +++ b/enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx @@ -33,6 +33,7 @@ export default function LoginPage() { setUserAtom({ username: response.data.username, isAdmin: false, + id: response.data.id, }); }, @@ -48,6 +49,7 @@ export default function LoginPage() { setUserAtom({ username: userAtomValue?.username || "", isAdmin: true, + id: userAtomValue?.id, }); } }; diff --git a/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx b/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx new file mode 100644 index 0000000..c7cafaa --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx @@ -0,0 +1,75 @@ +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: Infinity, + }); + + 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..52cddc8 --- /dev/null +++ b/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx @@ -0,0 +1,57 @@ +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"; + +type TSubmitChangesButton = { + className: string; + titleValue: string; + contentValue: string; +}; + +export default function SubmitChangesButton(props: TSubmitChangesButton) { + const { t } = useTranslation(); + + const [isDisabled, setIsDisabled] = useState(false); + + 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: () => { + navigate("/"); + }, + }); + + return ( + + ); +} diff --git a/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx index bd488f4..787d581 100644 --- a/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx +++ b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx @@ -34,7 +34,8 @@ export default function RegisterPage() { let response = await axiosLocalhost.post("/users", JSON.stringify(data)); setUserAtom({ username: response.data.username, - isAdmin: false + isAdmin: false, + id: response.data.id, }) }, diff --git a/enshi/src/Pages/MainPage/MainPage.tsx b/enshi/src/Pages/MainPage/MainPage.tsx index 12bb82d..bf710a8 100644 --- a/enshi/src/Pages/MainPage/MainPage.tsx +++ b/enshi/src/Pages/MainPage/MainPage.tsx @@ -24,6 +24,7 @@ export default function MainPage() { setUserData({ isAdmin: response.data["is_admin"], username: response.data["username"], + id: response.data["id"], }); return true; } catch (error) { diff --git a/enshi/src/routes/routes.tsx b/enshi/src/routes/routes.tsx index fd1c62a..a8f2323 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -7,6 +7,7 @@ import { import ArticleViewer from "../Components/ArticleViewer/ArticleViewer"; import AuthPageWrapper from "../Pages/AuthPageWrapper/AuthPageWrapper"; import LoginPage from "../Pages/LoginRegisterPage/LoginPage/LoginPage"; +import PostRedactor from "../Pages/LoginRegisterPage/PostRedactor/PostRedactor"; import RegisterPage from "../Pages/LoginRegisterPage/RegisterPage/RegisterPage"; import MainPage from "../Pages/MainPage/MainPage"; import PostCreatorPage from "../Pages/PostCreatorPage/PostCreatorPage"; @@ -40,6 +41,7 @@ export const routes = createRoutesFromElements( /> } /> + } /> Date: Wed, 4 Dec 2024 23:01:05 +0300 Subject: [PATCH 09/12] Random posts route --- enshi/package-lock.json | 81 +++++++++++++++++-- enshi/package.json | 1 + enshi/src/@types/PostTypes.ts | 8 ++ .../ArticleViewer/ArticleViewer.tsx | 41 +++++----- enshi/src/Pages/MainPage/MainPage.tsx | 11 ++- .../RandomPostsPage/PostCard/PostCard.tsx | 30 +++++++ .../Pages/RandomPostsPage/RandomPostsPage.tsx | 56 +++++++++++++ enshi/src/routes/routes.tsx | 5 +- enshi_back/db/go_queries/posts_queries.sql.go | 41 ++++++++++ enshi_back/db/queries/posts_queries.sql | 8 +- .../routes/postsRoutes/getRandomPosts.go | 43 ++++++++++ enshi_back/routes/routesSetup.go | 6 ++ 12 files changed, 301 insertions(+), 30 deletions(-) create mode 100644 enshi/src/@types/PostTypes.ts create mode 100644 enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx create mode 100644 enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx create mode 100644 enshi_back/routes/postsRoutes/getRandomPosts.go diff --git a/enshi/package-lock.json b/enshi/package-lock.json index 499cbe7..f4e7c72 100644 --- a/enshi/package-lock.json +++ b/enshi/package-lock.json @@ -12,6 +12,7 @@ "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/themes": "^3.1.3", @@ -2044,17 +2045,17 @@ } }, "node_modules/@radix-ui/react-scroll-area": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz", - "integrity": "sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.1.tgz", + "integrity": "sha512-FnM1fHfCtEZ1JkyfH/1oMiTcFBQvHKl4vD9WnpwkLgtF+UmnXMCad6ECPTaAjcDjam+ndOEJWgHyKDGNteWSHw==", "license": "MIT", "dependencies": { "@radix-ui/number": "1.1.0", "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-presence": "1.1.0", + "@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-layout-effect": "1.1.0" @@ -2074,6 +2075,45 @@ } } }, + "node_modules/@radix-ui/react-scroll-area/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-scroll-area/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-select": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz", @@ -2678,6 +2718,37 @@ } } }, + "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-scroll-area": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz", + "integrity": "sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "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/themes/node_modules/@radix-ui/react-tooltip": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.1.tgz", diff --git a/enshi/package.json b/enshi/package.json index 6effe72..6cc8117 100644 --- a/enshi/package.json +++ b/enshi/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.1", "@radix-ui/react-toast": "^1.2.2", "@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/themes": "^3.1.3", diff --git a/enshi/src/@types/PostTypes.ts b/enshi/src/@types/PostTypes.ts new file mode 100644 index 0000000..9068e49 --- /dev/null +++ b/enshi/src/@types/PostTypes.ts @@ -0,0 +1,8 @@ +export type GetRandomPostsRow = { + post_id: string; + // blog_id: number; + user_id: string; + title: string; + // created_at: Date; +} + diff --git a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx index 5e6cb85..15f8a4a 100644 --- a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx +++ b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx @@ -14,7 +14,7 @@ type TArticleViewer = { export default function ArticleViewer(props: TArticleViewer) { let queryParams = useParams(); - const user = useAtomValue(userAtom) + const user = useAtomValue(userAtom); const { data, isPending } = useQuery({ queryKey: [`post_${queryParams["postId"]}`], @@ -25,31 +25,32 @@ export default function ArticleViewer(props: TArticleViewer) { return response.data; }, - }) - + }); if (isPending) return ; return ( <> -
- - - - {data.title} - - - + +
+ + + + {data.title} + + + + - - - - -
+ + +
+
+ ); } diff --git a/enshi/src/Pages/MainPage/MainPage.tsx b/enshi/src/Pages/MainPage/MainPage.tsx index bf710a8..fe64d01 100644 --- a/enshi/src/Pages/MainPage/MainPage.tsx +++ b/enshi/src/Pages/MainPage/MainPage.tsx @@ -12,6 +12,10 @@ 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); @@ -53,11 +57,14 @@ export default function MainPage() {
) : ( - + - + 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..764b601 --- /dev/null +++ b/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx @@ -0,0 +1,56 @@ +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import { Container } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { GetRandomPostsRow } from "../../@types/PostTypes"; +import { axiosLocalhost } from "../../api/axios/axios"; +import PostCard from "./PostCard/PostCard"; + +const LIMIT = 10; + +export default function RandomPostsPage() { + 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 ( + <> + + + {data?.map((post, i) => { + return ( + + + + ); + })} + + + + + + + + + + + ); +} diff --git a/enshi/src/routes/routes.tsx b/enshi/src/routes/routes.tsx index a8f2323..86b324e 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -11,6 +11,7 @@ import PostRedactor from "../Pages/LoginRegisterPage/PostRedactor/PostRedactor"; import RegisterPage from "../Pages/LoginRegisterPage/RegisterPage/RegisterPage"; import MainPage from "../Pages/MainPage/MainPage"; import PostCreatorPage from "../Pages/PostCreatorPage/PostCreatorPage"; +import RandomPostsPage from "../Pages/RandomPostsPage/RandomPostsPage"; function ErrorBoundary() { let error = useRouteError(); @@ -22,12 +23,12 @@ function ErrorBoundary() { export const routes = createRoutesFromElements( <> } element={}> - Cringer path} /> + } /> Cringer path, but this a + This page is yet to be created } /> 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/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/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 8ab6818..99e4133 100644 --- a/enshi_back/routes/routesSetup.go +++ b/enshi_back/routes/routesSetup.go @@ -65,6 +65,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, From 1f7d95a4ff5b97119d03912e560f6785f277d236 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 5 Dec 2024 23:11:44 +0300 Subject: [PATCH 10/12] Votes for post --- .vscode/settings.json | 4 +- .../ArticleViewer/ArticleViewer.tsx | 24 +++++-- .../ArticleViewer/VoteButton/VoteButton.tsx | 65 +++++++++++++++++++ .../ArticleViewer/VoteCounter/VoteCounter.tsx | 33 ++++++++++ .../PostVotesPolicies/PostVotePolicies.go | 2 + .../PostVoteRules/readVotesRule.go | 17 +++++ .../db/go_queries/post_votes_queries.sql.go | 19 ++++++ enshi_back/db/queries/post_votes_queries.sql | 8 ++- enshi_back/middleware/postVotesMiddleware.go | 9 ++- enshi_back/routes/routesSetup.go | 7 +- enshi_back/routes/voteRoutes/createVote.go | 7 ++ enshi_back/routes/voteRoutes/getVote.go | 12 ++-- enshi_back/routes/voteRoutes/getVotes.go | 29 +++++++++ 13 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx create mode 100644 enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx create mode 100644 enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readVotesRule.go create mode 100644 enshi_back/routes/voteRoutes/getVotes.go diff --git a/.vscode/settings.json b/.vscode/settings.json index ce758c7..fd2040b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "cSpell.words": [ - "godotenv" + "downvotes", + "godotenv", + "upvotes" ] } \ No newline at end of file diff --git a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx index 15f8a4a..892920a 100644 --- a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx +++ b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx @@ -1,4 +1,4 @@ -import { Container, Flex, Separator, Text } from "@radix-ui/themes"; +import { Box, Container, Flex, Separator, Text } from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; import { Interweave } from "interweave"; import { useAtomValue } from "jotai"; @@ -7,6 +7,8 @@ import { axiosLocalhost } from "../../api/axios/axios"; import { userAtom } from "../../AtomStore/AtomStore"; import ChangePostButton from "./ChangePostButton/ChangePostButton"; import SkeletonPostLoader from "./SkeletonLoader/SkeletonLoader"; +import VoteButton, { DOWNVOTE, UPVOTE } from "./VoteButton/VoteButton"; +import VoteCounter from "./VoteCounter/VoteCounter"; type TArticleViewer = { htmlToParse?: string; @@ -38,12 +40,26 @@ export default function ArticleViewer(props: TArticleViewer) { {data.title} - - + diff --git a/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx b/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx new file mode 100644 index 0000000..46a880b --- /dev/null +++ b/enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx @@ -0,0 +1,65 @@ +import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons"; +import { IconButton } from "@radix-ui/themes"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { axiosLocalhost } from "../../../api/axios/axios"; + +export const UPVOTE = true; +export const DOWNVOTE = false; + +type TVoteButton = { + postId: string; + vote: boolean; +}; + +export default function VoteButton(props: TVoteButton) { + const queryClient = useQueryClient(); + + const { data } = useQuery({ + queryKey: [props.vote + "voteCheck"], + queryFn: async () => { + const response = await axiosLocalhost.get( + `post-vote/${props.postId}` + ); + + return (response.data?.vote as boolean) === props.vote || false; + }, + gcTime: 0, + }); + + const voteMutation = useMutation({ + mutationKey: [`voteMutation${props.vote}`], + onMutate: async () => { + queryClient.cancelQueries({ queryKey: [props.vote + "voteCheck"] }); + + queryClient.setQueryData([props.vote + "voteCheck"], true); + queryClient.setQueryData([!props.vote + "voteCheck"], false); + }, + mutationFn: async () => { + await axiosLocalhost.post(`post-votes/${props.postId}`, { + vote: props.vote, + }); + }, + onSuccess: () => {}, + onError: () => { + queryClient.setQueryData([props.vote + "voteCheck"], false); + }, + onSettled: () => { + queryClient.invalidateQueries({ + queryKey: [props.vote + "voteCheck"], + }); + queryClient.invalidateQueries({ + queryKey: ["post_vote_counter"], + }); + }, + }); + + return ( + voteMutation.mutate()} + > + {props.vote ? : } + + ); +} diff --git a/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx b/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx new file mode 100644 index 0000000..4d54842 --- /dev/null +++ b/enshi/src/Components/ArticleViewer/VoteCounter/VoteCounter.tsx @@ -0,0 +1,33 @@ +import { Box, Skeleton } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { axiosLocalhost } from "../../../api/axios/axios"; + +type TVoteCounter = { + postId: string; +}; + +export default function VoteCounter(props: TVoteCounter) { + const { data, isLoading } = useQuery({ + queryKey: ["post_vote_counter"], + queryFn: async () => { + const response = await axiosLocalhost.get( + `post-votes/${props.postId}` + ); + return response.data as { upvotes: number; downvotes: number }; + }, + }); + + const calculateRating = (upvotes: number, downvotes: number) => { + return upvotes + (-downvotes) + } + + if (isLoading) { + return + {calculateRating(0, 0)} + + } + + return + {calculateRating(data?.upvotes || 0, data?.downvotes || 0)} + ; +} diff --git a/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go b/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go index 045a327..9e597bf 100644 --- a/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go +++ b/enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go @@ -30,6 +30,8 @@ func PostVotePolicies(c *gin.Context) (bool, []error) { 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/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 688d23e..05e957c 100644 --- a/enshi_back/db/go_queries/post_votes_queries.sql.go +++ b/enshi_back/db/go_queries/post_votes_queries.sql.go @@ -65,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/queries/post_votes_queries.sql b/enshi_back/db/queries/post_votes_queries.sql index ee6b651..ea1443a 100644 --- a/enshi_back/db/queries/post_votes_queries.sql +++ b/enshi_back/db/queries/post_votes_queries.sql @@ -20,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/middleware/postVotesMiddleware.go b/enshi_back/middleware/postVotesMiddleware.go index f6805b9..0992fab 100644 --- a/enshi_back/middleware/postVotesMiddleware.go +++ b/enshi_back/middleware/postVotesMiddleware.go @@ -3,13 +3,14 @@ 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) @@ -18,7 +19,11 @@ func PostVotesMiddleware() gin.HandlerFunc { c.Set("target", postvotespolicies.CREATE_VOTE) case "GET": - c.Set("target", postvotespolicies.READ_VOTE) + if a != "post-votes" { + c.Set("target", postvotespolicies.READ_VOTE) + } else { + c.Set("target", "") + } } isAllowed, errors := postvotespolicies.PostVotePolicies(c) diff --git a/enshi_back/routes/routesSetup.go b/enshi_back/routes/routesSetup.go index 99e4133..2a21d4f 100644 --- a/enshi_back/routes/routesSetup.go +++ b/enshi_back/routes/routesSetup.go @@ -155,10 +155,15 @@ func SetupRotes(g *gin.Engine) error { ) postVoteGroup.GET( - "post-votes/:post-id", + "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()) diff --git a/enshi_back/routes/voteRoutes/createVote.go b/enshi_back/routes/voteRoutes/createVote.go index 58946dc..037317c 100644 --- a/enshi_back/routes/voteRoutes/createVote.go +++ b/enshi_back/routes/voteRoutes/createVote.go @@ -25,6 +25,13 @@ func CreateVote(c *gin.Context) { } 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) diff --git a/enshi_back/routes/voteRoutes/getVote.go b/enshi_back/routes/voteRoutes/getVote.go index 609e74d..a949618 100644 --- a/enshi_back/routes/voteRoutes/getVote.go +++ b/enshi_back/routes/voteRoutes/getVote.go @@ -14,11 +14,6 @@ import ( func GetVote(c *gin.Context) { var postVoteParams db_repo.GetPostVoteParams - 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) @@ -26,6 +21,13 @@ func GetVote(c *gin.Context) { } 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) 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) + } +} From d87663d3d47405115c5e88fda9ed52266e161ded Mon Sep 17 00:00:00 2001 From: Max Date: Fri, 6 Dec 2024 20:43:02 +0300 Subject: [PATCH 11/12] Blogs --- enshi/src/Pages/BlogPage/BlogPage.tsx | 13 ++++ .../Pages/UserBlogsPage/BlogBox/BlogBox.tsx | 21 ++++++ .../SkeletonBoxes/SkeletonBoxes.tsx | 17 +++++ .../src/Pages/UserBlogsPage/UserBlogsPage.tsx | 69 +++++++++++++++++++ .../{Pages => layout}/MainPage/MainPage.tsx | 0 enshi/src/routes/routes.tsx | 30 ++++++-- enshi_back/routes/blogRoutes/getUserBlogs.go | 29 ++++++++ enshi_back/routes/routesSetup.go | 7 ++ enshi_back/utils/cringe.go | 49 +++++++++++++ 9 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 enshi/src/Pages/BlogPage/BlogPage.tsx create mode 100644 enshi/src/Pages/UserBlogsPage/BlogBox/BlogBox.tsx create mode 100644 enshi/src/Pages/UserBlogsPage/SkeletonBoxes/SkeletonBoxes.tsx create mode 100644 enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx rename enshi/src/{Pages => layout}/MainPage/MainPage.tsx (100%) create mode 100644 enshi_back/routes/blogRoutes/getUserBlogs.go create mode 100644 enshi_back/utils/cringe.go 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/UserBlogsPage/BlogBox/BlogBox.tsx b/enshi/src/Pages/UserBlogsPage/BlogBox/BlogBox.tsx new file mode 100644 index 0000000..4c62b4b --- /dev/null +++ b/enshi/src/Pages/UserBlogsPage/BlogBox/BlogBox.tsx @@ -0,0 +1,21 @@ +import { Card } from '@radix-ui/themes'; +import { useNavigate } from 'react-router-dom'; + +type TBlogBox = { + title?: string; + blogId?: string; +} + +export default function BlogBox(props: TBlogBox) { + + const navigate = useNavigate() + + return ( + navigate(``)} + > + {props?.title || "...No title..."} + {props?.blogId || "adqwwd"} + + ) +} 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..1396ee4 --- /dev/null +++ b/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx @@ -0,0 +1,69 @@ +import { Box, Container, Flex } from "@radix-ui/themes"; +import { useQuery } from "@tanstack/react-query"; +import { useAtomValue } from "jotai"; +import { userAtom } from "../../AtomStore/AtomStore"; +import { axiosLocalhost } from "../../api/axios/axios"; +import BlogBox from "./BlogBox/BlogBox"; +import SkeletonBoxes from "./SkeletonBoxes/SkeletonBoxes"; + +export default function UserBlogsPage() { + const user = useAtomValue(userAtom); + + 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 + ); + + const { data, isPending, isFetching } = useQuery({ + queryKey: ["userBlogs"], + queryFn: async () => { + const response = await axiosLocalhost.get("/user/blogs", { + transformResponse: [(data) => data], + }); + + let temp = parseWithBigInt(response.data, isBigNumber); + + return temp as any[]; + }, + }); + + if (isFetching) + return ( + + + + ); + + return ( + + + + {data + ? data?.map((blog: any, b) => { + return ( + + ); + }) + : null} + + + + ); +} diff --git a/enshi/src/Pages/MainPage/MainPage.tsx b/enshi/src/layout/MainPage/MainPage.tsx similarity index 100% rename from enshi/src/Pages/MainPage/MainPage.tsx rename to enshi/src/layout/MainPage/MainPage.tsx diff --git a/enshi/src/routes/routes.tsx b/enshi/src/routes/routes.tsx index 86b324e..ef8fa3e 100644 --- a/enshi/src/routes/routes.tsx +++ b/enshi/src/routes/routes.tsx @@ -1,17 +1,20 @@ 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 MainPage from "../Pages/MainPage/MainPage"; import PostCreatorPage from "../Pages/PostCreatorPage/PostCreatorPage"; import RandomPostsPage from "../Pages/RandomPostsPage/RandomPostsPage"; +import UserBlogsPage from "../Pages/UserBlogsPage/UserBlogsPage"; function ErrorBoundary() { let error = useRouteError(); @@ -26,14 +29,16 @@ export const routes = createRoutesFromElements( } /> This page is yet to be created + + This page is yet to be created + } /> @@ -41,8 +46,21 @@ export const routes = createRoutesFromElements( } /> - } /> - } /> + } /> + + }> + + + + } + /> + + + } /> + } /> Date: Sun, 8 Dec 2024 00:32:02 +0300 Subject: [PATCH 12/12] Lies --- enshi/package-lock.json | 244 +++++++++++++++++- enshi/package.json | 2 +- enshi/src/App.tsx | 6 +- .../ArticleViewer/ArticleViewer.tsx | 80 +++++- enshi/src/Components/BlogBox/BlogBox.tsx | 25 ++ .../NavBar/NavigationMenu/NavigationMenu.tsx | 12 +- .../UserNicknameLink/UserNicknameLink.tsx | 33 +++ .../PostRedactor/PostRedactor.tsx | 3 +- .../SubmitChangesButton.tsx | 3 + .../RegisterPage/RegisterPage.tsx | 17 +- .../Pages/RandomPostsPage/RandomPostsPage.tsx | 61 +++-- .../Pages/UserBlogsPage/BlogBox/BlogBox.tsx | 21 -- .../src/Pages/UserBlogsPage/UserBlogsPage.tsx | 119 ++++++--- enshi/src/locale/en.ts | 14 + enshi/src/locale/ru.ts | 15 ++ enshi/src/utils/idnex.ts | 19 ++ enshi_back/db/go_queries/users_queries.sql.go | 11 + enshi_back/db/queries/users_queries.sql | 3 + enshi_back/routes/routesSetup.go | 7 + enshi_back/routes/userRoutes/getUser.go | 30 +++ 20 files changed, 625 insertions(+), 100 deletions(-) create mode 100644 enshi/src/Components/BlogBox/BlogBox.tsx create mode 100644 enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx delete mode 100644 enshi/src/Pages/UserBlogsPage/BlogBox/BlogBox.tsx create mode 100644 enshi/src/utils/idnex.ts create mode 100644 enshi_back/routes/userRoutes/getUser.go diff --git a/enshi/package-lock.json b/enshi/package-lock.json index f4e7c72..2b04d15 100644 --- a/enshi/package-lock.json +++ b/enshi/package-lock.json @@ -8,7 +8,7 @@ "name": "enshi", "version": "0.0.0", "dependencies": { - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-navigation-menu": "^1.2.1", @@ -1233,6 +1233,42 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "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-arrow": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", @@ -1420,25 +1456,25 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", - "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", + "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", "@radix-ui/react-focus-scope": "1.1.0", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-slot": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" + "react-remove-scroll": "2.6.0" }, "peerDependencies": { "@types/react": "*", @@ -1455,6 +1491,158 @@ } } }, + "node_modules/@radix-ui/react-dialog/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-dialog/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-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "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-dialog/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-dialog/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-dialog/node_modules/react-remove-scroll": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -2682,6 +2870,42 @@ } } }, + "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "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/themes/node_modules/@radix-ui/react-navigation-menu": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz", diff --git a/enshi/package.json b/enshi/package.json index 6cc8117..e00dff1 100644 --- a/enshi/package.json +++ b/enshi/package.json @@ -10,7 +10,7 @@ "preview": "vite preview" }, "dependencies": { - "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-navigation-menu": "^1.2.1", diff --git a/enshi/src/App.tsx b/enshi/src/App.tsx index b67896c..29bf349 100644 --- a/enshi/src/App.tsx +++ b/enshi/src/App.tsx @@ -1,4 +1,4 @@ -import { Theme } from "@radix-ui/themes"; +import { Theme, ThemePanel } from "@radix-ui/themes"; import "@radix-ui/themes/styles.css"; import { QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; @@ -17,8 +17,8 @@ export default function App() { - {/* */} - + + diff --git a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx index 892920a..9cc5024 100644 --- a/enshi/src/Components/ArticleViewer/ArticleViewer.tsx +++ b/enshi/src/Components/ArticleViewer/ArticleViewer.tsx @@ -1,4 +1,14 @@ -import { Box, Container, Flex, Separator, Text } from "@radix-ui/themes"; +import * as Dialog from "@radix-ui/react-dialog"; +import { Cross2Icon } from "@radix-ui/react-icons"; +import { + Box, + Button, + Container, + Flex, + Select, + Separator, + Text, +} from "@radix-ui/themes"; import { useQuery } from "@tanstack/react-query"; import { Interweave } from "interweave"; import { useAtomValue } from "jotai"; @@ -27,6 +37,8 @@ export default function ArticleViewer(props: TArticleViewer) { return response.data; }, + gcTime: 0, + refetchOnMount: true, }); if (isPending) return ; @@ -40,14 +52,19 @@ export default function ArticleViewer(props: TArticleViewer) { {data.title} - + - + + + + + + + + + + + Add this post to blog + + + + + {`Add "${data.title}" to blog...`} + + + + + + + This + + + This is + updated blog + + + This another + + + + + + + +
+ + + +
+ + + +
+
+
diff --git a/enshi/src/Components/BlogBox/BlogBox.tsx b/enshi/src/Components/BlogBox/BlogBox.tsx new file mode 100644 index 0000000..ff5788a --- /dev/null +++ b/enshi/src/Components/BlogBox/BlogBox.tsx @@ -0,0 +1,25 @@ +import { Avatar, Card, Flex, Heading } from "@radix-ui/themes"; +import { useNavigate } from "react-router-dom"; +import UserNicknameLink from "../UserNicknameLink/UserNicknameLink"; + +type TBlogBox = { + title?: string; + blogId?: string; + userId: string; +}; + +export default function BlogBox(props: TBlogBox) { + const navigate = useNavigate(); + + return ( + navigate(``)}> + + {props?.title || "...No title..."} + + + + + + + ); +} 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/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/LoginRegisterPage/PostRedactor/PostRedactor.tsx b/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx index c7cafaa..50d1e9b 100644 --- a/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx +++ b/enshi/src/Pages/LoginRegisterPage/PostRedactor/PostRedactor.tsx @@ -30,7 +30,8 @@ export default function PostRedactor() { return error; } }, - gcTime: Infinity, + gcTime: 0, + refetchOnMount: true }); return ( diff --git a/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx b/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx index 52cddc8..070448d 100644 --- a/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx +++ b/enshi/src/Pages/LoginRegisterPage/PostRedactor/SubmitChangesButton/SubmitChangesButton.tsx @@ -4,6 +4,7 @@ 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; @@ -15,6 +16,7 @@ export default function SubmitChangesButton(props: TSubmitChangesButton) { const { t } = useTranslation(); const [isDisabled, setIsDisabled] = useState(false); + const createToast = useToast(); const navigate = useNavigate(); const queryParams = useParams(); @@ -37,6 +39,7 @@ export default function SubmitChangesButton(props: TSubmitChangesButton) { setIsDisabled(false); }, onSuccess: () => { + createToast({title: "Post has been changed!"}) navigate("/"); }, }); diff --git a/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx index 787d581..4702221 100644 --- a/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx +++ b/enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx @@ -5,7 +5,7 @@ import { useMutation } from "@tanstack/react-query"; import { useSetAtom } from "jotai"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { axiosLocalhost } from "../../../api/axios/axios"; import { userAtom } from "../../../AtomStore/AtomStore"; import UseCapsLock from "../../../hooks/useCapsLock"; @@ -244,6 +244,21 @@ export default function RegisterPage() { {t("submit")} + + + {t("alreadyRegistered")}{" "} + + {t("logIn")} + {" "} + {t("now")} + + + + {t("byPressingTheButton")}{" "} + + {t("termsOfService")}. + + ); diff --git a/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx b/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx index 764b601..605e04f 100644 --- a/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx +++ b/enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx @@ -1,6 +1,7 @@ import * as ScrollArea from "@radix-ui/react-scroll-area"; -import { Container } from "@radix-ui/themes"; +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"; @@ -8,6 +9,8 @@ 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 () => { @@ -27,30 +30,38 @@ export default function RandomPostsPage() { return ( <> - - - {data?.map((post, i) => { - return ( - - - - ); - })} - - - - - - - - - + + + {t("discover")} + + + + + + + {data?.map((post, i) => { + return ( + + + + ); + })} + + + + + {/* + + */} + {/* */} + + ); } diff --git a/enshi/src/Pages/UserBlogsPage/BlogBox/BlogBox.tsx b/enshi/src/Pages/UserBlogsPage/BlogBox/BlogBox.tsx deleted file mode 100644 index 4c62b4b..0000000 --- a/enshi/src/Pages/UserBlogsPage/BlogBox/BlogBox.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { Card } from '@radix-ui/themes'; -import { useNavigate } from 'react-router-dom'; - -type TBlogBox = { - title?: string; - blogId?: string; -} - -export default function BlogBox(props: TBlogBox) { - - const navigate = useNavigate() - - return ( - navigate(``)} - > - {props?.title || "...No title..."} - {props?.blogId || "adqwwd"} - - ) -} diff --git a/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx b/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx index 1396ee4..599ae6d 100644 --- a/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx +++ b/enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx @@ -1,32 +1,20 @@ -import { Box, Container, Flex } from "@radix-ui/themes"; +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 { useAtomValue } from "jotai"; -import { userAtom } from "../../AtomStore/AtomStore"; import { axiosLocalhost } from "../../api/axios/axios"; -import BlogBox from "./BlogBox/BlogBox"; +import BlogBox from "../../Components/BlogBox/BlogBox"; +import { JSONWithInt64 } from "../../utils/idnex"; import SkeletonBoxes from "./SkeletonBoxes/SkeletonBoxes"; export default function UserBlogsPage() { - const user = useAtomValue(userAtom); - - 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 - ); - const { data, isPending, isFetching } = useQuery({ queryKey: ["userBlogs"], queryFn: async () => { @@ -34,13 +22,13 @@ export default function UserBlogsPage() { transformResponse: [(data) => data], }); - let temp = parseWithBigInt(response.data, isBigNumber); + let temp = JSONWithInt64(response.data); return temp as any[]; }, }); - if (isFetching) + if (isPending) return ( @@ -51,17 +39,86 @@ export default function UserBlogsPage() { + + Your blogs + + + + {data ? data?.map((blog: any, b) => { return ( - + <> + + ); }) : null} + + + + + + + + + + Create blog + + + Create your new blog. + +
+ + +
+
+ +