Localization | edit page

This commit is contained in:
Max 2024-11-23 20:49:12 +03:00
parent be99c53c69
commit 7c38c6ca9b
21 changed files with 220 additions and 73 deletions

View File

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

View File

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

View File

@ -19,6 +19,10 @@
text-transform: uppercase;
}
* {
font-family: "Times New Roman";
}
/*!
* Quill Editor v1.3.6
* https://quilljs.com/

View File

@ -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 (
<>
<div className="ql-snow">
<Container className="mt-4 ql-editor">
<Interweave content={props?.htmlToParse || ""} />
</Container>
</div>
</>

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import { userAtom } from "../../../../AtomStore/AtomStore";
export default function UserButton() {
const user = useAtomValue(userAtom);
const {t} = useTranslation()
const { t } = useTranslation();
return (
<div className="">
@ -26,7 +26,7 @@ export default function UserButton() {
<DropdownMenu.Content className="w-fit">
<DropdownMenu.Item>
<Link to={"/profile"}>
<Link to={"/user/:user-id/profile"}>
<Flex className="justify-between gap-2">
<Icon>
<PersonIcon />
@ -38,12 +38,14 @@ export default function UserButton() {
</DropdownMenu.Item>
<DropdownMenu.Item>
<Flex className="justify-between gap-2">
<Icon>
<LaptopIcon />
</Icon>
<Text>{t("yourBlogs")}</Text>
</Flex>
<Link to={"/user/blogs"}>
<Flex className="justify-between gap-2">
<Icon>
<LaptopIcon />
</Icon>
<Text>{t("yourBlogs")}</Text>
</Flex>
</Link>
</DropdownMenu.Item>
<DropdownMenu.Separator />
@ -57,12 +59,14 @@ export default function UserButton() {
<Text>{t("signOut")}</Text>
</Flex>
) : (
<Flex className="justify-between gap-2">
<Icon>
<EnterIcon />
</Icon>
<Text>{t("signIn")}</Text>
</Flex>
<Link to={"/login"}>
<Flex className="justify-between gap-2">
<Icon>
<EnterIcon />
</Icon>
<Text>{t("signIn")}</Text>
</Flex>
</Link>
)}
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

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

View File

@ -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 (
<Card
size={"2"}
className="absolute w-1/5
className="absolute w-1/4
left-[50%] top-[50%]
translate-x-[-50%] translate-y-[-50%]"
>
@ -66,6 +69,7 @@ export default function LoginPage() {
{t("loginForm")}
</Heading>
<Form.Root
className="flex flex-col gap-2"
onSubmit={(e) => {
e.preventDefault();
let formData = new FormData(
@ -80,10 +84,10 @@ export default function LoginPage() {
logInMutation.mutate(loginData);
}}
>
<Form.Field className="mb-2.5 gap-0.5 grid" name="username">
<Form.Field className="gap-0.5 grid" name="username">
<div className="flex items-baseline justify-between gap-2">
<Form.Label>
<Text size={"4"}>{t("username")}</Text>
<Text size={"3"}>{t("username")}</Text>
</Form.Label>
<Form.Message match="valueMissing">
<Text color="red">{t("errors.enterUsername")}</Text>
@ -115,7 +119,7 @@ export default function LoginPage() {
<Form.Field className="mb-2.5 gap-0.5 grid" name="password">
<div className="flex items-baseline justify-between gap-2">
<Form.Label>
<Text size={"4"}>{t("password")}</Text>
<Text size={"3"}>{t("password")}</Text>
</Form.Label>
<Form.Message match="valueMissing">
<Text color="red">{t("errors.enterPassword")}</Text>
@ -125,6 +129,7 @@ export default function LoginPage() {
<TextField.Root
type={showPassword ? "text" : "password"}
required
autoComplete="on"
>
<Form.ValidityState>
{(validity) => (
@ -157,10 +162,18 @@ export default function LoginPage() {
</Text>
<Form.Submit className="flex justify-center" asChild>
<Button type="submit" className="w-1/3 m-auto">
<Button type="submit" className="w-full p-4 m-auto">
<Text size={"3"}>{t("submit")}</Text>
</Button>
</Form.Submit>
<Text size={"1"} color="gray" className="block w-full text-center">
{t("suggestRegister")}{" "}
<Link to="/register">
<Text className="underline" weight={"bold"}>{t("register")}</Text>
</Link>{" "}
{t("now")}
</Text>
</Form.Root>
</Card>
);

View File

@ -59,6 +59,7 @@ export default function RegisterPage() {
{t("registerForm")}
</Heading>
<Form.Root
className="flex flex-col gap-2"
onSubmit={(e) => {
e.preventDefault();
let formData = new FormData(
@ -74,7 +75,7 @@ export default function RegisterPage() {
registerMutation.mutate(registerData);
}}
>
<Form.Field className="mb-2.5 gap-0.5 grid" name="username">
<Form.Field className="gap-0.5 grid" name="username">
<div className="flex items-baseline justify-between gap-2">
<Form.Label>
<Text size={"4"}>{t("username")}</Text>
@ -106,7 +107,7 @@ export default function RegisterPage() {
</Form.Control>
</Form.Field>
<Form.Field className="mb-2.5 gap-0.5 grid" name="email">
<Form.Field className="gap-0.5 grid" name="email">
<div className="flex items-baseline justify-between gap-2">
<Form.Label>
<Text size={"4"}>{t("email")}</Text>
@ -141,7 +142,7 @@ export default function RegisterPage() {
</Form.Control>
</Form.Field>
<Form.Field className="mb-2.5 gap-0.5 grid" name="password">
<Form.Field className="gap-0.5 grid" name="password">
<div className="flex items-baseline justify-between gap-2">
<Form.Label>
<Text size={"4"}>{t("password")}</Text>
@ -182,7 +183,7 @@ export default function RegisterPage() {
</Form.Field>
<Form.Field
className="mb-2.5 gap-0.5 grid"
className="gap-0.5 grid"
name="conf-password"
>
<div className="flex items-baseline justify-between">
@ -237,8 +238,8 @@ export default function RegisterPage() {
{t("errors.invalidRegisterData")}
</Text>
<Form.Submit className="flex justify-center" asChild>
<Button type="submit" className="w-1/3 m-auto">
<Form.Submit className="flex justify-center mt-2" asChild>
<Button type="submit" className="w-full m-auto">
<Text size={"3"}>{t("submit")}</Text>
</Button>
</Form.Submit>

View File

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

View File

@ -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 (
<Container size={"4"} className="mt-4">
<Text size={"7"}>{t("errors.unauthorized")}</Text>
</Container>
);
}
return (
<>
<Container className="mt-10">
<input
placeholder={"Post title"}
className="mb-2 border-0 border-b-[1px]
outline-none w-full border-b-gray-400
text-[60px] pl-4 pr-4 font-times"
></input>
<Editor onChange={setUserInput} />
<ArticleViewer htmlToParse={userInput} />
</Container>
</>
);

View File

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

View File

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

View File

@ -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: "Неверное имя пользователя или пароль",

View File

@ -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(
<App />

View File

@ -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(
<Route
path="/create"
element={
<PostCreatorPage />
<AuthPageWrapper>
<PostCreatorPage />
</AuthPageWrapper>
}
></Route>
</Route>

View File

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

View File

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

25
package-lock.json generated
View File

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

View File

@ -1,5 +1,6 @@
{
"dependencies": {
"@radix-ui/react-icons": "^1.3.2"
"@radix-ui/react-icons": "^1.3.2",
"interweave": "^13.1.0"
}
}