Localization | edit page

This commit is contained in:
Max 2024-11-21 21:18:53 +03:00
parent 418947022b
commit be99c53c69
21 changed files with 275 additions and 98 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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() {
<Theme className="h-fit" accentColor="indigo" grayColor="slate">
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<ThemePanel />
{/* <ThemePanel /> */}
<ReactQueryDevtools />
</QueryClientProvider>
</Theme>
);

View File

@ -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 = {

View File

@ -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 (
<Container size={"4"}>
// <Container size={"4"}>
<nav className="flex justify-center pt-2 pb-2 ml-4 mr-4">
<CustomNavigationMenu />
<SearchField />
<UserButton />
<RightButtonBar />
</nav>
</Container>
// </Container>
);
}

View File

@ -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 (
<Button variant="ghost" className="h-full">
<PlusIcon />
<Text>{t("createPost")}</Text>
</Button>
);
}

View File

@ -0,0 +1,12 @@
import CreatePostButton from "./CreatePostButton/CreatePostButton";
import UserButton from "./UserButton/UserButton";
export default function RightButtonBar() {
return (
<div className='flex flex-row justify-end flex-1 gap-4'>
<CreatePostButton />
<UserButton />
</div>
)
}

View File

@ -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 (
<div className="flex justify-end flex-1">
<div className="">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton>
@ -30,7 +32,7 @@ export default function UserButton() {
<PersonIcon />
</Icon>
<Text>Profile</Text>
<Text>{t("profile")}</Text>
</Flex>
</Link>
</DropdownMenu.Item>
@ -40,7 +42,7 @@ export default function UserButton() {
<Icon>
<LaptopIcon />
</Icon>
<Text>Your blogs</Text>
<Text>{t("yourBlogs")}</Text>
</Flex>
</DropdownMenu.Item>
@ -52,14 +54,14 @@ export default function UserButton() {
<Icon>
<ExitIcon />
</Icon>
<Text>Log out</Text>
<Text>{t("signOut")}</Text>
</Flex>
) : (
<Flex className="justify-between gap-2">
<Icon>
<EnterIcon />
</Icon>
<Text>Log in</Text>
<Text>{t("signIn")}</Text>
</Flex>
)}
</DropdownMenu.Item>

View File

@ -6,9 +6,9 @@ export default function SearchField() {
const {t} = useTranslation()
return (
<div className="flex-1">
<div className="flex justify-center flex-1">
<TextField.Root
className="w-full rounded-lg"
className="w-2/3 rounded-lg"
placeholder={t("search")}
>
<TextField.Slot>

View File

@ -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("/");
},
});

View File

@ -1,16 +0,0 @@
import LoginElement from "./LoginElement/LoginElement";
import RegisterElement from "./RegisterElement/RegisterElement";
export function LoginPage() {
return (
<LoginElement />
)
}
export function RegisterPage() {
return (
<RegisterElement />
)
}

View File

@ -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) => {

View File

@ -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 (
<>
<NavBar />
<Outlet />
<button
onClick={async () => {
let d = await axiosLocalhost.get("getCookie");
console.log(d.data);
}}
>
Click for cookie test
</button>
{isPending ? (
<div
className="absolute top-1/2 left-1/2
translate-x-[-50%] translate-y-[-50%]"
>
<Spinner size={"3"} />
</div>
) : (
<>
<NavBar />
<Outlet />
</>
)}
</>
);
}

View File

@ -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 (
<Container size={"4"} className="mt-4">
<Text size={"7"}>{t("errors.unauthorized")}</Text>
</Container>
);
}
return (
<>
<Container className="mt-10">
<Editor onChange={setUserInput} />
</Container>
</>
);
}

View File

@ -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",
},
};

View File

@ -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: "Вы должны быть авторизованы, чтобы сделать это",
},
};

View File

@ -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(
<>
<Route
path="/"
errorElement={<ErrorBoundary />}
element={<MainPage />}
>
<Route path="/" errorElement={<ErrorBoundary />} element={<MainPage />}>
<Route index element={<Text size={"5"}>Cringer path</Text>} />
<Route
path="/a?/c"
element={<Text weight={"regular"}>Cringer path, but this a</Text>}
element={
<Text weight={"regular"}>Cringer path, but this a</Text>
}
></Route>
<Route
path="/create"
element={
<PostCreatorPage />
}
></Route>
</Route>
@ -37,4 +47,4 @@ export const routes = createRoutesFromElements(
element={<RegisterPage />}
/>
</>
)
);

View File

@ -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()

View File

@ -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})
}

View File

@ -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})
}

View File

@ -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
}