This commit is contained in:
Max 2024-12-08 00:32:02 +03:00
parent d87663d3d4
commit 9d028a40f1
20 changed files with 625 additions and 100 deletions

244
enshi/package-lock.json generated
View File

@ -8,7 +8,7 @@
"name": "enshi", "name": "enshi",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "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-form": "^0.1.0",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-navigation-menu": "^1.2.1", "@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": { "node_modules/@radix-ui/react-arrow": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", "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": { "node_modules/@radix-ui/react-dialog": {
"version": "1.1.1", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz",
"integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/primitive": "1.1.0", "@radix-ui/primitive": "1.1.0",
"@radix-ui/react-compose-refs": "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-dismissable-layer": "1.1.0", "@radix-ui/react-dismissable-layer": "1.1.1",
"@radix-ui/react-focus-guards": "1.1.0", "@radix-ui/react-focus-guards": "1.1.1",
"@radix-ui/react-focus-scope": "1.1.0", "@radix-ui/react-focus-scope": "1.1.0",
"@radix-ui/react-id": "1.1.0", "@radix-ui/react-id": "1.1.0",
"@radix-ui/react-portal": "1.1.1", "@radix-ui/react-portal": "1.1.2",
"@radix-ui/react-presence": "1.1.0", "@radix-ui/react-presence": "1.1.1",
"@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-slot": "1.1.0", "@radix-ui/react-slot": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0",
"aria-hidden": "^1.1.1", "aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.7" "react-remove-scroll": "2.6.0"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@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": { "node_modules/@radix-ui/react-direction": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", "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": { "node_modules/@radix-ui/themes/node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz",

View File

@ -10,7 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "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-form": "^0.1.0",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-navigation-menu": "^1.2.1", "@radix-ui/react-navigation-menu": "^1.2.1",

View File

@ -1,4 +1,4 @@
import { Theme } from "@radix-ui/themes"; import { Theme, ThemePanel } from "@radix-ui/themes";
import "@radix-ui/themes/styles.css"; import "@radix-ui/themes/styles.css";
import { QueryClientProvider } from "@tanstack/react-query"; import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
@ -17,8 +17,8 @@ export default function App() {
<ToastProvider> <ToastProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<RouterProvider router={router} /> <RouterProvider router={router} />
{/* <ThemePanel /> */} <ThemePanel />
<ReactQueryDevtools /> <ReactQueryDevtools/>
</QueryClientProvider> </QueryClientProvider>
</ToastProvider> </ToastProvider>
</Theme> </Theme>

View File

@ -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 { useQuery } from "@tanstack/react-query";
import { Interweave } from "interweave"; import { Interweave } from "interweave";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
@ -27,6 +37,8 @@ export default function ArticleViewer(props: TArticleViewer) {
return response.data; return response.data;
}, },
gcTime: 0,
refetchOnMount: true,
}); });
if (isPending) return <SkeletonPostLoader />; if (isPending) return <SkeletonPostLoader />;
@ -40,14 +52,19 @@ export default function ArticleViewer(props: TArticleViewer) {
<Text className="mb-2" as="div" size={"9"}> <Text className="mb-2" as="div" size={"9"}>
{data.title} {data.title}
</Text> </Text>
<Flex gap={"3"} className="mt-4 mb-2"> <Flex
gap={"3"}
className="items-center mt-4 mb-2 align-baseline"
>
<Flex gap={"1"}> <Flex gap={"1"}>
<VoteButton <VoteButton
vote={UPVOTE} vote={UPVOTE}
postId={queryParams["postId"] || ""} postId={queryParams["postId"] || ""}
/> />
<VoteCounter postId={queryParams["postId"] || ""} /> <VoteCounter
postId={queryParams["postId"] || ""}
/>
<VoteButton <VoteButton
vote={DOWNVOTE} vote={DOWNVOTE}
@ -60,6 +77,63 @@ export default function ArticleViewer(props: TArticleViewer) {
postId={queryParams["postId"] || ""} postId={queryParams["postId"] || ""}
/> />
</Box> </Box>
<Dialog.Root>
<Dialog.Trigger asChild>
<Button
variant="surface"
className="h-5"
>
Add to blog
</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-blackA6 data-[state=open]:animate-overlayShow" />
<Dialog.Content className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[450px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none data-[state=open]:animate-contentShow">
<Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
Add this post to blog
</Dialog.Title>
<Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
<Flex>
<Text>
{`Add "${data.title}" to blog...`}
</Text>
<Select.Root defaultValue="apple">
<Select.Trigger />
<Select.Content>
<Select.Group>
<Select.Item value="orange">
This
</Select.Item>
<Select.Item value="apple">
This is
updated blog
</Select.Item>
<Select.Item value="grape">
This another
</Select.Item>
</Select.Group>
</Select.Content>
</Select.Root>
</Flex>
</Dialog.Description>
<div className="mt-[25px] flex justify-end">
<Dialog.Close asChild>
<Button>Confirm</Button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button
className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
aria-label="Close"
>
<Cross2Icon />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</Flex> </Flex>
</Flex> </Flex>
<Separator size={"4"} className="mb-2" /> <Separator size={"4"} className="mb-2" />

View File

@ -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 (
<Card className="w-full h-20" onClick={() => navigate(``)}>
<Flex direction={"column"}>
<Heading size={"4"}>{props?.title || "...No title..."}</Heading>
<Flex align={"center"} gap={"2"} mt={"1"}>
<Avatar size={"2"} className="rounded-full" fallback={"SI"} />
<UserNicknameLink userId={props.userId} />
</Flex>
</Flex>
</Card>
);
}

View File

@ -1,15 +1,19 @@
import * as NavigationMenu from "@radix-ui/react-navigation-menu"; import * as NavigationMenu from "@radix-ui/react-navigation-menu";
import { useThemeContext, Button, Heading } from "@radix-ui/themes"; import { Button, Heading, useThemeContext } from "@radix-ui/themes";
import { useNavigate, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
export default function CustomNavigationMenu() { export default function CustomNavigationMenu() {
const {t} = useTranslation()
return ( return (
<div className="flex-1"> <div className="flex-1">
<NavigationMenu.Root orientation="horizontal"> <NavigationMenu.Root orientation="horizontal">
<NavigationMenu.List className="flex items-center justify-start gap-8"> <NavigationMenu.List className="flex items-center justify-start gap-8">
<NavItem text="Home" to="/" /> <NavItem text={t("home")} to="/" />
<NavItem text="Following" to="/c" /> <NavItem text={t("following")} to="/c" />
</NavigationMenu.List> </NavigationMenu.List>
</NavigationMenu.Root> </NavigationMenu.Root>
</div> </div>

View File

@ -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 (
<Skeleton>
<Text>@Nickname</Text>
</Skeleton>
);
return (
<Link to={`/users/${data}`}>
<Text>@{data}</Text>
</Link>
);
}

View File

@ -30,7 +30,8 @@ export default function PostRedactor() {
return error; return error;
} }
}, },
gcTime: Infinity, gcTime: 0,
refetchOnMount: true
}); });
return ( return (

View File

@ -4,6 +4,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { axiosLocalhost } from "../../../../api/axios/axios"; import { axiosLocalhost } from "../../../../api/axios/axios";
import useToast from "../../../../hooks/useToast";
type TSubmitChangesButton = { type TSubmitChangesButton = {
className: string; className: string;
@ -15,6 +16,7 @@ export default function SubmitChangesButton(props: TSubmitChangesButton) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isDisabled, setIsDisabled] = useState(false); const [isDisabled, setIsDisabled] = useState(false);
const createToast = useToast();
const navigate = useNavigate(); const navigate = useNavigate();
const queryParams = useParams(); const queryParams = useParams();
@ -37,6 +39,7 @@ export default function SubmitChangesButton(props: TSubmitChangesButton) {
setIsDisabled(false); setIsDisabled(false);
}, },
onSuccess: () => { onSuccess: () => {
createToast({title: "Post has been changed!"})
navigate("/"); navigate("/");
}, },
}); });

View File

@ -5,7 +5,7 @@ import { useMutation } from "@tanstack/react-query";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; 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 { axiosLocalhost } from "../../../api/axios/axios";
import { userAtom } from "../../../AtomStore/AtomStore"; import { userAtom } from "../../../AtomStore/AtomStore";
import UseCapsLock from "../../../hooks/useCapsLock"; import UseCapsLock from "../../../hooks/useCapsLock";
@ -244,6 +244,21 @@ export default function RegisterPage() {
<Text size={"3"}>{t("submit")}</Text> <Text size={"3"}>{t("submit")}</Text>
</Button> </Button>
</Form.Submit> </Form.Submit>
<Text size={"1"} color="gray" className="block w-full text-center">
{t("alreadyRegistered")}{" "}
<Link to="/login">
<Text className="underline" weight={"bold"}>{t("logIn")}</Text>
</Link>{" "}
{t("now")}
</Text>
<Text size={"1"} color="gray" className="block w-full text-center">
{t("byPressingTheButton")}{" "}
<Link to="/register">
<Text className="underline" weight={"bold"}>{t("termsOfService")}</Text>.
</Link>
</Text>
</Form.Root> </Form.Root>
</Card> </Card>
); );

View File

@ -1,6 +1,7 @@
import * as ScrollArea from "@radix-ui/react-scroll-area"; 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 { useQuery } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { GetRandomPostsRow } from "../../@types/PostTypes"; import { GetRandomPostsRow } from "../../@types/PostTypes";
import { axiosLocalhost } from "../../api/axios/axios"; import { axiosLocalhost } from "../../api/axios/axios";
import PostCard from "./PostCard/PostCard"; import PostCard from "./PostCard/PostCard";
@ -8,6 +9,8 @@ import PostCard from "./PostCard/PostCard";
const LIMIT = 10; const LIMIT = 10;
export default function RandomPostsPage() { export default function RandomPostsPage() {
const {t} = useTranslation()
const { data, refetch } = useQuery({ const { data, refetch } = useQuery({
queryKey: ["random_posts_key"], queryKey: ["random_posts_key"],
queryFn: async () => { queryFn: async () => {
@ -27,30 +30,38 @@ export default function RandomPostsPage() {
return ( return (
<> <>
<ScrollArea.Root className="w-full overflow-hidden grow-1"> <Flex direction={"column"} className="mx-auto">
<ScrollArea.Viewport className="rounded size-full"> <Heading size={"9"} weight={"regular"} className="text-center">
{data?.map((post, i) => { {t("discover")}
return ( </Heading>
<Container size={"3"} key={`post${i}`}>
<PostCard post={post} /> <Separator size={"4"} className="my-8" />
</Container>
); <ScrollArea.Root className="w-full h-full overflow-hidden">
})} <ScrollArea.Viewport className="overflow-scroll rounded size-full">
</ScrollArea.Viewport> {data?.map((post, i) => {
<ScrollArea.Scrollbar return (
className="flex touch-none select-none bg-blackA3 p-0.5 transition-colors duration-[160ms] ease-out hover:bg-blackA5 data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col" <Container size={"3"} key={`post${i}`}>
orientation="vertical" <PostCard post={post} />
> </Container>
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-mauve10 before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-11 before:min-w-11 before:-translate-x-1/2 before:-translate-y-1/2" /> );
</ScrollArea.Scrollbar> })}
<ScrollArea.Scrollbar </ScrollArea.Viewport>
className="flex touch-none select-none bg-blackA3 p-0.5 transition-colors duration-[160ms] ease-out hover:bg-blackA5 data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col" <ScrollArea.Scrollbar
orientation="horizontal" className="z-50 flex touch-none select-none p-0.5 w-2"
> orientation="vertical"
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-mauve10 before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-[44px] before:min-w-[44px] before:-translate-x-1/2 before:-translate-y-1/2" /> >
</ScrollArea.Scrollbar> <ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-slate-200"/>
<ScrollArea.Corner className="bg-blackA5" /> </ScrollArea.Scrollbar>
</ScrollArea.Root> {/* <ScrollArea.Scrollbar
className="flex touch-none select-none bg-blackA3 p-0.5 transition-colors duration-[160ms] ease-out hover:bg-blackA5 data-[orientation=horizontal]:h-2.5 data-[orientation=vertical]:w-2.5 data-[orientation=horizontal]:flex-col"
orientation="horizontal"
>
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-mauve10 before:absolute before:left-1/2 before:top-1/2 before:size-full before:min-h-[44px] before:min-w-[44px] before:-translate-x-1/2 before:-translate-y-1/2" />
</ScrollArea.Scrollbar> */}
{/* <ScrollArea.Corner className="bg-blackA5" /> */}
</ScrollArea.Root>
</Flex>
</> </>
); );
} }

View File

@ -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 (
<Card className='w-full h-20'
onClick={() => navigate(``)}
>
{props?.title || "...No title..."}
{props?.blogId || "adqwwd"}
</Card>
)
}

View File

@ -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 { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { userAtom } from "../../AtomStore/AtomStore";
import { axiosLocalhost } from "../../api/axios/axios"; 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"; import SkeletonBoxes from "./SkeletonBoxes/SkeletonBoxes";
export default function UserBlogsPage() { 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({ const { data, isPending, isFetching } = useQuery({
queryKey: ["userBlogs"], queryKey: ["userBlogs"],
queryFn: async () => { queryFn: async () => {
@ -34,13 +22,13 @@ export default function UserBlogsPage() {
transformResponse: [(data) => data], transformResponse: [(data) => data],
}); });
let temp = parseWithBigInt(response.data, isBigNumber); let temp = JSONWithInt64(response.data);
return temp as any[]; return temp as any[];
}, },
}); });
if (isFetching) if (isPending)
return ( return (
<Container size={"1"}> <Container size={"1"}>
<SkeletonBoxes /> <SkeletonBoxes />
@ -51,17 +39,86 @@ export default function UserBlogsPage() {
<Box className="size-full"> <Box className="size-full">
<Container size={"1"}> <Container size={"1"}>
<Flex direction={"column"} gap={"2"}> <Flex direction={"column"} gap={"2"}>
<Text size={"9"} className="text-center">
Your blogs
</Text>
<Separator size={"4"} className="my-2" />
{data {data
? data?.map((blog: any, b) => { ? data?.map((blog: any, b) => {
return ( return (
<BlogBox <>
key={b} <BlogBox
title={blog.title} key={b}
blogId={blog.blog_id} title={blog.title}
/> blogId={blog.blog_id}
userId={blog.user_id}
/>
</>
); );
}) })
: null} : null}
<Dialog.Root>
<Dialog.Trigger asChild>
<Button onClick={() => {}}>
<PlusIcon />
</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-blackA6 data-[state=open]:animate-overlayShow" />
<Dialog.Content className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[450px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none data-[state=open]:animate-contentShow">
<Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
Create blog
</Dialog.Title>
<Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
Create your new blog.
</Dialog.Description>
<fieldset className="mb-[15px] flex items-center gap-5">
<label
className="w-[90px] text-right text-[15px] text-violet11"
htmlFor="title"
>
Blog title
</label>
<input
className="inline-flex h-[35px] w-full flex-1 items-center justify-center rounded px-2.5 text-[15px] leading-none text-violet11 shadow-[0_0_0_1px] shadow-violet7 outline-none focus:shadow-[0_0_0_2px] focus:shadow-violet8"
id="title"
defaultValue="My blog"
/>
</fieldset>
<fieldset className="mb-[15px] flex items-center gap-5">
<label
className="w-[90px] text-right text-[15px] text-violet11"
htmlFor="Description"
>
Description
</label>
<textarea
className="pt-2 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded px-2.5 text-[15px] leading-none text-violet11 shadow-[0_0_0_1px] shadow-violet7 outline-none focus:shadow-[0_0_0_2px] focus:shadow-violet8"
id="Description"
placeholder="Your description..."
/>
</fieldset>
<div className="mt-[25px] flex justify-end">
<Dialog.Close asChild>
<Button>
Create blog
</Button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button
className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
aria-label="Close"
>
<Cross2Icon />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</Flex> </Flex>
</Container> </Container>
</Box> </Box>

View File

@ -20,10 +20,24 @@ const en = {
registerForm: "Register", registerForm: "Register",
loginForm: "Log in", loginForm: "Log in",
alreadyRegistered: "Already registered?",
suggestRegister: "Don't have an account?", suggestRegister: "Don't have an account?",
register: "Register", register: "Register",
now: "now!", now: "now!",
logIn: "Log in",
updatePost: "Update",
byPressingTheButton: "By pressing the submit button you agree with our",
termsOfService: "Terms Of Service",
discover: "Discover something new...",
home: "Home",
following: "Following",
errors: { errors: {
enterUsername: "Please enter your username", enterUsername: "Please enter your username",
enterEmail: "Please enter your email", enterEmail: "Please enter your email",

View File

@ -21,10 +21,25 @@ const ru = {
registerForm: "Регистрация", registerForm: "Регистрация",
loginForm: "Вход", loginForm: "Вход",
alreadyRegistered: "Уже есть аккаунт?",
suggestRegister: "Не зарегистрированы?", suggestRegister: "Не зарегистрированы?",
register: "Создайте аккаунт", register: "Создайте аккаунт",
now: "сейчас!", now: "сейчас!",
logIn: "Войдите",
byPressingTheButton: "Нажимая `Подтвердить`, вы соглашаетесь с нашими",
termsOfService: "Условиями предоставления услуг.",
updatePost: "Изменить",
discover: "Найдите что-то новое",
home: "Главная",
following: "Отслеживаемые",
errors: { errors: {
enterUsername: "Это обязательное поле", enterUsername: "Это обязательное поле",
enterEmail: "Это обязательное поле", enterEmail: "Это обязательное поле",

19
enshi/src/utils/idnex.ts Normal file
View File

@ -0,0 +1,19 @@
const isBigNumber = (num: any) => !Number.isSafeInteger(+num);
const enquoteBigNumber = (jsonString: any, bigNumChecker: any) =>
jsonString.replaceAll(
/([:\s\[,]*)(\d+)([\s,\]]*)/g,
(matchingSubstr: any, prefix: any, bigNum: any, suffix: any) =>
bigNumChecker(bigNum)
? `${prefix}"${bigNum}"${suffix}`
: matchingSubstr
);
const parseWithBigInt = (jsonString: any, bigNumChecker: any) =>
JSON.parse(enquoteBigNumber(jsonString, bigNumChecker), (_key, value) =>
!isNaN(value) && bigNumChecker(value) ? BigInt(value).toString() : value
);
export const JSONWithInt64 = (jsonString: any) => {
return parseWithBigInt(jsonString, isBigNumber);
};

View File

@ -152,6 +152,17 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
return i, err return i, err
} }
const getUserUsernameById = `-- name: GetUserUsernameById :one
SELECT username FROM users WHERE user_id = $1
`
func (q *Queries) GetUserUsernameById(ctx context.Context, userID int64) (string, error) {
row := q.db.QueryRow(ctx, getUserUsernameById, userID)
var username string
err := row.Scan(&username)
return username, err
}
const updateUserPasswordHash = `-- name: UpdateUserPasswordHash :one const updateUserPasswordHash = `-- name: UpdateUserPasswordHash :one
UPDATE public.users UPDATE public.users
SET "password"=$1 SET "password"=$1

View File

@ -4,6 +4,9 @@ SELECT * FROM users;
-- name: GetUserById :one -- name: GetUserById :one
SELECT * FROM users WHERE user_id = $1; SELECT * FROM users WHERE user_id = $1;
-- name: GetUserUsernameById :one
SELECT username FROM users WHERE user_id = $1;
-- name: GetUserByUsername :one -- name: GetUserByUsername :one
SELECT * FROM users WHERE username = $1; SELECT * FROM users WHERE username = $1;

View File

@ -8,6 +8,7 @@ import (
bookmarksroutes "enshi/routes/bookmarksRoutes" bookmarksroutes "enshi/routes/bookmarksRoutes"
"enshi/routes/postsRoutes" "enshi/routes/postsRoutes"
"enshi/routes/userProfileRoutes" "enshi/routes/userProfileRoutes"
userroutes "enshi/routes/userRoutes"
voteroutes "enshi/routes/voteRoutes" voteroutes "enshi/routes/voteRoutes"
"net/http" "net/http"
"strings" "strings"
@ -176,10 +177,16 @@ func SetupRotes(g *gin.Engine) error {
temporal := g.Group("/") temporal := g.Group("/")
temporal.Use(middleware.AuthMiddleware()) temporal.Use(middleware.AuthMiddleware())
temporal.GET( temporal.GET(
"/user/blogs", "/user/blogs",
blogRoutes.GetUserBlogs, blogRoutes.GetUserBlogs,
) )
freeGroup.GET(
"/user/:user-id",
userroutes.GetUserUsername,
)
return nil return nil
} }

View File

@ -0,0 +1,30 @@
package userroutes
import (
"context"
rest_api_stuff "enshi/REST_API_stuff"
db_repo "enshi/db/go_queries"
"enshi/db_connection"
"enshi/middleware/getters"
"net/http"
"github.com/gin-gonic/gin"
)
func GetUserUsername(c *gin.Context) {
userId, err := getters.GetInt64Param(c, "user-id")
if err != nil {
rest_api_stuff.BadRequestAnswer(c, err)
}
userInfo, err := db_repo.New(db_connection.Dbx).GetUserUsernameById(
context.Background(),
userId,
)
if err != nil {
rest_api_stuff.InternalErrorAnswer(c, err)
}
c.IndentedJSON(http.StatusOK, userInfo)
}