Compare commits

..

19 Commits

Author SHA1 Message Date
Max
1659e007e4 Post card improvement 2025-02-09 11:49:37 +03:00
Max
d1474bce35 Small imp 2025-02-07 19:41:06 +03:00
Max
b82dddc25d Small imp 2025-02-07 19:36:53 +03:00
Max
f176741afa Card improvement 2025-02-07 19:35:30 +03:00
Max
f70582ce83 Tweaking at home page 2025-02-06 23:20:14 +03:00
Max
c36e4a7f7d Infinite scrolling setup 2025-02-05 18:27:21 +03:00
Max
3bb21f67e9 Infinite scrolling setup 2025-02-05 18:08:10 +03:00
Max
facaa96955 wtf 2025-02-05 15:13:15 +03:00
Max
6a22797610 Normal tooltip | Fixed quill redactor 2025-02-05 11:34:33 +03:00
Max
6d6babc305 Another 2025-02-04 22:05:48 +03:00
Max
474d76840b Another 2025-02-04 22:05:37 +03:00
Max
ee24349680 Some improvements to visuals 2025-02-04 22:05:00 +03:00
Max
c0dcbcf2c5 Added badge migration 2025-02-04 21:33:08 +03:00
Max
547bc9e9e9 Support of go migration 2025-02-04 20:17:55 +03:00
Max
ad1aff3692 Visual improvements 2025-02-03 23:42:08 +03:00
Max
6054bc0403 Some improvements 2025-02-02 17:48:11 +03:00
Max
0eb840b82a Justifyed text 2025-02-02 16:34:32 +03:00
Max
7edaa62cdf Scrollable profile page 2025-02-02 16:18:40 +03:00
Max
f99e39c712 Profile page barebones 2025-02-02 14:33:49 +03:00
79 changed files with 1989 additions and 499 deletions

12
.vscode/settings.json vendored
View File

@ -3,5 +3,17 @@
"downvotes",
"godotenv",
"upvotes"
],
"sqltools.connections": [
{
"previewLimit": 50,
"server": "nekiiinkognito.ru",
"port": 5432,
"askForPassword": true,
"driver": "PostgreSQL",
"name": "enshi",
"database": "postgres",
"username": "neki"
}
]
}

View File

@ -1,4 +1,7 @@
{
"tabWidth": 4,
"useTabs": false
"useTabs": true,
"printWidth": 80,
"semi": true,
"quoteProps": "consistent"
}

View File

@ -1,21 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap"
rel="stylesheet"
/>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap"
rel="stylesheet"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Enshi</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<title>Enshi</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

142
enshi/package-lock.json generated
View File

@ -1,13 +1,14 @@
{
"name": "enshi",
"version": "0.1.7",
"version": "0.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "enshi",
"version": "0.1.7",
"version": "0.1.8",
"dependencies": {
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-form": "^0.1.0",
"@radix-ui/react-icons": "^1.3.2",
@ -18,7 +19,9 @@
"@radix-ui/themes": "^3.1.3",
"@tanstack/react-query": "^5.55.0",
"@tanstack/react-query-devtools": "^5.61.0",
"@types/quill": "^2.0.14",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"html-react-parser": "^5.1.16",
"i18n": "^0.15.1",
"i18next": "^23.14.0",
@ -28,10 +31,11 @@
"jotai": "^2.9.3",
"jotai-immer": "^0.4.1",
"primereact": "^10.8.2",
"quill": "^2.0.2",
"quill": "^2.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.1",
"react-intersection-observer": "^9.15.1",
"react-quill": "^2.0.0",
"react-router-dom": "^6.26.2"
},
@ -1294,12 +1298,12 @@
}
},
"node_modules/@radix-ui/react-aspect-ratio": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.0.tgz",
"integrity": "sha512-dP87DM/Y7jFlPgUZTlhx6FF5CEzOiaxp2rBCKlaXlpH5Ip/9Fg5zZ9lDOQ5o/MOfUlf36eak14zoWYpgcgGoOg==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.2.tgz",
"integrity": "sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0"
"@radix-ui/react-primitive": "2.0.2"
},
"peerDependencies": {
"@types/react": "*",
@ -1316,6 +1320,62 @@
}
}
},
"node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
"integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
"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-aspect-ratio/node_modules/@radix-ui/react-primitive": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.1.2"
},
"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-aspect-ratio/node_modules/@radix-ui/react-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.1"
},
"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-avatar": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz",
@ -2933,6 +2993,29 @@
}
}
},
"node_modules/@radix-ui/themes/node_modules/@radix-ui/react-aspect-ratio": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.0.tgz",
"integrity": "sha512-dP87DM/Y7jFlPgUZTlhx6FF5CEzOiaxp2rBCKlaXlpH5Ip/9Fg5zZ9lDOQ5o/MOfUlf36eak14zoWYpgcgGoOg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.0.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@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",
@ -3422,12 +3505,13 @@
"license": "MIT"
},
"node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-2.0.14.tgz",
"integrity": "sha512-zvoXCRnc2Dl8g+7/9VSAmRWPN6oH+MVhTPizmCR+GJCITplZ5VRVzMs4+a/nOE3yzNwEZqylJJrMB07bwbM1/g==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
"parchment": "^1.1.2",
"quill-delta": "^5.1.0"
}
},
"node_modules/@types/quill/node_modules/parchment": {
@ -4213,6 +4297,12 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
@ -6409,9 +6499,9 @@
"license": "MIT"
},
"node_modules/quill": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz",
"integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
"integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
"license": "BSD-3-Clause",
"dependencies": {
"eventemitter3": "^5.0.1",
@ -6484,6 +6574,21 @@
}
}
},
"node_modules/react-intersection-observer": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/react-intersection-observer/-/react-intersection-observer-9.15.1.tgz",
"integrity": "sha512-vGrqYEVWXfH+AGu241uzfUpNK4HAdhCkSAyFdkMb9VWWXs6mxzBLpWCxEy9YcnDNY2g9eO6z7qUtTBdA9hc8pA==",
"license": "MIT",
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -6511,6 +6616,15 @@
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/react-quill/node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
}
},
"node_modules/react-quill/node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",

View File

@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-form": "^0.1.0",
"@radix-ui/react-icons": "^1.3.2",
@ -21,7 +22,9 @@
"@radix-ui/themes": "^3.1.3",
"@tanstack/react-query": "^5.55.0",
"@tanstack/react-query-devtools": "^5.61.0",
"@types/quill": "^2.0.14",
"axios": "^1.7.7",
"dayjs": "^1.11.13",
"html-react-parser": "^5.1.16",
"i18n": "^0.15.1",
"i18next": "^23.14.0",
@ -31,10 +34,11 @@
"jotai": "^2.9.3",
"jotai-immer": "^0.4.1",
"primereact": "^10.8.2",
"quill": "^2.0.2",
"quill": "^2.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^15.0.1",
"react-intersection-observer": "^9.15.1",
"react-quill": "^2.0.0",
"react-router-dom": "^6.26.2"
},

View File

@ -4,5 +4,25 @@ export type GetRandomPostsRow = {
user_id: string;
title: string;
// created_at: Date;
}
};
export type TPostData = {
title: string;
content: string;
};
export type Post = {
blog_id: string | null;
created_at: string; // ISO 8601 date string
post_id: string;
title: string;
user_id: string;
content: string;
};
export type SelectedPostsResponse = {
selected_posts: Post[];
has_next_page: boolean;
next_page_index: number;
prev_page_index: number;
};

View File

@ -3,3 +3,24 @@ export type TUser = {
isAdmin: boolean;
id?: string | number;
}
export type TInfoUser = {
user_id: number;
username: string;
email: string;
createdAt: string;
is_admin: boolean;
display_name: string;
};
export type TProfile = {
user_id: number;
bio: string;
avatar_url: string;
website_url: string;
};
export type TGetUserInfoResponse = {
user_info: TInfoUser;
profile_info: TProfile;
};

View File

@ -426,6 +426,16 @@
content: '';
display: table;
}
.ql-toolbar {
position: relative;
width: 100%;
}
.ql-container {
overflow-y: auto;
}
.ql-snow.ql-toolbar button,
.ql-snow .ql-toolbar button {
background: none;

View File

@ -1,23 +1,33 @@
import { Theme, ThemePanel } from "@radix-ui/themes";
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 "axios";
import dayjs from "dayjs";
import relativeTime from 'dayjs/plugin/relativeTime';
import { useAtomValue } from "jotai";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import queryClient from "./api/QueryClient/QueryClient";
import "./App.css";
import { themeAtom } from "./AtomStore/AtomStore";
import ToastProvider from "./Components/ToastProvider/ToastProvider";
import { routes } from "./routes/routes";
dayjs.extend(relativeTime)
const router = createBrowserRouter(routes);
export default function App() {
const theme = useAtomValue(themeAtom);
return (
<Theme className="h-fit" accentColor="sky" grayColor="slate" appearance="dark">
<Theme className="h-fit" accentColor="sky" grayColor="slate" appearance={theme}>
<ToastProvider>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<ThemePanel />
{/* <ThemePanel /> */}
<ReactQueryDevtools/>
</QueryClientProvider>
</ToastProvider>

View File

@ -1,5 +1,6 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";
import { TPostData } from "../@types/PostTypes";
import { TUser } from "../@types/UserType";
export const userAtom = atom<TUser>();
@ -7,10 +8,18 @@ export const userAtom = atom<TUser>();
export const postCreationAtom = atom<string>();
export const postCreationTitleAtom = atom<string>();
type TPostData = {
title: string;
content: string;
};
export const themeAtom = atomWithStorage<"light" | "dark">(
"theme",
"light",
{
getItem: (key) => localStorage.getItem(key) as any,
setItem: (key, value) => localStorage.setItem(key, value as any),
removeItem: (key) => localStorage.removeItem(key),
},
{
getOnInit: true,
}
);
export const storagePostAtom = atomWithStorage<TPostData>(
"draft-post",

View File

@ -1,4 +1,4 @@
import { Box, Container, Flex, Separator, Text } from "@radix-ui/themes";
import { Box, Flex, ScrollArea, Separator, Text } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import { Interweave } from "interweave";
import { useAtomValue } from "jotai";
@ -12,71 +12,67 @@ import VoteButton, { DOWNVOTE, UPVOTE } from "./VoteButton/VoteButton";
import VoteCounter from "./VoteCounter/VoteCounter";
type TArticleViewer = {
htmlToParse?: string;
htmlToParse?: string;
};
export default function ArticleViewer(props: TArticleViewer) {
let queryParams = useParams();
const user = useAtomValue(userAtom);
let queryParams = useParams();
const user = useAtomValue(userAtom);
const { data, isPending } = useQuery({
queryKey: [`post_${queryParams["postId"]}`],
queryFn: async () => {
const response = await axiosLocalhost.get(
`posts/${queryParams["postId"]}`
);
const { data, isPending } = useQuery({
queryKey: [`post_${queryParams["postId"]}`],
queryFn: async () => {
const response = await axiosLocalhost.get(
`posts/${queryParams["postId"]}`
);
return response.data;
},
gcTime: 0,
refetchOnMount: true,
});
return response.data;
},
gcTime: 0,
refetchOnMount: true,
});
if (isPending) return <SkeletonPostLoader />;
if (isPending) return <SkeletonPostLoader />;
return (
<>
<Container size={"3"}>
<div className="ql-snow ql-editor">
<Container size={"2"} className="mt-4">
<Flex direction={"column"}>
<Text className="mb-2" as="div" size={"9"}>
{data.title}
</Text>
<Flex
gap={"3"}
className="items-center mt-4 mb-2 align-baseline"
>
<Flex gap={"1"}>
<VoteButton
vote={UPVOTE}
postId={queryParams["postId"] || ""}
/>
return (
<ScrollArea className="p-0 mx-auto overflow-hidden ql-editor max-w-pc-width">
<Flex direction={"column"} className="overflow-hidden">
<Text className="mb-2" as="div" size={"9"}>
{data.title}
</Text>
<Flex
gap={"3"}
className="items-center mt-4 mb-2 overflow-hidden align-baseline"
>
<Flex gap={"1"}>
<VoteButton
vote={UPVOTE}
postId={queryParams["postId"] || ""}
/>
<VoteCounter
postId={queryParams["postId"] || ""}
/>
<VoteCounter postId={queryParams["postId"] || ""} />
<VoteButton
vote={DOWNVOTE}
postId={queryParams["postId"] || ""}
/>
</Flex>
<VoteButton
vote={DOWNVOTE}
postId={queryParams["postId"] || ""}
/>
</Flex>
<Box hidden={data.user_id != user?.id}>
<ChangePostButton
postId={queryParams["postId"] || ""}
/>
</Box>
<Box hidden={data.user_id != user?.id}>
<ChangePostButton
postId={queryParams["postId"] || ""}
/>
</Box>
{user ? <AddPostToBlogDialog /> : null}
</Flex>
</Flex>
<Separator size={"4"} className="mb-2" />
<Interweave content={data.content} />
</Container>
</div>
</Container>
</>
);
{user ? <AddPostToBlogDialog /> : null}
</Flex>
</Flex>
<Separator size={"4"} className="my-2" />
<Text>
<Interweave content={data.content} />
</Text>
</ScrollArea>
);
}

View File

@ -1,7 +1,5 @@
import Sources from "quill";
import Quill, { Delta } from "quill/core";
import { forwardRef, useEffect, useRef, useState } from "react";
import ReactQuill from "react-quill";
import { Delta } from "quill/core";
import { forwardRef } from "react";
type TEditor = {
readOnly?: boolean;
@ -30,61 +28,55 @@ const modules = {
* @param onChange - function that accepts Delta element
*/
const Editor = forwardRef((props: TEditor) => {
const editor = useRef(null);
const [quill, setQuill] = useState<Quill | null>(null);
const [value, setValue] = useState(new Delta());
// const editor = useRef(null);
// const [quill, setQuill] = useState<Quill | null>(null);
// const [value, setValue] = useState(new Delta());
const [loaded, setLoaded] = useState(false);
// const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (editor.current) {
//@ts-ignore
const temp = editor.current.getEditor() as Quill;
setQuill(temp);
}
return () => {
setQuill(null);
};
}, [editor.current]);
// useEffect(() => {
// if (editor.current) {
// //@ts-ignore
// const temp = editor.current.getEditor() as Quill;
// setQuill(temp);
// }
// return () => {
// setQuill(null);
// };
// }, [editor.current]);
useEffect(() => {
const quill = new Quill(document.createElement("div"));
const t = quill.clipboard.convert({
html: props.defaultValue as string,
}) as Delta;
// useEffect(() => {
// const quill = new Quill(document.createElement("div"));
// console.log(`AMOOOGUS`, props.defaultValue);
if (!loaded) {
setValue(t);
// const t = quill.clipboard.convert({
// html: props.defaultValue as string,
// }) as Delta;
console.log(t);
}
// if (!loaded) {
// setValue(t);
// console.log(t);
// }
setLoaded(true);
}, [props.defaultValue]);
// 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 || "");
if (loaded) setValue(fullDelta || new Delta());
};
// 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 || "");
// if (loaded) setValue(fullDelta || new Delta());
// };
return (
<div className="text-editor">
<ReactQuill
value={value}
ref={editor}
modules={modules}
onChange={changeHandler}
theme="snow"
placeholder="Type your thoughts here..."
/>
<div className="text-editor h-[400px]">
DEPRECATED
</div>
);
});

View File

@ -0,0 +1,85 @@
import { Delta, default as Quill, default as Sources } from "quill";
import "quill/dist/quill.snow.css"; // make sure to import Quill's CSS
import { useEffect, useRef } from "react";
type TEditor = {
readOnly?: boolean;
defaultValue?: string | Delta;
onChange?: (html: string) => void;
onSelectionChange?: (range: any) => void;
};
const modules = {
toolbar: [
[{ header: [1, 2, 3, 4, 5, false] }],
["bold", "italic", "underline", "strike", "blockquote", "span-wrapper"],
[
{ list: "ordered" },
{ list: "bullet" },
{ indent: "-1" },
{ indent: "+1" },
],
["link", "image"],
["clean"],
[{ align: [] }],
],
};
const TrueEditor = (props: TEditor) => {
const containerRef = useRef<HTMLDivElement>(null);
const quillRef = useRef<Quill | null>(null);
useEffect(() => {
if (containerRef.current) {
quillRef.current = new Quill(containerRef.current, {
modules,
theme: "snow",
readOnly: props.readOnly || false,
placeholder: "Type your thoughts here...",
});
if (props.defaultValue) {
if (typeof props.defaultValue === "string") {
const delta = quillRef.current.clipboard.convert({ html: props.defaultValue });
quillRef.current.setContents(delta, "silent");
} else {
quillRef.current.setContents(props.defaultValue, "silent");
}
}
quillRef.current.on(
"text-change",
(_delta: Delta, _oldDelta: Delta, _source: Sources) => {
if (props.onChange) {
const html =
containerRef.current?.querySelector(".ql-editor")?.innerHTML ||
"";
props.onChange(html);
}
}
);
if (props.onSelectionChange) {
quillRef.current.on("selection-change", (range, _oldRange, _source) => {
if(props.onSelectionChange) props.onSelectionChange(range);
});
}
}
return () => {
if (quillRef.current) {
quillRef.current.off("text-change");
quillRef.current.off("selection-change");
quillRef.current = null;
}
};
}, []);
return (
<div className="flex flex-col w-full mx-auto overflow-hidden text-editor flex-grow-1 max-w-pc-width">
<div ref={containerRef} />
</div>
);
};
export default TrueEditor;

View File

@ -1,15 +1,18 @@
import { Flex } from "@radix-ui/themes";
import CustomNavigationMenu from "./NavigationMenu/NavigationMenu";
import RightButtonBar from "./RightButtonBar/RightButtonBar";
import SearchField from "./SearchField/SearchField";
export default function NavBar() {
return (
<nav className="flex justify-center pt-2 pb-2 ml-4 mr-4 flex-[1] max-h-fit">
<CustomNavigationMenu />
<Flex className="justify-center w-full">
<nav className="flex justify-center pt-2 pb-2 ml-4 mr-4 mb-4 mt-2 flex-[1] max-h-fit max-w-pc-width">
<CustomNavigationMenu />
<SearchField />
<SearchField />
<RightButtonBar />
</nav>
<RightButtonBar />
</nav>
</Flex>
);
}

View File

@ -9,8 +9,8 @@ export default function CustomNavigationMenu() {
return (
<div className="flex-1">
<NavigationMenu.Root orientation="horizontal">
<NavigationMenu.List className="flex items-center justify-start gap-8">
<NavigationMenu.Root orientation="horizontal" className="h-full">
<NavigationMenu.List className="flex items-center justify-start h-full gap-8 my-auto">
<NavItem text={t("home")} to="/" />
<NavItem text={t("following")} to="/c" />

View File

@ -1,4 +1,4 @@
import { PlusIcon } from "@radix-ui/react-icons";
import { Pencil1Icon } from "@radix-ui/react-icons";
import { Button, Text } from "@radix-ui/themes";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
@ -8,8 +8,8 @@ export default function CreatePostButton() {
return (
<Link to={"/create"}>
<Button variant="ghost" className="h-full">
<PlusIcon />
<Button variant="ghost" className="items-center h-full px-[6px] pr-[8px] py-0 my-auto m-0 overflow-hidden">
<Pencil1Icon />
<Text>{t("createPost")}</Text>
</Button>
</Link>

View File

@ -1,11 +1,13 @@
import CreatePostButton from "./CreatePostButton/CreatePostButton";
import ThemeChangeButton from "./ThemeChangeButton/ThemeChangeButton";
import UserButton from "./UserButton/UserButton";
export default function RightButtonBar() {
return (
<div className='flex flex-row justify-end flex-1 gap-4'>
<div className='flex flex-row items-center justify-end flex-1 gap-2'>
<CreatePostButton />
<ThemeChangeButton />
<UserButton />
</div>
)

View File

@ -0,0 +1,31 @@
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
import { IconButton } from "@radix-ui/themes";
import { useAtom } from "jotai";
import { themeAtom } from "../../../../AtomStore/AtomStore";
export default function ThemeChangeButton() {
const [theme, setTheme] = useAtom(themeAtom);
const toggleTheme = () => {
if (theme === "light") {
setTheme("dark");
} else {
setTheme("light");
}
};
return (
<IconButton
size={"3"}
onClick={toggleTheme}
className="mx-0 my-auto rounded-full p-[8px]"
variant="ghost"
>
{theme === "light" ? (
<SunIcon className="size-6" />
) : (
<MoonIcon className="size-6" />
)}
</IconButton>
);
}

View File

@ -8,8 +8,8 @@ export default function LoginButton() {
const { t } = useTranslation();
return (
<Link to={"/login"}>
<Flex className="justify-between gap-2">
<Link to={"/login"} className="size-full">
<Flex className="items-center justify-start h-full gap-2">
<Icon>
<EnterIcon />
</Icon>

View File

@ -36,7 +36,7 @@ export default function LogoutButton() {
return (
<Flex
className="justify-between gap-2"
className="items-center justify-start gap-2 cursor-pointer size-full"
onClick={() => logoutMutation.mutate()}
>
<Icon>

View File

@ -1,12 +1,15 @@
import { LaptopIcon, PersonIcon } from "@radix-ui/react-icons";
import {
LaptopIcon,
PersonIcon
} from "@radix-ui/react-icons";
import { DropdownMenu, Flex, IconButton, Text } from "@radix-ui/themes";
DropdownMenu,
Flex,
IconButton,
Text,
Tooltip,
} from "@radix-ui/themes";
import { Icon } from "@radix-ui/themes/dist/esm/components/callout.js";
import { useAtomValue } from "jotai";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { userAtom } from "../../../../AtomStore/AtomStore";
import LoginButton from "./LoginButton/LoginButton";
import LogoutButton from "./LogoutButton/LogoutButton";
@ -15,47 +18,51 @@ export default function UserButton() {
const user = useAtomValue(userAtom);
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton className="cursor-pointer">
<PersonIcon />
</IconButton>
</DropdownMenu.Trigger>
<Tooltip content={"User menu"} className="w-fit">
<DropdownMenu.Trigger>
<IconButton
size={"3"}
className="items-center my-auto rounded-full"
variant="ghost"
>
<PersonIcon className="size-6" />
</IconButton>
</DropdownMenu.Trigger>
</Tooltip>
<DropdownMenu.Content className="w-fit">
<DropdownMenu.Item>
<Link to={"/user/:user-id/profile"}>
<Flex className="justify-between gap-2">
<DropdownMenu.Item onClick={() => navigate("/profile")}>
<div>
<Flex className="justify-between w-full gap-2">
<Icon>
<PersonIcon />
</Icon>
<Text>{t("profile")}</Text>
</Flex>
</Link>
</div>
</DropdownMenu.Item>
<DropdownMenu.Item>
<Link to={"/user/blogs"}>
<DropdownMenu.Item onClick={() => navigate("/user/blogs")}>
<div>
<Flex className="justify-between gap-2">
<Icon>
<LaptopIcon />
</Icon>
<Text>{t("yourBlogs")}</Text>
</Flex>
</Link>
</div>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<DropdownMenu.Item color={user ? "red" : "green"}>
{user ? (
<LogoutButton />
) : (
<LoginButton />
)}
{user ? <LogoutButton /> : <LoginButton />}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>

View File

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

View File

@ -0,0 +1,29 @@
import { Box, Button, Text } from "@radix-ui/themes";
import { PropsWithChildren, ReactNode } from "react";
import { useLocation, useNavigate } from "react-router-dom";
type TButtonLink = {
label: string;
path: string;
icon?: ReactNode;
} & PropsWithChildren;
export default function ButtonLink(props: TButtonLink) {
const { pathname } = useLocation();
const navigate = useNavigate();
const onClick = () => {
navigate(props.path);
};
return (
<Button
className="relative"
variant={pathname === props.path ? "solid" : "outline"}
onClick={onClick}
>
<Box className="absolute left-4">{props?.children}</Box>
<Text>{props.label}</Text>
</Button>
);
}

View File

@ -0,0 +1,67 @@
import {
BoxIcon,
FileTextIcon,
LockClosedIcon,
PersonIcon,
} from "@radix-ui/react-icons";
import {
Box,
Button,
Flex,
Separator,
TabNav,
Text
} from "@radix-ui/themes";
import UserCard from "../UserCard/UserCard";
import ButtonLink from "./ButtonLink/ButtonLink";
import TabLink from "./TabLink/TabLink";
export default function ProfileNavbar() {
return (
<>
<Flex className="relative flex flex-col sm:hidden">
<TabNav.Root size={"2"}>
<TabLink label="About" path="/profile" />
<TabLink label="Security" path="/profile/sec" />
<TabLink label="Posts" path="/profile/posts" />
</TabNav.Root>
</Flex>
<Flex
direction={"column"}
gap={"4"}
className="absolute min-w-56 shrink collapse sm:relative sm:visible"
>
<UserCard />
<Box className="mx-2">
<Separator orientation={"horizontal"} size={"4"} />
</Box>
<Flex gap={"2"} direction={"column"}>
<ButtonLink label="About" path="/profile">
<PersonIcon />
</ButtonLink>
<ButtonLink label="Security" path="/profile/sec">
<LockClosedIcon />
</ButtonLink>
<ButtonLink label="Posts" path="/profile/posts">
<FileTextIcon />
</ButtonLink>
<Button variant="outline" className="relative">
<BoxIcon className="absolute left-4" />
<Text className="max-w-[60%]" truncate>
Work in progress...
</Text>
</Button>
</Flex>
</Flex>
</>
);
}

View File

@ -0,0 +1,22 @@
import { Flex, TabNav, Text } from "@radix-ui/themes";
import { useLocation, useNavigate } from "react-router-dom";
type TTabLink = {
label: string;
path: string;
};
export default function TabLink(props: TTabLink) {
const { pathname } = useLocation();
const navigate = useNavigate();
const onClick = () => navigate(props.path);
return (
<TabNav.Link active={pathname === props.path} onClick={onClick}>
<Flex className="items-center gap-1">
<Text>{props.label}</Text>
</Flex>
</TabNav.Link>
);
}

View File

@ -1,28 +0,0 @@
import * as RadixTooltip from "@radix-ui/react-tooltip";
import { Card, Text, Theme } from "@radix-ui/themes";
type TTooltipProps = {
text: string;
} & React.PropsWithChildren;
export default function Tooltip(props: TTooltipProps) {
return (
<RadixTooltip.Provider>
<RadixTooltip.Root>
<RadixTooltip.Trigger>{props.children}</RadixTooltip.Trigger>
<RadixTooltip.Portal>
<RadixTooltip.Content side="top">
<RadixTooltip.Content>
<Theme panelBackground="translucent">
<Card className="p-1 -translate-y-1 w-fit h-fit animate-appearTooltip">
<Text>{props.text}</Text>
</Card>
</Theme>
</RadixTooltip.Content>
</RadixTooltip.Content>
</RadixTooltip.Portal>
</RadixTooltip.Root>
</RadixTooltip.Provider>
);
}

View File

@ -0,0 +1,92 @@
import {
Avatar,
Badge,
Card,
Flex,
Separator,
Skeleton,
Text,
} from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import { useAtomValue } from "jotai";
import { TGetUserInfoResponse } from "../../@types/UserType";
import { userAtom } from "../../AtomStore/AtomStore";
import { axiosLocalhost } from "../../api/axios/axios";
import { JSONWithInt64 } from "../../utils/idnex";
type TUserCard = {
username?: string;
userId?: string;
};
export default function UserCard(props: TUserCard) {
const user = useAtomValue(userAtom);
const { data, isLoading } = useQuery({
queryKey: [`userCard${props.userId || user?.id}`],
queryFn: async () => {
const response = await axiosLocalhost.get(
`/users/info/${props.userId || user?.id?.toString()}`,
{
transformResponse: [(data) => data],
}
);
const parsedResponse = JSONWithInt64(response.data);
console.log("parsedResponse", parsedResponse);
return parsedResponse as TGetUserInfoResponse;
},
});
const getInitials = (username: string): string => {
const result = username
.split(" ")
.map((word) => word[0].toUpperCase())
.join("");
return result;
};
const getUsername = (): string => {
if (!user || props.username) return props.username || "";
return user.username;
};
return (
<Card>
<Flex gap={"2"} direction={"column"}>
<Flex direction={"row"} gap={"2"} align={"center"}>
<Avatar
fallback={<div>{getInitials(getUsername())}</div>}
radius="full"
loading="lazy"
/>
<Flex className="flex-col overflow-hidden">
{isLoading ? (
<Skeleton>
<Text truncate>{`Temporal`}</Text>
</Skeleton>
) : (
<Text truncate>
{data?.user_info.display_name ||
`Non specified`}
</Text>
)}
<Text size={"1"} color={"gray"}>
{`@${data?.user_info.username}`}
</Text>
</Flex>
</Flex>
<Separator size={"4"} />
<Flex gap={"2"}>
<Badge>test</Badge>
<Badge color="amber">user</Badge>
<Badge color="red">admin</Badge>
</Flex>
</Flex>
</Card>
);
}

View File

@ -1,33 +1,60 @@
import { Skeleton, Text } from "@radix-ui/themes";
import { HoverCard, Link, Skeleton, Text } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { lazy, Suspense } from "react";
import { axiosLocalhost } from "../../api/axios/axios";
type TUserNicknameLink = {
userId: string;
userId: string;
};
const UserCard = lazy(() => import("../UserCard/UserCard"));
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;
},
});
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>
);
if (isPending)
return (
<Skeleton>
<Text
size={{
sm: "4",
md: "5",
lg: "6",
}}
>
@Nickname
</Text>
</Skeleton>
);
return (
<Link to={`/users/${data}`}>
<Text>@{data}</Text>
</Link>
);
return (
<HoverCard.Root>
<HoverCard.Trigger>
<Link href={`/users/${data}`}>
<Text
size={{
sm: "3",
md: "4",
lg: "5",
}}
>
@{data}
</Text>
</Link>
</HoverCard.Trigger>
<HoverCard.Content className="p-0" maxWidth={'220px'}>
<Suspense fallback={<Skeleton />}>
<UserCard userId={props.userId} />
</Suspense>
</HoverCard.Content>
</HoverCard.Root>
);
}

View File

@ -9,6 +9,7 @@ import { Link, useNavigate } from "react-router-dom";
import { axiosLocalhost } from "../../../api/axios/axios";
import { userAtom } from "../../../AtomStore/AtomStore";
import UseCapsLock from "../../../hooks/useCapsLock";
import { JSONWithInt64 } from "../../../utils/idnex";
import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton";
type TLoginData = {
@ -28,12 +29,18 @@ export default function LoginPage() {
mutationFn: async (data: TLoginData) => {
let response = await axiosLocalhost.post(
"/login",
JSON.stringify(data)
JSON.stringify(data),
{
transformResponse: [data => data]
}
);
const parsedData = JSONWithInt64(response.data);
setUserAtom({
username: response.data.username,
username: parsedData.username,
isAdmin: false,
id: response.data.id,
id: parsedData.id,
});
},
@ -63,7 +70,7 @@ export default function LoginPage() {
return (
<Card
size={"2"}
className="absolute w-1/4
className="absolute w-[25rem] min-w-[20rem]
left-[50%] top-[50%]
translate-x-[-50%] translate-y-[-50%]"
>

View File

@ -9,6 +9,7 @@ import { Link, useNavigate } from "react-router-dom";
import { axiosLocalhost } from "../../../api/axios/axios";
import { userAtom } from "../../../AtomStore/AtomStore";
import UseCapsLock from "../../../hooks/useCapsLock";
import { JSONWithInt64 } from "../../../utils/idnex";
import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton";
type TRegisterData = {
@ -31,11 +32,16 @@ export default function RegisterPage() {
const registerMutation = useMutation({
mutationFn: async (data: TRegisterData) => {
let response = await axiosLocalhost.post("/users", JSON.stringify(data));
let response = await axiosLocalhost.post("/users", JSON.stringify(data), {
transformResponse: [data => data]
});
const parsedResponse = JSONWithInt64(response.data)
setUserAtom({
username: response.data.username,
username: parsedResponse.username,
isAdmin: false,
id: response.data.id,
id: parsedResponse.id,
})
},

View File

@ -2,9 +2,9 @@ import { Box, Container, Flex } from "@radix-ui/themes";
import { useAtom, useSetAtom } from "jotai";
import {
postCreationAtom,
postCreationTitleAtom
postCreationTitleAtom,
} from "../../AtomStore/AtomStore";
import Editor from "../../Components/Editor/Editor";
import TrueEditor from "../../Components/Editor/TrueEditor";
import SubmitPostButton from "./SubmitPostButton/SubmitPostButton";
export default function PostCreatorPage() {
@ -13,10 +13,13 @@ export default function PostCreatorPage() {
return (
<>
<Box className="flex flex-col flex-1">
<Flex gap={"4"} direction={"column"} className="flex-[1]">
<Container className="flex-[1]">
<Box className="flex flex-col flex-1 overflow-hidden">
<Flex
gap={"4"}
direction={"column"}
className="justify-start overflow-hidden"
>
<Container>
<input
placeholder={"Post title"}
className="mb-2 border-0 border-b-[1px]
@ -29,9 +32,7 @@ export default function PostCreatorPage() {
/>
</Container>
<Container className="overflow-y-auto flex-grow-[100]">
<Editor onChange={setContentValue} />
</Container>
<TrueEditor onChange={setContentValue} />
<Box className="flex justify-center flex-[1] mb-4">
<SubmitPostButton className="text-2xl rounded-full w-52" />

View File

@ -2,8 +2,8 @@ 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 { axiosLocalhost } from "../../api/axios/axios";
import TrueEditor from "../../Components/Editor/TrueEditor";
import SubmitChangesButton from "./SubmitChangesButton/SubmitChangesButton";
export default function PostRedactor() {
@ -12,7 +12,7 @@ export default function PostRedactor() {
const queryParams = useParams();
const { isPending } = useQuery({
const { isLoading } = useQuery({
queryKey: ["changePostKey", queryParams.postId],
queryFn: async () => {
try {
@ -31,16 +31,20 @@ export default function PostRedactor() {
}
},
gcTime: 0,
refetchOnMount: true
refetchOnMount: true,
});
return (
<>
<Box className="flex flex-col flex-1">
<Flex gap={"4"} direction={"column"} className="flex-[1]">
<Container className="flex-[1]">
<Box className="flex flex-col flex-1 overflow-hidden">
<Flex
gap={"4"}
direction={"column"}
className="overflow-hidden"
>
<Container className="">
<input
disabled={isPending}
disabled={isLoading}
placeholder={"Post title"}
className="mb-2 border-0 border-b-[1px]
outline-none w-full border-b-gray-400
@ -52,22 +56,32 @@ export default function PostRedactor() {
/>
</Container>
<Container className="overflow-y-auto flex-grow-[100]">
{isPending ? (
<Spinner />
) : (
<Editor
defaultValue={contentValue}
onChange={setContentValue}
/>
)}
</Container>
{/* <Container className="overflow-hidden flex-grow-[100]">
{isPending ? (
<Spinner />
) : (
<Editor
defaultValue={contentValue}
onChange={setContentValue}
/>
)}
</Container> */}
<Box className="flex justify-center flex-[1] mb-4">
{isLoading ? (
<Spinner />
) : (
<TrueEditor
defaultValue={contentValue}
onChange={setContentValue}
/>
)}
<Box className="flex justify-center flex-[1] mb-4 ">
<SubmitChangesButton
contentValue={contentValue}
titleValue={titleValue}
className="text-2xl rounded-full w-52" />
contentValue={contentValue}
titleValue={titleValue}
className="text-2xl rounded-full w-52"
/>
</Box>
</Flex>
</Box>

View File

@ -3,8 +3,8 @@ import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom";
import { axiosLocalhost } from "../../../../api/axios/axios";
import useToast from "../../../../hooks/useToast";
import { axiosLocalhost } from "../../../api/axios/axios";
import useToast from "../../../hooks/useToast";
type TSubmitChangesButton = {
className: string;

View File

@ -0,0 +1,50 @@
import { Inset } from "@radix-ui/themes";
import { useMemo } from "react";
type TInsetImageProps = {
isHovered: boolean;
ref_: React.RefObject<HTMLDivElement>;
windowWidth: number;
};
export default function InsetImage(props: TInsetImageProps) {
const seed = useMemo(() => {
return Math.floor(Math.random() * (1 + Math.random()) * 100000);
}, []);
return (
<Inset
side={"left"}
clip={"padding-box"}
className={`max-w-[${
props.isHovered ? "100%" : "225px"
}] transition-[flex] duration-[250ms]
${props.isHovered ? "flex-1" : "flex-[0.5]"}
relative overflow-hidden h-72`}
>
<img
style={{
minWidth: `${
Math.min(
props.windowWidth,
props.ref_.current?.clientWidth || 0
) / 2
}px`,
transform: `${
props.isHovered
? "translateX(0)"
: `translateX(calc(-50% + ${
Math.min(
props.windowWidth,
props.ref_.current?.clientWidth || 0
) / 6
}px))`
}`,
}}
className={`h-72 transition-all duration-[250ms]`}
src={`https://picsum.photos/seed/${seed}/1000/600?grayscale`}
alt="Bold typography"
/>
</Inset>
);
}

View File

@ -1,30 +1,121 @@
import { ImageIcon } from "@radix-ui/react-icons";
import { Box, Card, Heading } from "@radix-ui/themes";
import { CalendarIcon } from "@radix-ui/react-icons";
import { Box, Card, Flex, Heading, Text, Tooltip } from "@radix-ui/themes";
import dayjs from "dayjs";
import "dayjs/locale/ru";
import { Interweave } from "interweave";
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import { GetRandomPostsRow } from "../../../@types/PostTypes";
import { Post } from "../../../@types/PostTypes";
import UserNicknameLink from "../../../Components/UserNicknameLink/UserNicknameLink";
import InsetImage from "./InsetImage/InsetImage";
type TPostCard = {
post: GetRandomPostsRow;
};
export default function PostCard({ post }: TPostCard) {
const navigate = useNavigate()
export default function PostCard(props: Post) {
const navigate = useNavigate();
const ref = useRef<HTMLDivElement>(null);
const clickHandler = () => {
navigate(`/posts/${post.post_id.toString()}`)
}
const [windowWidth, setWindowWidth] = useState<number>(window.innerWidth);
const [isHovered, setIsHovered] = useState(false);
return (
<Card className="h-32 mb-4" onClick={clickHandler}>
<Box className="flex size-full">
<Box>
<ImageIcon className="w-full h-full" />
</Box>
const parsedDate = dayjs(props.created_at)
.locale("ru")
.format("DD MMMM YYYY");
<Box className="px-4 pt-2">
<Heading>{post.title}</Heading>
</Box>
</Box>
</Card>
);
const clickHandler = () => {
navigate(`/posts/${props.post_id.toString()}`);
};
useEffect(() => {
const f = () => {
setWindowWidth(window.innerWidth);
console.log(`Window width: ${window.innerWidth} px`);
};
f();
window.addEventListener("resize", f);
return () => window.removeEventListener("resize", f);
}, []);
return (
<Card
ref={ref}
className="flex w-full cursor-pointer max-h-72"
onClick={clickHandler}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<InsetImage
isHovered={isHovered}
ref_={ref}
windowWidth={windowWidth}
/>
<Flex
direction={"column"}
className="justify-between flex-1 w-full gap-4 px-4 pt-2"
>
<Heading
size={{
sm: "4",
md: "5",
lg: "6",
}}
className="flex items-center h-fit"
>
{props.title}
</Heading>
<Flex
direction={"column"}
justify={"between"}
className="h-full overflow-hidden"
>
<Box className="overflow-y-hidden">
<Text
size={{
sm: "4",
md: "5",
lg: "6",
}}
>
<Interweave content={props.content} />
</Text>
</Box>
<Flex className="gap-2 lg:gap-4">
<Tooltip content={`Written at`}>
<Flex className="items-center gap-2 h-fit">
<CalendarIcon className="size-6" />
<Text
size={{
sm: "3",
md: "4",
lg: "5",
}}
weight={"medium"}
className="flex items-center gap-1"
>
{`${parsedDate}`}
</Text>
</Flex>
</Tooltip>
<Flex className="items-center gap-2 h-fit">
<Text
size={{
sm: "3",
md: "4",
lg: "5",
}}
>
Author:{" "}
</Text>
<UserNicknameLink userId={props.user_id} />
</Flex>
</Flex>
</Flex>
</Flex>
</Card>
);
}

View File

@ -1,67 +1,80 @@
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Container, Flex, Heading, Separator } from "@radix-ui/themes";
import { useQuery } from "@tanstack/react-query";
import { Box, Flex, ScrollArea, Text } from "@radix-ui/themes";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { GetRandomPostsRow } from "../../@types/PostTypes";
import { useInView } from "react-intersection-observer";
import { SelectedPostsResponse } from "../../@types/PostTypes";
import { axiosLocalhost } from "../../api/axios/axios";
import PostCard from "./PostCard/PostCard";
const LIMIT = 10;
const LIMIT = 7;
export default function RandomPostsPage() {
const {t} = useTranslation()
const { t } = useTranslation();
const { data, refetch } = useQuery({
queryKey: ["random_posts_key"],
queryFn: async () => {
try {
const response = await axiosLocalhost.get(
`/posts/random?limit=${LIMIT}`
);
const [ref, inView] = useInView();
return response.data as GetRandomPostsRow[];
} catch (error) {
console.log(`Something went wrong`);
}
const { data, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: [`random_post_inf`],
queryFn: async ({ pageParam }): Promise<SelectedPostsResponse> => {
const response = await axiosLocalhost.get(
`/posts/random?limit=${LIMIT}&offset=${pageParam}`
);
return [];
},
});
return response.data as SelectedPostsResponse;
},
initialPageParam: 0,
getPreviousPageParam: (lastPage) =>
lastPage.prev_page_index < 0 ? undefined : lastPage.prev_page_index,
getNextPageParam: (lastPage) =>
lastPage.next_page_index < 0 ? undefined : lastPage.next_page_index,
});
return (
<>
<Flex direction={"column"} className="mx-auto">
<Heading size={"9"} weight={"regular"} className="text-center">
{t("discover")}
</Heading>
useEffect(() => {
if (inView) {
if (hasNextPage) fetchNextPage();
}
}, [inView]);
<Separator size={"4"} className="my-8" />
return (
<>
<ScrollArea>
<Flex
direction={"column"}
className="w-full overflow-hidden sm:mx-auto max-w-pc-width "
>
<Flex direction={"column"} gap={"4"} className="mx-4 xl:mx-0">
{data?.pages.map((post, i) => {
return (
<Flex
direction={"column"}
gap={"4"}
key={`${i}`}
>
{post.selected_posts.map((post, j) => {
return <PostCard key={j} {...post} />;
})}
</Flex>
);
})}
<ScrollArea.Root className="w-full h-full overflow-hidden">
<ScrollArea.Viewport className="overflow-scroll rounded size-full">
{data?.map((post, i) => {
return (
<Container size={"3"} key={`post${i}`}>
<PostCard post={post} />
</Container>
);
})}
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="z-50 flex touch-none select-none p-0.5 w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="relative flex-1 rounded-[10px] bg-slate-200"/>
</ScrollArea.Scrollbar>
{/* <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>
</>
);
<Box ref={ref} className="w-full mb-4 text-center">
{isFetching ? (
<Text>Loading more...</Text>
) : hasNextPage ? (
<Text
className="cursor-pointer"
onClick={() => fetchNextPage()}
>
Load more posts
</Text>
) : (
<Text>No more posts to load</Text>
)}
</Box>
</Flex>
</Flex>
</ScrollArea>
</>
);
}

View File

@ -7,10 +7,6 @@ import BlogCreationDialog from "../../Components/Dialogs/BlogCreationDialog/Blog
import { JSONWithInt64 } from "../../utils/idnex";
import SkeletonBoxes from "./SkeletonBoxes/SkeletonBoxes";
const TAGS = Array.from({ length: 50 }).map(
(_, i, a) => `v1.2.0-beta.${a.length - i}`
);
export default function UserBlogsPage() {
const { data, isPending, isFetching } = useQuery({
queryKey: ["userBlogs"],
@ -19,7 +15,7 @@ export default function UserBlogsPage() {
transformResponse: [(data) => data],
});
let temp = JSONWithInt64(response.data);
const temp = JSONWithInt64(response.data);
return temp as any[];
},
@ -52,16 +48,14 @@ export default function UserBlogsPage() {
<ScrollArea.Viewport className="size-full">
<Flex direction={"column"} gap={"2"}>
{data
? data?.map((blog: any, b) => {
? data?.map((blog: any, b: number) => {
return (
<>
<BlogBox
key={b}
title={blog.title}
blogId={blog.blog_id}
userId={blog.user_id}
/>
</>
);
})
: null}

View File

@ -0,0 +1,8 @@
export default function UserPostsPage() {
return (
<div>
<h1>User Posts Page</h1>
</div>
)
}

View File

@ -0,0 +1,83 @@
import {
Badge,
Box,
DataList,
Flex,
ScrollArea,
Separator,
Text,
TextArea,
} from "@radix-ui/themes";
export default function UserProfilePage() {
return (
<Flex direction={"column"} className="flex-grow-[8] overflow-hidden">
<ScrollArea
type="auto"
scrollbars="vertical"
className="flex-grow-[1] pt-4"
>
<Text size={"8"}>Base info</Text>
<Separator className="w-full my-4" />
<Box className="mb-8">
<DataList.Root size={"3"}>
<DataList.Item className="items-center">
<DataList.Label className="min-w-40">
Username
</DataList.Label>
<DataList.Value>
@Definitely_fake_user
</DataList.Value>
</DataList.Item>
<DataList.Item>
<DataList.Label className="min-w-40">
Email
</DataList.Label>
<DataList.Value>fake@email.com</DataList.Value>
</DataList.Item>
<DataList.Item>
<DataList.Label className="min-w-40">
Display name
</DataList.Label>
<DataList.Value>Isaev</DataList.Value>
</DataList.Item>
<DataList.Item>
<DataList.Label className="min-w-40">
Badges
</DataList.Label>
<DataList.Value>
<Flex
gap={"2"}
wrap={"wrap"}
className="content-evenly"
>
<Badge size={"3"}>User</Badge>
<Badge size={"3"} color="red">
Admin
</Badge>
<Badge size={"3"} color="green">
Writer
</Badge>
</Flex>
</DataList.Value>
</DataList.Item>
</DataList.Root>
</Box>
<Text size={"8"}>
Bio
</Text>
<Separator className="w-full my-4" />
<TextArea resize={"vertical"} placeholder="Add your bio here...">
</TextArea>
</ScrollArea>
</Flex>
);
}

View File

@ -0,0 +1,8 @@
export default function UserSecurityPage() {
return (
<div>
<h1>User Security Page</h1>
</div>
)
}

View File

@ -0,0 +1,21 @@
export const loremText = `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quam phasellus id at vivamus rutrum per justo dapibus elementum dictum litora natoque. Lorem vestibulum arcu platea volutpat eros aenean class odio nec potenti lorem duis. Nunc per tempor accumsan sollicitudin curae praesent egestas venenatis donec aliquam placerat vel. Aliquam fermentum ipsum massa cum facilisi parturient mus egestas dictumst integer platea nec. Ullamcorper nisl sit vivamus nostra litora scelerisque aptent ad cubilia in fusce vehicula. Sem cum cubilia sagittis sed arcu tortor lectus egestas ac sociis orci viverra. Non euismod egestas congue scelerisque at nisl ad morbi mattis felis mus primis. Ultrices tellus aliquet in at sit nam etiam quisque imperdiet taciti vulputate ipsum. Sit vivamus leo at accumsan habitasse dictum mi turpis nisl elit convallis per. Aenean venenatis gravida sociosqu tempor porta senectus aptent turpis purus et bibendum senectus. A neque arcu torquent eleifend urna taciti in dui nec et curae conubia. Condimentum arcu vivamus porttitor per mollis non luctus sociis sociis mollis a dui. Imperdiet aenean euismod aliquet mauris ridiculus consequat varius vulputate tempor lacus mollis odio.
Imperdiet varius imperdiet neque habitasse eros taciti elit pretium sollicitudin habitasse tortor interdum. Non a dignissim ac cras velit nisi rutrum eu gravida erat class aliquam. Hac hac himenaeos sagittis elit et natoque interdum vehicula lacinia imperdiet adipiscing tortor. Gravida auctor litora ipsum rhoncus felis tellus ridiculus suscipit dignissim praesent blandit eu. Dolor bibendum massa sapien consectetur sodales odio justo blandit sapien posuere magna rhoncus. Nascetur maecenas sociosqu placerat proin varius iaculis viverra laoreet lectus hac habitant ligula. Aenean consequat vitae tempor ligula pharetra condimentum mollis praesent at ipsum quam nam. Dapibus purus orci malesuada penatibus mattis sociosqu consectetur natoque ultricies sagittis pretium nullam. Arcu erat aliquet adipiscing sociis sem ultricies ante mus interdum eu sit arcu. Interdum fusce elementum pharetra parturient tortor eros fermentum dignissim parturient maecenas facilisi mi. Nunc orci conubia lacinia imperdiet dictumst facilisi consectetur nam cum dictumst mollis ornare. Tortor volutpat blandit nunc consequat penatibus eleifend tempus sed quis mauris luctus mattis. Condimentum velit proin taciti vestibulum ante cum ante lorem porta dictumst ornare vehicula. Habitasse fermentum gravida consequat odio volutpat imperdiet nisi velit posuere euismod curae interdum.
Sem ullamcorper est senectus dapibus ornare congue taciti euismod aenean hac et ullamcorper. Potenti mattis mi ridiculus quis parturient mus id praesent taciti parturient phasellus aenean. Montes parturient cubilia congue montes aenean maecenas neque maecenas iaculis condimentum a metus. Mattis proin nec dictum dictumst consequat lacus phasellus odio porta ac interdum vivamus. Nec euismod ullamcorper erat convallis neque molestie curae metus sociosqu curae congue natoque. Netus porttitor tempus commodo netus interdum faucibus ad nullam malesuada magna tortor auctor. Aptent mus massa per aliquet neque mattis dignissim elit etiam vestibulum justo curae. Urna in vestibulum et vivamus porta duis fringilla curae rhoncus fermentum habitant malesuada. Fermentum odio neque sociis etiam habitant quisque habitasse litora nascetur neque platea porttitor. Varius elit urna venenatis nisl quis consectetur senectus condimentum curae risus a taciti. Ultricies quisque sapien nam consectetur at habitasse faucibus tincidunt eu mollis fusce fringilla. Potenti massa mus iaculis phasellus ac ad consectetur tempor fusce blandit ipsum sociis. Malesuada nam nostra vivamus bibendum bibendum parturient nisi sociosqu tincidunt justo dui dignissim. Interdum habitasse curae etiam parturient convallis faucibus potenti cubilia fusce eu amet quisque.
Ad ullamcorper sollicitudin phasellus pretium a eleifend vel taciti ipsum ultricies bibendum purus. Ipsum posuere vivamus lacinia felis nulla lacus maecenas aliquam curae nec nostra nostra. Lorem mi nullam dolor sapien quisque nulla eros conubia accumsan netus porttitor vestibulum. Sociosqu quam taciti potenti accumsan nisi cum ridiculus ridiculus accumsan dictum mattis sociis. A sit eu primis eros ultrices ac eleifend volutpat condimentum sociosqu est lacus. Habitant montes venenatis lacinia viverra nascetur vel commodo eleifend pretium vestibulum molestie consequat. Et netus purus risus rhoncus parturient a blandit blandit ante justo vitae nullam. Natoque at torquent eget facilisi taciti scelerisque ridiculus mus vestibulum et enim senectus. Est et aliquet eget vulputate lacinia leo curabitur arcu ornare diam litora taciti. Cum ultrices rhoncus ante nullam odio eleifend neque lobortis rutrum fringilla sed suscipit. Pretium sit vestibulum elementum odio suscipit mollis pellentesque etiam hac sapien lorem himenaeos. Ridiculus ullamcorper varius rutrum porta himenaeos praesent odio habitasse hac netus curae eros. Velit massa sapien platea pulvinar malesuada fermentum varius sapien vehicula mi vel vivamus. Duis turpis ultricies suscipit habitasse curae quisque mus morbi nunc rhoncus malesuada viverra.
Fusce montes luctus auctor ligula luctus nisi proin per ac nunc imperdiet eleifend. Sem blandit magnis scelerisque etiam sapien cras eleifend vitae justo suspendisse lobortis eu. Eleifend lacus dapibus mi nunc iaculis purus eleifend litora lorem dolor porta luctus. Commodo per nisl senectus dictum cum nostra dolor aenean quam egestas amet venenatis. Feugiat praesent pulvinar leo eleifend turpis netus donec litora adipiscing nascetur aenean ante. Quis mi velit risus venenatis nulla vitae consequat est sapien lorem amet congue. Pharetra venenatis neque nunc quisque a eleifend cum nisi ante netus nostra scelerisque. Etiam felis ullamcorper sodales montes id euismod eget vivamus elit fermentum neque lectus. Massa montes proin pellentesque praesent class felis conubia vivamus bibendum condimentum gravida mattis. Dictumst tempor netus sollicitudin quis aptent vivamus fringilla amet taciti convallis sollicitudin conubia. Dui morbi praesent magnis ligula vivamus proin magnis eget arcu metus lorem egestas. Laoreet dapibus fringilla inceptos pellentesque euismod netus ante urna risus elementum integer nunc. Vivamus inceptos penatibus risus mus consectetur quis lacinia luctus vivamus elementum pharetra consectetur. Ornare donec imperdiet est mi arcu at consequat dignissim eleifend aliquet taciti molestie.
Sodales nisl magna blandit curae bibendum sociosqu justo phasellus penatibus gravida eu velit. Ornare potenti venenatis tempor integer donec quis sociosqu pulvinar dictum urna interdum euismod. Sodales magna non integer risus purus metus fermentum nascetur interdum ridiculus curabitur blandit. Curabitur senectus id lorem a habitant sed semper accumsan sed euismod sodales volutpat. Bibendum sem quisque lobortis magna nisi magna elit facilisi sed quisque est duis. Imperdiet risus nullam magna potenti commodo eros ridiculus conubia aenean convallis nunc habitant. Rhoncus conubia mus curae maecenas suspendisse suspendisse nisi sed commodo litora tempor bibendum. Dictumst rutrum aliquet consectetur ad laoreet gravida metus montes feugiat quisque nulla elit. Potenti dui erat eleifend himenaeos primis turpis sollicitudin id pretium tortor elementum nec. Pretium nunc quisque morbi senectus himenaeos mollis varius rhoncus venenatis metus magna ipsum. Justo praesent amet dignissim adipiscing primis quam eros duis viverra cum eros rhoncus. Primis pretium erat potenti nisi felis elementum torquent at habitant cubilia nascetur dui. Tortor hac non a adipiscing duis laoreet a maecenas dolor aliquet pretium himenaeos. Netus adipiscing hac class placerat adipiscing accumsan condimentum eros tincidunt potenti conubia integer.
Magna placerat sem mi nunc pellentesque aliquet habitant aptent posuere faucibus gravida at. Aliquet vestibulum molestie suscipit enim porta sed in dui quam mi lacus suspendisse. Vulputate curae suscipit pretium mauris morbi risus fringilla metus consequat ultricies magnis fringilla. Maecenas lacinia venenatis cras non placerat vitae diam conubia cras enim feugiat sodales. Varius mi parturient fusce tellus nullam consectetur arcu dapibus dictum praesent platea primis. Id natoque sapien non natoque aliquet dapibus pharetra sollicitudin consequat egestas suscipit vestibulum. Congue venenatis ligula iaculis senectus facilisi fermentum ipsum eros mattis gravida venenatis montes. Aliquet placerat interdum senectus magnis montes ullamcorper nisi morbi egestas ultricies vitae lorem. Euismod primis imperdiet elementum sociis tincidunt commodo donec nisi dictumst justo purus convallis. Sapien fusce dolor curae pulvinar velit vestibulum ante vel quis erat duis suscipit. Class dapibus tellus himenaeos magna diam vitae ac cras sociis ultricies dui tellus. Ultricies neque urna orci mus sit parturient netus massa montes dignissim posuere erat. Platea laoreet lectus sagittis fusce duis dui nunc volutpat laoreet primis sapien auctor. Odio ligula himenaeos aliquet nisl class phasellus viverra sapien facilisi cras potenti eleifend.
Dolor gravida hac dictum consequat phasellus et vulputate non nisl mi sociosqu montes. Risus felis blandit nostra consectetur integer pellentesque quisque varius egestas gravida lobortis natoque. Lacus quisque nullam metus massa hac amet primis phasellus vel odio class ullamcorper. Bibendum orci orci interdum luctus lacinia facilisi senectus bibendum lacus urna tellus mattis. Cras curae erat amet metus consequat mollis cras egestas ligula eget rhoncus ornare. Nulla taciti semper feugiat venenatis tempor sociosqu etiam rhoncus fermentum aliquet torquent ac. Tempus morbi litora dictumst tincidunt ad sem mus leo nostra metus natoque magna. Nullam facilisi lobortis porttitor ornare ad porta morbi donec rhoncus orci cras erat. Lacus eleifend vivamus imperdiet condimentum gravida suscipit ipsum facilisi pretium ridiculus ipsum viverra. Consequat gravida a cras eleifend ultricies leo habitant posuere maecenas magna aliquet cubilia. Semper dolor sit sagittis leo taciti penatibus dictum lacus nunc malesuada taciti eget. Feugiat ad enim posuere dolor risus leo placerat ridiculus condimentum purus porta sapien. Neque dolor fusce morbi class cum erat sociis curae vulputate porttitor viverra sit. Parturient facilisi semper urna ipsum conubia per odio ante vestibulum rhoncus potenti per.
Quisque suspendisse in lorem felis facilisi elit risus lorem class sapien quam fermentum. Taciti aliquam nunc tempus platea eget litora fusce habitant ullamcorper massa lorem eu. Lacinia hendrerit nulla nisl dignissim nostra massa viverra phasellus magna convallis malesuada habitant. Est nullam eget enim sit montes scelerisque feugiat suscipit id eget sollicitudin tempus. Condimentum mollis faucibus dapibus tincidunt euismod eu interdum vitae mauris dictumst nisi mollis. Inceptos tortor donec tortor convallis fermentum interdum nunc praesent ultricies laoreet et phasellus. Senectus ultricies mi gravida etiam leo nullam viverra pharetra nec praesent pretium eget. Risus eros tempor tempor suspendisse sodales at urna penatibus lorem luctus montes montes. Turpis dapibus eros posuere placerat penatibus sollicitudin congue aptent fusce pretium gravida volutpat. Pellentesque pharetra tempor pretium venenatis suscipit ipsum feugiat dictum morbi sit vivamus sodales. Non quam fusce at suspendisse ultricies tincidunt quis vel etiam sollicitudin cum interdum. Egestas eleifend nulla tellus ullamcorper condimentum sodales commodo tempor lobortis mauris volutpat iaculis. Malesuada velit dignissim fermentum sollicitudin penatibus tincidunt parturient suscipit nisi non etiam molestie. Lectus habitasse odio mollis hac tempor nisi mauris nascetur euismod dignissim natoque urna.
Consequat rhoncus odio metus lacinia dictumst varius viverra justo leo euismod cras ligula. Mi gravida commodo habitasse purus fermentum etiam luctus natoque sed cras eleifend imperdiet. Sagittis nec orci habitant montes tempus eleifend sociis quis phasellus platea accumsan semper. Hac mattis magnis fringilla nostra montes faucibus bibendum eleifend a netus lobortis urna. Magna laoreet mi luctus condimentum aliquet purus suspendisse donec feugiat penatibus elit nascetur. Dictumst habitasse adipiscing sit nostra eleifend viverra tempus fermentum ornare montes felis netus. Lacinia metus varius vestibulum lectus rutrum pulvinar dictumst velit ullamcorper lectus ante est. Dolor tortor ac massa vel metus ornare habitant volutpat vitae etiam cras in. Duis lorem quisque aliquam dapibus eu quis cras sagittis pulvinar nullam ad luctus. Porta ligula velit erat phasellus elit magnis pharetra est ac enim praesent molestie. Ad inceptos viverra sem adipiscing proin ad non venenatis platea pellentesque inceptos magnis. Pretium bibendum dictumst volutpat cras fringilla dapibus cum lorem tempor bibendum habitasse nulla. Condimentum commodo magna varius ornare torquent adipiscing molestie malesuada non ad parturient turpis. Phasellus magna mauris pellentesque montes scelerisque vehicula hac pharetra iaculis integer leo tempus.
`;

View File

@ -1,4 +1,5 @@
@import url('https://fonts.googleapis.com/css2?family=Edu+AU+VIC+WA+NT+Pre:wght@400..700&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Pochaevsk&display=swap');
@tailwind base;
@tailwind components;
@ -10,10 +11,13 @@
}
}
.radix-themes {
--default-font-family: "Times New Roman"; ;
:root {
--max-content-width: 70rem;
}
--heading-font-family: "Edu AU VIC WA NT Pre", cursive;
.radix-themes {
--default-font-family:
--heading-font-family: "Edu AU VIC WA NT Pre", cursive;
/* Your custom font for <Heading> components */
--code-font-family:
/* Your custom font for <Code> components */

View File

@ -5,6 +5,7 @@ import { Outlet } from "react-router-dom";
import { axiosLocalhost } from "../../api/axios/axios";
import { userAtom } from "../../AtomStore/AtomStore";
import NavBar from "../../Components/NavBar/NavBar";
import { JSONWithInt64 } from "../../utils/idnex";
const REFETCH_INTERVAL_IN_MINUTES = 5;
const RETRY_INTERVAL_IN_SECONDS = 1;
@ -12,10 +13,6 @@ 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);
@ -23,12 +20,16 @@ export default function MainPage() {
queryKey: ["authKey"],
queryFn: async () => {
try {
const response = await axiosLocalhost.get("/auth/check");
const response = await axiosLocalhost.get("/auth/check", {
transformResponse: [data => data]
});
const parsedResponse = JSONWithInt64(response.data)
setUserData({
isAdmin: response.data["is_admin"],
username: response.data["username"],
id: response.data["id"],
isAdmin: parsedResponse["is_admin"],
username: parsedResponse["username"],
id: parsedResponse["id"],
});
return true;
} catch (error) {
@ -62,10 +63,11 @@ export default function MainPage() {
direction={"column"}
className="min-h-[100vh] max-h-[100vh] overflow-hidden"
>
<Box flexGrow={"1"} className="flex-[1]">
<NavBar />
</Box>
<Box flexGrow={"100"} className="flex overflow-hidden flex-">
<NavBar />
<Box
flexGrow={"1"}
className="flex flex-col overflow-hidden"
>
<Outlet />
</Box>
</Flex>

View File

@ -0,0 +1,26 @@
import { Box, Flex, Separator } from "@radix-ui/themes";
import { Outlet } from "react-router-dom";
import ProfileNavbar from "../../Components/ProfileNavbar/ProfileNavbar";
export default function ProfilePage() {
return (
<Flex
className={`
relative flex-col flex-1 gap-0 mx-4
sm:flex-row sm:gap-4 sm:mx-4
md:w-full md:max-w-pc-width md:mx-auto
overflow-hidden
`}
>
<Box className="">
<ProfileNavbar />
</Box>
<Box className="my-2 collapse sm:visible">
<Separator orientation="vertical" size={"4"} />
</Box>
<Outlet />
</Flex>
);
}

View File

@ -7,14 +7,18 @@ import {
} from "react-router-dom";
import ArticleViewer from "../Components/ArticleViewer/ArticleViewer";
import MainPage from "../layout/MainPage/MainPage";
import ProfilePage from "../layout/ProfilePage/ProfilePage";
import AuthPageWrapper from "../Pages/AuthPageWrapper/AuthPageWrapper";
import BlogPage from "../Pages/BlogPage/BlogPage";
import LoginPage from "../Pages/LoginRegisterPage/LoginPage/LoginPage";
import PostRedactor from "../Pages/LoginRegisterPage/PostRedactor/PostRedactor";
import RegisterPage from "../Pages/LoginRegisterPage/RegisterPage/RegisterPage";
import PostCreatorPage from "../Pages/PostCreatorPage/PostCreatorPage";
import PostRedactor from "../Pages/PostRedactor/PostRedactor";
import RandomPostsPage from "../Pages/RandomPostsPage/RandomPostsPage";
import UserBlogsPage from "../Pages/UserBlogsPage/UserBlogsPage";
import UserPostsPage from "../Pages/UserPostsPage/UserPostsPage";
import UserProfilePage from "../Pages/UserProfilePage/UserProfilePage";
import UserSecurityPage from "../Pages/UserSecurityPage/UserSecurityPage";
function ErrorBoundary() {
let error = useRouteError();
@ -46,6 +50,21 @@ export const routes = createRoutesFromElements(
}
/>
<Route
path="profile"
element={
<AuthPageWrapper>
<ProfilePage />
</AuthPageWrapper>
}
>
<Route index element={<UserProfilePage />} />
<Route path="posts" element={<UserPostsPage />} />
<Route path='sec' element={<UserSecurityPage />} />
</Route>
<Route path="blogs/:blogId" element={<BlogPage />} />
<Route path="user" element={<Outlet />}>

View File

@ -7,6 +7,9 @@ export default {
"primary-color": "var(--primary-color)",
"secondary-color": "var(--secondary-color)",
},
maxWidth: {
"pc-width": "var(--max-content-width)",
},
fontFamily: {
'times': "Times New Roman"
},

View File

@ -0,0 +1,15 @@
#!/bin/bash
NAME=$1
NEXT_INDEX=$(ls migrations/*.up.sql | wc -l)
NEXT_INDEX=$((NEXT_INDEX + 1))
UP_FILE="migrations/${NEXT_INDEX}_$NAME.up.sql"
DOWN_FILE="migrations/${NEXT_INDEX}_$NAME.down.sql"
touch "$UP_FILE" "$DOWN_FILE"
echo "Created migration files:"
echo " $UP_FILE"
echo " $DOWN_FILE"

View File

@ -0,0 +1,243 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// source: badge_queries.sql
package db_repo
import (
"context"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
const addBadgeToUser = `-- name: AddBadgeToUser :one
INSERT INTO public.users_badges (user_id, badge_id)
VALUES
(
$1,
$2
)
RETURNING user_id, badge_id
`
type AddBadgeToUserParams struct {
UserID int64 `json:"user_id"`
BadgeID uuid.UUID `json:"badge_id"`
}
func (q *Queries) AddBadgeToUser(ctx context.Context, arg AddBadgeToUserParams) (UsersBadge, error) {
row := q.db.QueryRow(ctx, addBadgeToUser, arg.UserID, arg.BadgeID)
var i UsersBadge
err := row.Scan(&i.UserID, &i.BadgeID)
return i, err
}
const deleteBadge = `-- name: DeleteBadge :exec
DELETE FROM public.badges
WHERE
id = $1
`
func (q *Queries) DeleteBadge(ctx context.Context, id uuid.UUID) error {
_, err := q.db.Exec(ctx, deleteBadge, id)
return err
}
const getAllBadges = `-- name: GetAllBadges :many
SELECT id, name, description, color
FROM public.badges
`
func (q *Queries) GetAllBadges(ctx context.Context) ([]Badge, error) {
rows, err := q.db.Query(ctx, getAllBadges)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Badge
for rows.Next() {
var i Badge
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Color,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getBadgeByID = `-- name: GetBadgeByID :one
SELECT id, name, description, color
FROM public.badges
WHERE id = $1
`
func (q *Queries) GetBadgeByID(ctx context.Context, id uuid.UUID) (Badge, error) {
row := q.db.QueryRow(ctx, getBadgeByID, id)
var i Badge
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Color,
)
return i, err
}
const getUserBadges = `-- name: GetUserBadges :many
SELECT b.id, b.name, b.description, b.color
FROM public.badges b
JOIN public.users_badges ub ON b.id = ub.badge_id
WHERE ub.user_id = $1
`
func (q *Queries) GetUserBadges(ctx context.Context, userID int64) ([]Badge, error) {
rows, err := q.db.Query(ctx, getUserBadges, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Badge
for rows.Next() {
var i Badge
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Color,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUsersWithBadge = `-- name: GetUsersWithBadge :many
SELECT u.user_id, u.username, u.email, u.password, u.created_at, u.is_admin, u.display_name
FROM public.users u
JOIN public.users_badges ub ON u.user_id = ub.user_id
WHERE ub.badge_id = $1
`
func (q *Queries) GetUsersWithBadge(ctx context.Context, badgeID uuid.UUID) ([]User, error) {
rows, err := q.db.Query(ctx, getUsersWithBadge, badgeID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []User
for rows.Next() {
var i User
if err := rows.Scan(
&i.UserID,
&i.Username,
&i.Email,
&i.Password,
&i.CreatedAt,
&i.IsAdmin,
&i.DisplayName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertBadge = `-- name: InsertBadge :one
INSERT INTO public.badges (id, name, description, color)
VALUES
(
uuid_generate_v4(),
$1,
$2,
$3
)
RETURNING id, name, description, color
`
type InsertBadgeParams struct {
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Color pgtype.Text `json:"color"`
}
func (q *Queries) InsertBadge(ctx context.Context, arg InsertBadgeParams) (Badge, error) {
row := q.db.QueryRow(ctx, insertBadge, arg.Name, arg.Description, arg.Color)
var i Badge
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Color,
)
return i, err
}
const removeBadgeFromUser = `-- name: RemoveBadgeFromUser :exec
DELETE FROM public.users_badges
WHERE
user_id = $1
AND badge_id = $2
`
type RemoveBadgeFromUserParams struct {
UserID int64 `json:"user_id"`
BadgeID uuid.UUID `json:"badge_id"`
}
func (q *Queries) RemoveBadgeFromUser(ctx context.Context, arg RemoveBadgeFromUserParams) error {
_, err := q.db.Exec(ctx, removeBadgeFromUser, arg.UserID, arg.BadgeID)
return err
}
const updateBadge = `-- name: UpdateBadge :one
UPDATE public.badges
SET
name = $2,
description = $3,
color = $4
WHERE
id = $1
RETURNING id, name, description, color
`
type UpdateBadgeParams struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Color pgtype.Text `json:"color"`
}
func (q *Queries) UpdateBadge(ctx context.Context, arg UpdateBadgeParams) (Badge, error) {
row := q.db.QueryRow(ctx, updateBadge,
arg.ID,
arg.Name,
arg.Description,
arg.Color,
)
var i Badge
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.Color,
)
return i, err
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: blogs_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: bookmarks_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: categories_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: comments_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: favorites_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: likes_queries.sql
package db_repo

View File

@ -1,13 +1,21 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
package db_repo
import (
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)
type Badge struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
Color pgtype.Text `json:"color"`
}
type Blog struct {
BlogID int64 `json:"blog_id"`
UserID int64 `json:"user_id"`
@ -71,10 +79,11 @@ type PostVote struct {
}
type Profile struct {
UserID int64 `json:"user_id"`
Bio pgtype.Text `json:"bio"`
AvatarUrl pgtype.Text `json:"avatar_url"`
WebsiteUrl pgtype.Text `json:"website_url"`
UserID int64 `json:"user_id"`
Bio pgtype.Text `json:"bio"`
AvatarUrl pgtype.Text `json:"avatar_url"`
WebsiteUrl pgtype.Text `json:"website_url"`
EmailVerified pgtype.Bool `json:"email_verified"`
}
type Tag struct {
@ -83,10 +92,16 @@ type Tag struct {
}
type User struct {
UserID int64 `json:"user_id"`
Username string `json:"username" validate:"required"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
CreatedAt pgtype.Timestamp `json:"created_at"`
IsAdmin bool `json:"is_admin"`
UserID int64 `json:"user_id"`
Username string `json:"username" validate:"required"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
CreatedAt pgtype.Timestamp `json:"created_at"`
IsAdmin bool `json:"is_admin"`
DisplayName pgtype.Text `json:"display_name"`
}
type UsersBadge struct {
UserID int64 `json:"user_id"`
BadgeID uuid.UUID `json:"badge_id"`
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: multi_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: post_tags_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: post_votes_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: posts_queries.sql
package db_repo
@ -146,45 +146,70 @@ 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
const getRandomPosts = `-- name: GetRandomPosts :one
WITH all_posts AS (
SELECT
COUNT(*) AS post_count
FROM
public.posts
),
filtered_posts AS (
SELECT
ARRAY(
SELECT
json_build_object(
'post_id', post_id::text, 'blog_id', blog_id::text,
'user_id', user_id::text, 'title', title,
'created_at', created_at, 'content', SUBSTRING(content FROM 1 FOR 300)
)
FROM
public.posts
ORDER BY
created_at DESC
LIMIT $1 OFFSET $3
) as selected_posts
)
SELECT
fp.selected_posts,
(ap.post_count - ($2 + 1) * $1)::int > 0 as has_next_page,
case
when (ap.post_count - ($2 + 1) * $1)::int > 0
then $2 + 1
else -1
end as next_page_index,
case
when (ap.post_count - ( $2 + 1 ) * $1 + 1 * $1)::int <= ap.post_count
then $2 - 1
else -1
end as prev_page_index
FROM
filtered_posts fp,
all_posts ap
`
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"`
type GetRandomPostsParams struct {
Column1 interface{} `json:"column_1"`
Column2 interface{} `json:"column_2"`
Offset int32 `json:"offset"`
}
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
type GetRandomPostsRow struct {
SelectedPosts interface{} `json:"selected_posts"`
HasNextPage bool `json:"has_next_page"`
NextPageIndex int32 `json:"next_page_index"`
PrevPageIndex int32 `json:"prev_page_index"`
}
func (q *Queries) GetRandomPosts(ctx context.Context, arg GetRandomPostsParams) (GetRandomPostsRow, error) {
row := q.db.QueryRow(ctx, getRandomPosts, arg.Column1, arg.Column2, arg.Offset)
var i GetRandomPostsRow
err := row.Scan(
&i.SelectedPosts,
&i.HasNextPage,
&i.NextPageIndex,
&i.PrevPageIndex,
)
return i, err
}
const updatePostBlogId = `-- name: UpdatePostBlogId :exec

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: profiles_queries.sql
package db_repo
@ -15,7 +15,7 @@ const clearProfileByUserId = `-- name: ClearProfileByUserId :one
UPDATE public.profiles
SET bio='', avatar_url='', website_url=''
WHERE user_id=$1
RETURNING user_id, bio, avatar_url, website_url
RETURNING user_id, bio, avatar_url, website_url, email_verified
`
func (q *Queries) ClearProfileByUserId(ctx context.Context, userID int64) (Profile, error) {
@ -26,6 +26,7 @@ func (q *Queries) ClearProfileByUserId(ctx context.Context, userID int64) (Profi
&i.Bio,
&i.AvatarUrl,
&i.WebsiteUrl,
&i.EmailVerified,
)
return i, err
}
@ -34,7 +35,7 @@ const createProfileForUser = `-- name: CreateProfileForUser :one
INSERT INTO public.profiles
(user_id, bio, avatar_url, website_url)
VALUES($1, '', '', '')
RETURNING user_id, bio, avatar_url, website_url
RETURNING user_id, bio, avatar_url, website_url, email_verified
`
func (q *Queries) CreateProfileForUser(ctx context.Context, userID int64) (Profile, error) {
@ -45,6 +46,7 @@ func (q *Queries) CreateProfileForUser(ctx context.Context, userID int64) (Profi
&i.Bio,
&i.AvatarUrl,
&i.WebsiteUrl,
&i.EmailVerified,
)
return i, err
}
@ -60,7 +62,7 @@ func (q *Queries) DeleteProfileByUserId(ctx context.Context, userID int64) error
}
const getProfileByUserId = `-- name: GetProfileByUserId :one
SELECT user_id, bio, avatar_url, website_url FROM public.profiles WHERE user_id = $1
SELECT user_id, bio, avatar_url, website_url, email_verified FROM public.profiles WHERE user_id = $1
`
func (q *Queries) GetProfileByUserId(ctx context.Context, userID int64) (Profile, error) {
@ -71,6 +73,7 @@ func (q *Queries) GetProfileByUserId(ctx context.Context, userID int64) (Profile
&i.Bio,
&i.AvatarUrl,
&i.WebsiteUrl,
&i.EmailVerified,
)
return i, err
}
@ -79,7 +82,7 @@ const updateProfileByUserId = `-- name: UpdateProfileByUserId :one
UPDATE public.profiles
SET bio=$2, avatar_url=$3, website_url=$4
WHERE user_id=$1
RETURNING user_id, bio, avatar_url, website_url
RETURNING user_id, bio, avatar_url, website_url, email_verified
`
type UpdateProfileByUserIdParams struct {
@ -102,6 +105,7 @@ func (q *Queries) UpdateProfileByUserId(ctx context.Context, arg UpdateProfileBy
&i.Bio,
&i.AvatarUrl,
&i.WebsiteUrl,
&i.EmailVerified,
)
return i, err
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: tags_queries.sql
package db_repo

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// sqlc v1.28.0
// source: users_queries.sql
package db_repo
@ -13,7 +13,7 @@ const createUser = `-- name: CreateUser :one
INSERT INTO public.users
(user_id, username, email, "password", created_at, is_admin)
VALUES($1, $2, $3, $4, CURRENT_TIMESTAMP, false)
RETURNING user_id, username, email, password, created_at, is_admin
RETURNING user_id, username, email, password, created_at, is_admin, display_name
`
type CreateUserParams struct {
@ -38,6 +38,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
&i.Password,
&i.CreatedAt,
&i.IsAdmin,
&i.DisplayName,
)
return i, err
}
@ -63,7 +64,7 @@ func (q *Queries) DeleteUserByUsername(ctx context.Context, username string) err
}
const getAllUsers = `-- name: GetAllUsers :many
SELECT user_id, username, email, password, created_at, is_admin FROM users
SELECT user_id, username, email, password, created_at, is_admin, display_name FROM users
`
func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
@ -82,6 +83,7 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
&i.Password,
&i.CreatedAt,
&i.IsAdmin,
&i.DisplayName,
); err != nil {
return nil, err
}
@ -94,7 +96,7 @@ func (q *Queries) GetAllUsers(ctx context.Context) ([]User, error) {
}
const getUserByEmailOrNickname = `-- name: GetUserByEmailOrNickname :one
SELECT user_id, username, email, password, created_at, is_admin FROM users WHERE username = $1 OR email = $2 LIMIT 1
SELECT user_id, username, email, password, created_at, is_admin, display_name FROM users WHERE username = $1 OR email = $2 LIMIT 1
`
type GetUserByEmailOrNicknameParams struct {
@ -112,12 +114,13 @@ func (q *Queries) GetUserByEmailOrNickname(ctx context.Context, arg GetUserByEma
&i.Password,
&i.CreatedAt,
&i.IsAdmin,
&i.DisplayName,
)
return i, err
}
const getUserById = `-- name: GetUserById :one
SELECT user_id, username, email, password, created_at, is_admin FROM users WHERE user_id = $1
SELECT user_id, username, email, password, created_at, is_admin, display_name FROM users WHERE user_id = $1
`
func (q *Queries) GetUserById(ctx context.Context, userID int64) (User, error) {
@ -130,12 +133,13 @@ func (q *Queries) GetUserById(ctx context.Context, userID int64) (User, error) {
&i.Password,
&i.CreatedAt,
&i.IsAdmin,
&i.DisplayName,
)
return i, err
}
const getUserByUsername = `-- name: GetUserByUsername :one
SELECT user_id, username, email, password, created_at, is_admin FROM users WHERE username = $1
SELECT user_id, username, email, password, created_at, is_admin, display_name FROM users WHERE username = $1
`
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
@ -148,6 +152,7 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
&i.Password,
&i.CreatedAt,
&i.IsAdmin,
&i.DisplayName,
)
return i, err
}
@ -167,7 +172,7 @@ const updateUserPasswordHash = `-- name: UpdateUserPasswordHash :one
UPDATE public.users
SET "password"=$1
WHERE user_id=$2
RETURNING user_id, username, email, password, created_at, is_admin
RETURNING user_id, username, email, password, created_at, is_admin, display_name
`
type UpdateUserPasswordHashParams struct {
@ -185,6 +190,7 @@ func (q *Queries) UpdateUserPasswordHash(ctx context.Context, arg UpdateUserPass
&i.Password,
&i.CreatedAt,
&i.IsAdmin,
&i.DisplayName,
)
return i, err
}

View File

@ -0,0 +1,3 @@
DROP TABLE IF EXISTS users_badges;
DROP TABLE IF EXISTS badges;

View File

@ -0,0 +1,16 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS public.badges(
id uuid PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
color VARCHAR(32)
);
CREATE TABLE IF NOT EXISTS public.users_badges(
user_id BIGINT NOT NULL,
badge_id uuid NOT NULL,
constraint users_badges_pkey primary key (user_id, badge_id),
CONSTRAINT "user_id_badge_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON DELETE CASCADE,
CONSTRAINT "badge_id_user_id_fkey" FOREIGN KEY ("badge_id") REFERENCES "public"."badges" ("id") ON DELETE CASCADE
);

View File

@ -0,0 +1,19 @@
-- Drop the explicit index (if not dropped automatically with the table)
DROP INDEX IF EXISTS "public"."profiles_user_id_idx";
-- Drop tables in reverse order to avoid foreign key conflicts
DROP TABLE IF EXISTS "public"."profiles" CASCADE;
DROP TABLE IF EXISTS "public"."post_votes" CASCADE;
DROP TABLE IF EXISTS "public"."post_tags" CASCADE;
DROP TABLE IF EXISTS "public"."tags" CASCADE;
DROP TABLE IF EXISTS "public"."likes" CASCADE;
DROP TABLE IF EXISTS "public"."favorites" CASCADE;
DROP TABLE IF EXISTS "public"."comments" CASCADE;
DROP TABLE IF EXISTS "public"."bookmarks" CASCADE;
DROP TABLE IF EXISTS "public"."posts" CASCADE;
DROP TABLE IF EXISTS "public"."blogs" CASCADE;
DROP TABLE IF EXISTS "public"."users" CASCADE;
DROP TABLE IF EXISTS "public"."categories" CASCADE;
-- Optionally, if the up migration created the schema (and you want to remove it):
-- DROP SCHEMA IF EXISTS "public" CASCADE;

View File

@ -0,0 +1,121 @@
CREATE SCHEMA IF NOT EXISTS "public";
-- Create "categories" table
CREATE TABLE IF NOT EXISTS "public"."categories" (
"category_id" integer NOT NULL,
"category_name" character varying(50) NOT NULL,
PRIMARY KEY ("category_id"),
CONSTRAINT "categories_category_name_key" UNIQUE ("category_name")
);
-- Create "users" table
CREATE TABLE IF NOT EXISTS "public"."users" (
"user_id" bigint NOT NULL,
"username" character varying(50) NOT NULL,
"email" character varying(100) NOT NULL,
"password" character varying(255) NOT NULL,
"created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
"is_admin" boolean NOT NULL,
"display_name" character varying(32) NULL,
PRIMARY KEY ("user_id"),
CONSTRAINT "users_email_key" UNIQUE ("email"),
CONSTRAINT "users_username_key" UNIQUE ("username")
);
-- Create "blogs" table
CREATE TABLE IF NOT EXISTS "public"."blogs" (
"blog_id" bigint NOT NULL,
"user_id" bigint NOT NULL,
"title" character varying(255) NULL,
"description" text NULL,
"category_id" integer NULL,
"created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("blog_id"),
CONSTRAINT "blogs_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."categories" ("category_id") ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT "blogs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Create "posts" table
CREATE TABLE IF NOT EXISTS "public"."posts" (
"post_id" bigint NOT NULL,
"blog_id" bigint NULL,
"user_id" bigint NOT NULL,
"title" character varying(255) NULL,
"content" text NULL,
"created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("post_id"),
CONSTRAINT "posts_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs" ("blog_id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "posts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Create "bookmarks" table
CREATE TABLE IF NOT EXISTS "public"."bookmarks" (
"user_id" bigint NOT NULL,
"post_id" bigint NOT NULL,
"bookmarked_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("user_id", "post_id"),
CONSTRAINT "bookmarks_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "bookmarks_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Create "comments" table
CREATE TABLE IF NOT EXISTS "public"."comments" (
"comment_id" bigint NOT NULL,
"post_id" bigint NULL,
"user_id" bigint NULL,
"content" text NULL,
"created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("comment_id"),
CONSTRAINT "comments_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "comments_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Create "favorites" table
CREATE TABLE IF NOT EXISTS "public"."favorites" (
"user_id" bigint NOT NULL,
"blog_id" bigint NOT NULL,
"favorited_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("user_id", "blog_id"),
CONSTRAINT "favorites_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs" ("blog_id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "favorites_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Create "likes" table
CREATE TABLE IF NOT EXISTS "public"."likes" (
"like_id" bigint NOT NULL,
"user_id" bigint NULL,
"comment_id" bigint NULL,
"created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("like_id"),
CONSTRAINT "likes_comment_id_fkey" FOREIGN KEY ("comment_id") REFERENCES "public"."comments" ("comment_id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "likes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Create "tags" table
CREATE TABLE IF NOT EXISTS "public"."tags" (
"tag_id" integer NOT NULL,
"tag_name" character varying(50) NOT NULL,
PRIMARY KEY ("tag_id"),
CONSTRAINT "tags_tag_name_key" UNIQUE ("tag_name")
);
-- Create "post_tags" table
CREATE TABLE IF NOT EXISTS "public"."post_tags" (
"post_id" bigint NOT NULL,
"tag_id" integer NOT NULL,
PRIMARY KEY ("post_id", "tag_id"),
CONSTRAINT "post_tags_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "post_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "public"."tags" ("tag_id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Create "post_votes" table
CREATE TABLE IF NOT EXISTS "public"."post_votes" (
"post_id" bigint NOT NULL,
"user_id" bigint NOT NULL,
"vote" boolean NOT NULL,
PRIMARY KEY ("post_id", "user_id"),
CONSTRAINT "post_votes_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "post_votes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE NO ACTION
);
-- Create "profiles" table
CREATE TABLE IF NOT EXISTS "public"."profiles" (
"user_id" bigint NOT NULL,
"bio" text NULL,
"avatar_url" character varying(255) NULL,
"website_url" character varying(100) NULL,
PRIMARY KEY ("user_id"),
CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE
);
-- Create index "profiles_user_id_idx" to table: "profiles"
CREATE UNIQUE INDEX IF NOT EXISTS "profiles_user_id_idx" ON "public"."profiles" ("user_id");

View File

@ -0,0 +1,2 @@
ALTER TABLE profiles
DROP COLUMN email_verified;

View File

@ -0,0 +1,2 @@
ALTER TABLE profiles
ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;

View File

@ -1,30 +0,0 @@
-- Add new schema named "public"
CREATE SCHEMA IF NOT EXISTS "public";
-- Set comment to schema: "public"
COMMENT ON SCHEMA "public" IS 'standard public schema';
-- Create "categories" table
CREATE TABLE "public"."categories" ("category_id" integer NOT NULL, "category_name" character varying(50) NOT NULL, PRIMARY KEY ("category_id"), CONSTRAINT "categories_category_name_key" UNIQUE ("category_name"));
-- Create "users" table
CREATE TABLE "public"."users" ("user_id" bigint NOT NULL, "username" character varying(50) NOT NULL, "email" character varying(100) NOT NULL, "password" character varying(255) NOT NULL, "created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, "is_admin" boolean NOT NULL, PRIMARY KEY ("user_id"), CONSTRAINT "users_email_key" UNIQUE ("email"), CONSTRAINT "users_username_key" UNIQUE ("username"));
-- Create "blogs" table
CREATE TABLE "public"."blogs" ("blog_id" bigint NOT NULL, "user_id" bigint NOT NULL, "title" character varying(255) NULL, "description" text NULL, "category_id" integer NULL, "created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("blog_id"), CONSTRAINT "blogs_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "public"."categories" ("category_id") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "blogs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "posts" table
CREATE TABLE "public"."posts" ("post_id" bigint NOT NULL, "blog_id" bigint NULL, "user_id" bigint NOT NULL, "title" character varying(255) NULL, "content" text NULL, "created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("post_id"), CONSTRAINT "posts_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs" ("blog_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "posts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "bookmarks" table
CREATE TABLE "public"."bookmarks" ("user_id" bigint NOT NULL, "post_id" bigint NOT NULL, "bookmarked_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("user_id", "post_id"), CONSTRAINT "bookmarks_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "bookmarks_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "comments" table
CREATE TABLE "public"."comments" ("comment_id" bigint NOT NULL, "post_id" bigint NULL, "user_id" bigint NULL, "content" text NULL, "created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("comment_id"), CONSTRAINT "comments_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "comments_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "favorites" table
CREATE TABLE "public"."favorites" ("user_id" bigint NOT NULL, "blog_id" bigint NOT NULL, "favorited_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("user_id", "blog_id"), CONSTRAINT "favorites_blog_id_fkey" FOREIGN KEY ("blog_id") REFERENCES "public"."blogs" ("blog_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "favorites_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "likes" table
CREATE TABLE "public"."likes" ("like_id" bigint NOT NULL, "user_id" bigint NULL, "comment_id" bigint NULL, "created_at" timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("like_id"), CONSTRAINT "likes_comment_id_fkey" FOREIGN KEY ("comment_id") REFERENCES "public"."comments" ("comment_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "likes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "tags" table
CREATE TABLE "public"."tags" ("tag_id" integer NOT NULL, "tag_name" character varying(50) NOT NULL, PRIMARY KEY ("tag_id"), CONSTRAINT "tags_tag_name_key" UNIQUE ("tag_name"));
-- Create "post_tags" table
CREATE TABLE "public"."post_tags" ("post_id" bigint NOT NULL, "tag_id" integer NOT NULL, PRIMARY KEY ("post_id", "tag_id"), CONSTRAINT "post_tags_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE CASCADE, CONSTRAINT "post_tags_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "public"."tags" ("tag_id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create "post_votes" table
CREATE TABLE "public"."post_votes" ("post_id" bigint NOT NULL, "user_id" bigint NOT NULL, "vote" boolean NOT NULL, PRIMARY KEY ("post_id", "user_id"), CONSTRAINT "post_votes_post_id_fkey" FOREIGN KEY ("post_id") REFERENCES "public"."posts" ("post_id") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "post_votes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE NO ACTION);
-- Create "profiles" table
CREATE TABLE "public"."profiles" ("user_id" bigint NOT NULL, "bio" text NULL, "avatar_url" character varying(255) NULL, "website_url" character varying(100) NULL, PRIMARY KEY ("user_id"), CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("user_id") ON UPDATE NO ACTION ON DELETE CASCADE);
-- Create index "profiles_user_id_idx" to table: "profiles"
CREATE UNIQUE INDEX "profiles_user_id_idx" ON "public"."profiles" ("user_id");

View File

@ -0,0 +1,61 @@
-- name: InsertBadge :one
INSERT INTO public.badges (id, name, description, color)
VALUES
(
uuid_generate_v4(),
$1,
$2,
$3
)
RETURNING *;
-- name: UpdateBadge :one
UPDATE public.badges
SET
name = $2,
description = $3,
color = $4
WHERE
id = $1
RETURNING *;
-- name: DeleteBadge :exec
DELETE FROM public.badges
WHERE
id = $1;
-- name: GetAllBadges :many
SELECT *
FROM public.badges;
-- name: GetBadgeByID :one
SELECT *
FROM public.badges
WHERE id = $1;
-- name: AddBadgeToUser :one
INSERT INTO public.users_badges (user_id, badge_id)
VALUES
(
$1,
$2
)
RETURNING *;
-- name: RemoveBadgeFromUser :exec
DELETE FROM public.users_badges
WHERE
user_id = $1
AND badge_id = $2;
-- name: GetUserBadges :many
SELECT b.*
FROM public.badges b
JOIN public.users_badges ub ON b.id = ub.badge_id
WHERE ub.user_id = $1;
-- name: GetUsersWithBadge :many
SELECT u.*
FROM public.users u
JOIN public.users_badges ub ON u.user_id = ub.user_id
WHERE ub.badge_id = $1;

View File

@ -35,8 +35,42 @@ SET blog_id=$2, updated_at=CURRENT_TIMESTAMP
WHERE post_id = $1
RETURNING *;
-- name: GetRandomPosts :many
SELECT post_id, blog_id, user_id, title, created_at
FROM public.posts
ORDER BY RANDOM()
LIMIT $1;
-- name: GetRandomPosts :one
WITH all_posts AS (
SELECT
COUNT(*) AS post_count
FROM
public.posts
),
filtered_posts AS (
SELECT
ARRAY(
SELECT
json_build_object(
'post_id', post_id::text, 'blog_id', blog_id::text,
'user_id', user_id::text, 'title', title,
'created_at', created_at, 'content', SUBSTRING(content FROM 1 FOR 300)
)
FROM
public.posts
ORDER BY
created_at DESC
LIMIT $1 OFFSET $3
) as selected_posts
)
SELECT
fp.selected_posts,
(ap.post_count - ($2 + 1) * $1)::int > 0 as has_next_page,
case
when (ap.post_count - ($2 + 1) * $1)::int > 0
then $2 + 1
else -1
end as next_page_index,
case
when (ap.post_count - ( $2 + 1 ) * $1 + 1 * $1)::int <= ap.post_count
then $2 - 1
else -1
end as prev_page_index
FROM
filtered_posts fp,
all_posts ap;

View File

@ -22,7 +22,7 @@ func TargetMiddleware() gin.HandlerFunc {
case "POST":
c.Set("target", POST)
case "GET":
c.Set("target", DELETE)
c.Set("target", GET)
}
c.Next()

View File

@ -16,7 +16,7 @@ func GetClaimsFromContext(c *gin.Context) (auth.UserInfoJWT, error) {
claims, exists := c.Get(global.ContextTokenData)
if !exists {
return auth.UserInfoJWT{}, fmt.Errorf("error getting user id")
return auth.UserInfoJWT{}, fmt.Errorf("error getting user id 1")
}
parsedUserId, err := strconv.ParseInt(

View File

@ -12,7 +12,7 @@ func GetUserIdFromContext(c *gin.Context) (int64, error) {
userId, exists := c.Get(global.ContextUserId)
if !exists {
return -1, fmt.Errorf("error getting user id")
return -1, fmt.Errorf("error getting user id 2")
}
if parsedUserId, err := strconv.ParseInt(userId.(string), 10, 64); err != nil {

View File

@ -14,6 +14,7 @@ const (
POST_BLOG_MIDDLEWARE = "POST_BLOG_MIDDLEWARE"
POST_VOTE_MIDDLEWARE = "POST_VOTE_MIDDLEWARE"
POST_VOTES_MIDDLEWARE = "POST_VOTES_MIDDLEWARE"
USER_MIDDLEWARE = "USER_MIDDLEWARE"
)
var MiddlewareProvider = middleware.MiddlewareProvider{
@ -152,6 +153,25 @@ var policiesToRegister = map[string]middleware.RulesToCheck{
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
},
USER_MIDDLEWARE: {
middleware.GET: {
Rules: make([]rules.RuleFunction, 0),
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.PUT: {
Rules: []rules.RuleFunction{
globalrules.AuthorizedRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
middleware.DELETE: {
Rules: []rules.RuleFunction{
globalrules.IsAdminRule,
},
MustBeCompleted: rules.ALL_RULES_MUST_BE_COMPLETED,
},
},
}
func InitMiddlewareProvider() {

View File

@ -19,25 +19,38 @@ func GetRandomPost(c *gin.Context) {
return
}
postsData, err :=
db_repo.New(db_connection.Dbx).
GetRandomPosts(context.Background(), int32(limit))
offset, err := strconv.Atoi(c.DefaultQuery("offset", "0"))
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)),
})
params := db_repo.GetRandomPostsParams{
Column1: int32(limit),
Column2: int32(offset),
Offset: int32(offset * limit),
}
c.IndentedJSON(http.StatusOK, result)
postsData, err :=
db_repo.New(db_connection.Dbx).
GetRandomPosts(context.Background(), params)
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, postsData)
}

View File

@ -194,6 +194,14 @@ func SetupRotes(g *gin.Engine) error {
voteroutes.GetVotes,
)
userGroup := g.Group("/users/")
userGroup.Use(MiddlewareProvider.GetMiddleware(USER_MIDDLEWARE))
userGroup.GET(
"/info/:user-id",
userroutes.GetUserInfo,
)
// Admin group routes
adminGroup := g.Group("/admin/")
adminGroup.Use(middleware.AdminMiddleware())

View File

@ -0,0 +1,35 @@
package userroutes
import (
"context"
db_repo "enshi/db/go_queries"
"enshi/db_connection"
"enshi/middleware/getters"
"github.com/gin-gonic/gin"
)
func GetUserInfo(c *gin.Context) {
userId, err := getters.GetInt64Param(c, "user-id")
if err != nil {
c.JSON(400, gin.H{"error": "Invalid user ID"})
return
}
userInfo, err := db_repo.New(db_connection.Dbx).GetUserById(context.Background(), userId)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to retrieve user information"})
return
}
userProfileInfo, err := db_repo.New(db_connection.Dbx).GetProfileByUserId(context.Background(), userId)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to retrieve user profile information"})
return
}
c.JSON(200, gin.H{
"user_info": userInfo,
"profile_info": userProfileInfo,
})
}