Compare commits
19 Commits
develop
...
feature/us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1659e007e4 | ||
|
|
d1474bce35 | ||
|
|
b82dddc25d | ||
|
|
f176741afa | ||
|
|
f70582ce83 | ||
|
|
c36e4a7f7d | ||
|
|
3bb21f67e9 | ||
|
|
facaa96955 | ||
|
|
6a22797610 | ||
|
|
6d6babc305 | ||
|
|
474d76840b | ||
|
|
ee24349680 | ||
|
|
c0dcbcf2c5 | ||
|
|
547bc9e9e9 | ||
|
|
ad1aff3692 | ||
|
|
6054bc0403 | ||
|
|
0eb840b82a | ||
|
|
7edaa62cdf | ||
|
|
f99e39c712 |
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": false
|
||||
"useTabs": true,
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"quoteProps": "consistent"
|
||||
}
|
||||
|
||||
@ -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
142
enshi/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
85
enshi/src/Components/Editor/TrueEditor.tsx
Normal file
85
enshi/src/Components/Editor/TrueEditor.tsx
Normal 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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
29
enshi/src/Components/ProfileNavbar/ButtonLink/ButtonLink.tsx
Normal file
29
enshi/src/Components/ProfileNavbar/ButtonLink/ButtonLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
67
enshi/src/Components/ProfileNavbar/ProfileNavbar.tsx
Normal file
67
enshi/src/Components/ProfileNavbar/ProfileNavbar.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
enshi/src/Components/ProfileNavbar/TabLink/TabLink.tsx
Normal file
22
enshi/src/Components/ProfileNavbar/TabLink/TabLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
92
enshi/src/Components/UserCard/UserCard.tsx
Normal file
92
enshi/src/Components/UserCard/UserCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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%]"
|
||||
>
|
||||
|
||||
@ -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,
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
8
enshi/src/Pages/UserPostsPage/UserPostsPage.tsx
Normal file
8
enshi/src/Pages/UserPostsPage/UserPostsPage.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
export default function UserPostsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>User Posts Page</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
83
enshi/src/Pages/UserProfilePage/UserProfilePage.tsx
Normal file
83
enshi/src/Pages/UserProfilePage/UserProfilePage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
8
enshi/src/Pages/UserSecurityPage/UserSecurityPage.tsx
Normal file
8
enshi/src/Pages/UserSecurityPage/UserSecurityPage.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
|
||||
export default function UserSecurityPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>User Security Page</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
enshi/src/constants/loremText.ts
Normal file
21
enshi/src/constants/loremText.ts
Normal 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.
|
||||
`;
|
||||
@ -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 */
|
||||
|
||||
@ -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>
|
||||
|
||||
26
enshi/src/layout/ProfilePage/ProfilePage.tsx
Normal file
26
enshi/src/layout/ProfilePage/ProfilePage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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 />}>
|
||||
|
||||
@ -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"
|
||||
},
|
||||
|
||||
15
enshi_back/db/create_migration.sh
Executable file
15
enshi_back/db/create_migration.sh
Executable 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"
|
||||
243
enshi_back/db/go_queries/badge_queries.sql.go
Normal file
243
enshi_back/db/go_queries/badge_queries.sql.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// sqlc v1.28.0
|
||||
|
||||
package db_repo
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
3
enshi_back/db/migrations/0003_badge_table.down.sql
Normal file
3
enshi_back/db/migrations/0003_badge_table.down.sql
Normal file
@ -0,0 +1,3 @@
|
||||
DROP TABLE IF EXISTS users_badges;
|
||||
|
||||
DROP TABLE IF EXISTS badges;
|
||||
16
enshi_back/db/migrations/0003_badge_table.up.sql
Normal file
16
enshi_back/db/migrations/0003_badge_table.up.sql
Normal 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
|
||||
);
|
||||
19
enshi_back/db/migrations/01_migration.down.sql
Normal file
19
enshi_back/db/migrations/01_migration.down.sql
Normal 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;
|
||||
121
enshi_back/db/migrations/01_migration.up.sql
Normal file
121
enshi_back/db/migrations/01_migration.up.sql
Normal 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");
|
||||
2
enshi_back/db/migrations/02_migration.down.sql
Normal file
2
enshi_back/db/migrations/02_migration.down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE profiles
|
||||
DROP COLUMN email_verified;
|
||||
2
enshi_back/db/migrations/02_migration.up.sql
Normal file
2
enshi_back/db/migrations/02_migration.up.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE profiles
|
||||
ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;
|
||||
@ -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");
|
||||
61
enshi_back/db/queries/badge_queries.sql
Normal file
61
enshi_back/db/queries/badge_queries.sql
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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)
|
||||
|
||||
}
|
||||
|
||||
@ -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())
|
||||
|
||||
35
enshi_back/routes/userRoutes/getUserInfo.go
Normal file
35
enshi_back/routes/userRoutes/getUserInfo.go
Normal 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,
|
||||
})
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user