commit
81a3cc5971
4
.gitignore
vendored
4
.gitignore
vendored
@ -13,6 +13,10 @@ dist-ssr
|
||||
*.local
|
||||
secret.env
|
||||
|
||||
privkey.pem
|
||||
fullchain.pem
|
||||
gin.log
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -1,5 +1,7 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"godotenv"
|
||||
"downvotes",
|
||||
"godotenv",
|
||||
"upvotes"
|
||||
]
|
||||
}
|
||||
25
compose.yml
Normal file
25
compose.yml
Normal file
@ -0,0 +1,25 @@
|
||||
services:
|
||||
|
||||
nginx:
|
||||
build: ./enshi
|
||||
ports:
|
||||
- 127.0.0.1:80:80
|
||||
- 127.0.0.1:443:443
|
||||
networks:
|
||||
- app-network
|
||||
restart: unless-stopped
|
||||
|
||||
enshi_back:
|
||||
build: ./enshi_back
|
||||
ports:
|
||||
- 127.0.0.1:9876:9876
|
||||
networks:
|
||||
- app-network
|
||||
environment:
|
||||
- ENV=docker
|
||||
- DOMAIN=localhost
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
1
enshi/.dockerignore
Normal file
1
enshi/.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
||||
22
enshi/Dockerfile
Normal file
22
enshi/Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
ENV VITE_ENV=docker
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY ./nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Certificates
|
||||
COPY ./nginx/fullchain.pem /etc/nginx/ssl/
|
||||
COPY ./nginx/privkey.pem /etc/nginx/ssl/
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
@ -1,50 +1 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
# README
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
37
enshi/nginx/nginx.conf
Normal file
37
enshi/nginx/nginx.conf
Normal file
@ -0,0 +1,37 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
# server_name nekiiinkognito.ru www.nekiiinkognito.ru;
|
||||
server_name localhost;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers EECDH+AESGCM:EDH+AESGCM;
|
||||
ssl_ecdh_curve secp384r1;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/v1/ {
|
||||
proxy_pass http://enshi_back:9876/;
|
||||
}
|
||||
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
}
|
||||
647
enshi/package-lock.json
generated
647
enshi/package-lock.json
generated
@ -8,17 +8,25 @@
|
||||
"name": "enshi",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-form": "^0.1.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@radix-ui/themes": "^3.1.3",
|
||||
"@tanstack/react-query": "^5.55.0",
|
||||
"@tanstack/react-query-devtools": "^5.61.0",
|
||||
"axios": "^1.7.7",
|
||||
"html-react-parser": "^5.1.16",
|
||||
"i18n": "^0.15.1",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"interweave": "^13.1.0",
|
||||
"jotai": "^2.9.3",
|
||||
"jotai-immer": "^0.4.1",
|
||||
"primereact": "^10.8.2",
|
||||
"quill": "^2.0.2",
|
||||
"react": "^18.3.1",
|
||||
@ -1226,6 +1234,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz",
|
||||
"integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.0",
|
||||
"@radix-ui/react-focus-guards": "1.1.0",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||
@ -1413,25 +1457,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz",
|
||||
"integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz",
|
||||
"integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.0",
|
||||
"@radix-ui/react-focus-guards": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.2",
|
||||
"@radix-ui/react-presence": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.7"
|
||||
"react-remove-scroll": "2.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@ -1448,6 +1492,158 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
|
||||
"integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
|
||||
"integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
||||
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
|
||||
"integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.6",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll-bar": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
|
||||
"integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
@ -1618,6 +1814,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-icons": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
|
||||
"integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||
@ -2029,17 +2234,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz",
|
||||
"integrity": "sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.1.tgz",
|
||||
"integrity": "sha512-FnM1fHfCtEZ1JkyfH/1oMiTcFBQvHKl4vD9WnpwkLgtF+UmnXMCad6ECPTaAjcDjam+ndOEJWgHyKDGNteWSHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
@ -2059,6 +2264,45 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
||||
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz",
|
||||
@ -2212,6 +2456,130 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz",
|
||||
"integrity": "sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-collection": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||
"@radix-ui/react-portal": "1.1.2",
|
||||
"@radix-ui/react-presence": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
|
||||
"integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
||||
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-toggle": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz",
|
||||
@ -2503,6 +2871,42 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/themes/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz",
|
||||
"integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.0",
|
||||
"@radix-ui/react-focus-guards": "1.1.0",
|
||||
"@radix-ui/react-focus-scope": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.1",
|
||||
"@radix-ui/react-presence": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/themes/node_modules/@radix-ui/react-navigation-menu": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.0.tgz",
|
||||
@ -2539,6 +2943,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/themes/node_modules/@radix-ui/react-scroll-area": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.1.0.tgz",
|
||||
"integrity": "sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/number": "1.1.0",
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/themes/node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.1.tgz",
|
||||
@ -2807,9 +3242,19 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.54.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.54.1.tgz",
|
||||
"integrity": "sha512-hKS+WRpT5zBFip21pB6Jx1C0hranWQrbv5EJ7qPoiV5MYI3C8rTCqWC9DdBseiPT1JgQWh8Y55YthuYZNiw3Xw==",
|
||||
"version": "5.60.6",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.6.tgz",
|
||||
"integrity": "sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-devtools": {
|
||||
"version": "5.59.20",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.59.20.tgz",
|
||||
"integrity": "sha512-vxhuQ+8VV4YWQSFxQLsuM+dnEKRY7VeRzpNabFXdhEwsBYLrjXlF1pM38A8WyKNLqZy8JjyRO8oP4Wd/oKHwuQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@ -2817,12 +3262,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.55.0.tgz",
|
||||
"integrity": "sha512-2uYuxEbRQD8TORUiTUacEOwt1e8aoSqUOJFGY5TUrh6rQ3U85zrMS2wvbNhBhXGh6Vj69QDCP2yv8tIY7joo6Q==",
|
||||
"version": "5.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.61.0.tgz",
|
||||
"integrity": "sha512-SBzV27XAeCRBOQ8QcC94w2H1Md0+LI0gTWwc3qRJoaGuewKn5FNW4LSqwPFJZVEItfhMfGT7RpZuSFXjTi12pQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.54.1"
|
||||
"@tanstack/query-core": "5.60.6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@ -2832,6 +3277,23 @@
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query-devtools": {
|
||||
"version": "5.61.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.61.0.tgz",
|
||||
"integrity": "sha512-hd3yXl+KV+OGQmAw946qHAFp6DygcXcYN+1ai9idYddx6uEQyCwYk3jyIBOQEUw9uzN5DOGJLBsgd/QcimDQsA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.59.20"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.61.0",
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@ -3336,6 +3798,12 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.20",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||
@ -3374,6 +3842,17 @@
|
||||
"postcss": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.7.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
|
||||
"integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
@ -3602,6 +4081,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@ -3738,6 +4229,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-node-es": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||
@ -3926,6 +4426,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
@ -4357,6 +4863,26 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.9",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz",
|
||||
@ -4374,6 +4900,20 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fraction.js": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
|
||||
@ -4745,6 +5285,16 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
@ -4778,6 +5328,22 @@
|
||||
"integrity": "sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/interweave": {
|
||||
"version": "13.1.0",
|
||||
"resolved": "https://registry.npmjs.org/interweave/-/interweave-13.1.0.tgz",
|
||||
"integrity": "sha512-JIDq0+2NYg0cgL7AB26fBcV0yZdiJvPDBp+aF6k8gq6Cr1kH5Gd2/Xqn7j8z+TGb8jCWZn739jzalCz+nPYwcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-html": "^1.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "ko-fi",
|
||||
"url": "https://ko-fi.com/milesjohnson"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
@ -4970,6 +5536,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jotai-immer": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jotai-immer/-/jotai-immer-0.4.1.tgz",
|
||||
"integrity": "sha512-nQTt1HBKie/5OJDck1qLpV1PeBA6bjJLAczEYAx70PD8R4Mbu7gtexfBUCzJh6W6ecsOfwHksAYAesVth6SN9A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"immer": ">=9.0.0",
|
||||
"jotai": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@ -5185,6 +5761,27 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@ -5712,6 +6309,12 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
||||
@ -1,26 +1,35 @@
|
||||
{
|
||||
"name": "enshi",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint-build": "tsc -b && vite build",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-form": "^0.1.0",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.1",
|
||||
"@radix-ui/react-scroll-area": "^1.2.1",
|
||||
"@radix-ui/react-toast": "^1.2.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@radix-ui/themes": "^3.1.3",
|
||||
"@tanstack/react-query": "^5.55.0",
|
||||
"@tanstack/react-query-devtools": "^5.61.0",
|
||||
"axios": "^1.7.7",
|
||||
"html-react-parser": "^5.1.16",
|
||||
"i18n": "^0.15.1",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"interweave": "^13.1.0",
|
||||
"jotai": "^2.9.3",
|
||||
"jotai-immer": "^0.4.1",
|
||||
"primereact": "^10.8.2",
|
||||
"quill": "^2.0.2",
|
||||
"react": "^18.3.1",
|
||||
|
||||
8
enshi/src/@types/PostTypes.ts
Normal file
8
enshi/src/@types/PostTypes.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export type GetRandomPostsRow = {
|
||||
post_id: string;
|
||||
// blog_id: number;
|
||||
user_id: string;
|
||||
title: string;
|
||||
// created_at: Date;
|
||||
}
|
||||
|
||||
5
enshi/src/@types/UserType.ts
Normal file
5
enshi/src/@types/UserType.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export type TUser = {
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
id?: string | number;
|
||||
}
|
||||
11
enshi/src/@types/index.d.ts
vendored
Normal file
11
enshi/src/@types/index.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
type TToast = {
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: React.Component;
|
||||
};
|
||||
|
||||
type TExistingToast = TToast & {
|
||||
id: number;
|
||||
resetFunc: (arg0: boolean) => void;
|
||||
open: boolean;
|
||||
};
|
||||
@ -19,6 +19,10 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: "Times New Roman";
|
||||
}
|
||||
|
||||
/*!
|
||||
* Quill Editor v1.3.6
|
||||
* https://quilljs.com/
|
||||
|
||||
@ -1,42 +1,26 @@
|
||||
import "./App.css";
|
||||
import "@radix-ui/themes/styles.css";
|
||||
import { Theme, ThemePanel } from "@radix-ui/themes";
|
||||
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import "@radix-ui/themes/styles.css";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import queryClient from "./api/QueryClient/QueryClient";
|
||||
import { routes } from "./routes/routes";
|
||||
import { useEffect } from "react";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import "axios";
|
||||
import { axiosLocalhost } from "./api/axios/axios";
|
||||
import { createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
import queryClient from "./api/QueryClient/QueryClient";
|
||||
import "./App.css";
|
||||
import ToastProvider from "./Components/ToastProvider/ToastProvider";
|
||||
import { routes } from "./routes/routes";
|
||||
|
||||
const router = createBrowserRouter(routes);
|
||||
|
||||
export default function App() {
|
||||
useEffect(() => {
|
||||
let f = async () => {
|
||||
let c = await axiosLocalhost.post(
|
||||
"/login",
|
||||
{
|
||||
nickname: "StasikChess",
|
||||
password: "123456",
|
||||
}
|
||||
);
|
||||
|
||||
console.log(c.headers);
|
||||
console.log(document.cookie);
|
||||
};
|
||||
|
||||
f();
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Theme className="h-fit" accentColor="indigo" grayColor="slate">
|
||||
<ToastProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
<ThemePanel />
|
||||
<ReactQueryDevtools/>
|
||||
</QueryClientProvider>
|
||||
</ToastProvider>
|
||||
</Theme>
|
||||
);
|
||||
}
|
||||
|
||||
50
enshi/src/AtomStore/AtomStore.ts
Normal file
50
enshi/src/AtomStore/AtomStore.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
import { TUser } from "../@types/UserType";
|
||||
|
||||
export const userAtom = atom<TUser>();
|
||||
|
||||
export const postCreationAtom = atom<string>();
|
||||
export const postCreationTitleAtom = atom<string>();
|
||||
|
||||
type TPostData = {
|
||||
title: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export const storagePostAtom = atomWithStorage<TPostData>(
|
||||
"draft-post",
|
||||
{ title: "", content: "" },
|
||||
{
|
||||
getItem: (key) => sessionStorage.getItem(key) as any,
|
||||
setItem: (key, value) => sessionStorage.setItem(key, value as any),
|
||||
removeItem: (key) => sessionStorage.removeItem(key),
|
||||
},
|
||||
{ getOnInit: true }
|
||||
);
|
||||
|
||||
export const toastAtom = atom<TExistingToast[]>([]);
|
||||
export const setToastAtom = atom(null, (get, set, value: TToast) => {
|
||||
let maxToastId = Math.max(...get(toastAtom).map((toast) => toast.id));
|
||||
maxToastId = maxToastId >= 0 ? maxToastId : 1;
|
||||
let atomValueWithNewToast = get(toastAtom);
|
||||
atomValueWithNewToast = [
|
||||
...atomValueWithNewToast,
|
||||
{
|
||||
id: maxToastId + 1,
|
||||
resetFunc: (_) => {
|
||||
let currentToasts = get(toastAtom);
|
||||
let afterRemoval = currentToasts.filter(
|
||||
(toast) => toast.id != maxToastId + 1
|
||||
);
|
||||
set(toastAtom, afterRemoval);
|
||||
},
|
||||
title: value.title,
|
||||
action: value.action,
|
||||
description: value.description,
|
||||
open: true,
|
||||
},
|
||||
];
|
||||
|
||||
set(toastAtom, atomValueWithNewToast);
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
import { Container } from "@radix-ui/themes";
|
||||
import React from "react";
|
||||
|
||||
export default function ArcticleViewer() {
|
||||
return (
|
||||
<>
|
||||
<div className="ql-snow">
|
||||
<Container className="mt-4 ql-editor">
|
||||
</Container>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
146
enshi/src/Components/ArticleViewer/ArticleViewer.tsx
Normal file
146
enshi/src/Components/ArticleViewer/ArticleViewer.tsx
Normal file
@ -0,0 +1,146 @@
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Flex,
|
||||
Select,
|
||||
Separator,
|
||||
Text,
|
||||
} from "@radix-ui/themes";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Interweave } from "interweave";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { axiosLocalhost } from "../../api/axios/axios";
|
||||
import { userAtom } from "../../AtomStore/AtomStore";
|
||||
import ChangePostButton from "./ChangePostButton/ChangePostButton";
|
||||
import SkeletonPostLoader from "./SkeletonLoader/SkeletonLoader";
|
||||
import VoteButton, { DOWNVOTE, UPVOTE } from "./VoteButton/VoteButton";
|
||||
import VoteCounter from "./VoteCounter/VoteCounter";
|
||||
|
||||
type TArticleViewer = {
|
||||
htmlToParse?: string;
|
||||
};
|
||||
|
||||
export default function ArticleViewer(props: TArticleViewer) {
|
||||
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"]}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
},
|
||||
gcTime: 0,
|
||||
refetchOnMount: true,
|
||||
});
|
||||
|
||||
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"] || ""}
|
||||
/>
|
||||
|
||||
<VoteCounter
|
||||
postId={queryParams["postId"] || ""}
|
||||
/>
|
||||
|
||||
<VoteButton
|
||||
vote={DOWNVOTE}
|
||||
postId={queryParams["postId"] || ""}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Box hidden={data.user_id != user?.id}>
|
||||
<ChangePostButton
|
||||
postId={queryParams["postId"] || ""}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<Button
|
||||
variant="surface"
|
||||
className="h-5"
|
||||
>
|
||||
Add to blog
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-blackA6 data-[state=open]:animate-overlayShow" />
|
||||
<Dialog.Content className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[450px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none data-[state=open]:animate-contentShow">
|
||||
<Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
|
||||
Add this post to blog
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
|
||||
<Flex>
|
||||
<Text>
|
||||
{`Add "${data.title}" to blog...`}
|
||||
</Text>
|
||||
<Select.Root defaultValue="apple">
|
||||
<Select.Trigger />
|
||||
<Select.Content>
|
||||
<Select.Group>
|
||||
<Select.Item value="orange">
|
||||
This
|
||||
</Select.Item>
|
||||
<Select.Item value="apple">
|
||||
This is
|
||||
updated blog
|
||||
</Select.Item>
|
||||
<Select.Item value="grape">
|
||||
This another
|
||||
</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Flex>
|
||||
</Dialog.Description>
|
||||
|
||||
<div className="mt-[25px] flex justify-end">
|
||||
<Dialog.Close asChild>
|
||||
<Button>Confirm</Button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Cross2Icon />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Separator size={"4"} className="mb-2" />
|
||||
<Interweave content={data.content} />
|
||||
</Container>
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { Button } from "@radix-ui/themes";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type TChangePostButton = {
|
||||
postId: number | string;
|
||||
};
|
||||
|
||||
export default function ChangePostButton(props: TChangePostButton) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Button
|
||||
size={"1"}
|
||||
className="h-5"
|
||||
variant="surface"
|
||||
onClick={() => navigate("/posts/change/" + props.postId)}
|
||||
>
|
||||
{"Change article"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
import { Container, Skeleton, Text } from "@radix-ui/themes";
|
||||
import {
|
||||
headerLong,
|
||||
headerShort,
|
||||
pText,
|
||||
} from "../../../constants/textForSkeleton";
|
||||
|
||||
export default function SkeletonPostLoader() {
|
||||
return (
|
||||
<Container size={"2"} className="mt-4">
|
||||
<Skeleton>
|
||||
<Text size={"6"}>{headerLong}</Text>
|
||||
<br />
|
||||
<Text size={"6"}>{headerShort}</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Text>{pText}</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Text wrap={"pretty"}>{pText}</Text>
|
||||
<br />
|
||||
<br />
|
||||
<Text>{pText}</Text>
|
||||
</Skeleton>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
65
enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx
Normal file
65
enshi/src/Components/ArticleViewer/VoteButton/VoteButton.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { DoubleArrowDownIcon, DoubleArrowUpIcon } from "@radix-ui/react-icons";
|
||||
import { IconButton } from "@radix-ui/themes";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { axiosLocalhost } from "../../../api/axios/axios";
|
||||
|
||||
export const UPVOTE = true;
|
||||
export const DOWNVOTE = false;
|
||||
|
||||
type TVoteButton = {
|
||||
postId: string;
|
||||
vote: boolean;
|
||||
};
|
||||
|
||||
export default function VoteButton(props: TVoteButton) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: [props.vote + "voteCheck"],
|
||||
queryFn: async () => {
|
||||
const response = await axiosLocalhost.get(
|
||||
`post-vote/${props.postId}`
|
||||
);
|
||||
|
||||
return (response.data?.vote as boolean) === props.vote || false;
|
||||
},
|
||||
gcTime: 0,
|
||||
});
|
||||
|
||||
const voteMutation = useMutation({
|
||||
mutationKey: [`voteMutation${props.vote}`],
|
||||
onMutate: async () => {
|
||||
queryClient.cancelQueries({ queryKey: [props.vote + "voteCheck"] });
|
||||
|
||||
queryClient.setQueryData([props.vote + "voteCheck"], true);
|
||||
queryClient.setQueryData([!props.vote + "voteCheck"], false);
|
||||
},
|
||||
mutationFn: async () => {
|
||||
await axiosLocalhost.post(`post-votes/${props.postId}`, {
|
||||
vote: props.vote,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {},
|
||||
onError: () => {
|
||||
queryClient.setQueryData([props.vote + "voteCheck"], false);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [props.vote + "voteCheck"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["post_vote_counter"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
variant={data ? "solid" : "outline"}
|
||||
size={"1"}
|
||||
onClick={() => voteMutation.mutate()}
|
||||
>
|
||||
{props.vote ? <DoubleArrowUpIcon /> : <DoubleArrowDownIcon />}
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { Box, Skeleton } from "@radix-ui/themes";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { axiosLocalhost } from "../../../api/axios/axios";
|
||||
|
||||
type TVoteCounter = {
|
||||
postId: string;
|
||||
};
|
||||
|
||||
export default function VoteCounter(props: TVoteCounter) {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["post_vote_counter"],
|
||||
queryFn: async () => {
|
||||
const response = await axiosLocalhost.get(
|
||||
`post-votes/${props.postId}`
|
||||
);
|
||||
return response.data as { upvotes: number; downvotes: number };
|
||||
},
|
||||
});
|
||||
|
||||
const calculateRating = (upvotes: number, downvotes: number) => {
|
||||
return upvotes + (-downvotes)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton>
|
||||
{calculateRating(0, 0)}
|
||||
</Skeleton>
|
||||
}
|
||||
|
||||
return <Box>
|
||||
{calculateRating(data?.upvotes || 0, data?.downvotes || 0)}
|
||||
</Box>;
|
||||
}
|
||||
25
enshi/src/Components/BlogBox/BlogBox.tsx
Normal file
25
enshi/src/Components/BlogBox/BlogBox.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { Avatar, Card, Flex, Heading } from "@radix-ui/themes";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import UserNicknameLink from "../UserNicknameLink/UserNicknameLink";
|
||||
|
||||
type TBlogBox = {
|
||||
title?: string;
|
||||
blogId?: string;
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export default function BlogBox(props: TBlogBox) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Card className="w-full h-20" onClick={() => navigate(``)}>
|
||||
<Flex direction={"column"}>
|
||||
<Heading size={"4"}>{props?.title || "...No title..."}</Heading>
|
||||
<Flex align={"center"} gap={"2"} mt={"1"}>
|
||||
<Avatar size={"2"} className="rounded-full" fallback={"SI"} />
|
||||
<UserNicknameLink userId={props.userId} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -1,25 +1,19 @@
|
||||
import Quill, { Delta, } from "quill/core";
|
||||
import ReactQuill from "react-quill";
|
||||
import React, {
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import Sources from "quill";
|
||||
import Quill, { Delta } from "quill/core";
|
||||
import { forwardRef, useEffect, useRef, useState } from "react";
|
||||
import ReactQuill from "react-quill";
|
||||
|
||||
type TEditor = {
|
||||
readOnly?: boolean;
|
||||
defaultValue?: string | Delta;
|
||||
onChange: (d: string) => void; // TODO: make type
|
||||
onSelectionChange?: any; // TODO same as before
|
||||
onChange?: (d: string) => void;
|
||||
onSelectionChange?: any;
|
||||
};
|
||||
|
||||
const modules = {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
["bold", "italic", "underline", "strike", "blockquote"],
|
||||
[{ header: [1, 2, 3, 4, 5, false] }],
|
||||
["bold", "italic", "underline", "strike", "blockquote", "span-wrapper"],
|
||||
[
|
||||
{ list: "ordered" },
|
||||
{ list: "bullet" },
|
||||
@ -38,7 +32,9 @@ const modules = {
|
||||
const Editor = forwardRef((props: TEditor) => {
|
||||
const editor = useRef(null);
|
||||
const [quill, setQuill] = useState<Quill | null>(null);
|
||||
const [value, setValue] = useState(new Delta())
|
||||
const [value, setValue] = useState(new Delta());
|
||||
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editor.current) {
|
||||
@ -51,25 +47,41 @@ const Editor = forwardRef((props: TEditor) => {
|
||||
};
|
||||
}, [editor.current]);
|
||||
|
||||
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()
|
||||
props.onChange(val || "")
|
||||
setValue(fullDelta || new Delta())
|
||||
useEffect(() => {
|
||||
const quill = new Quill(document.createElement("div"));
|
||||
const t = quill.clipboard.convert({
|
||||
html: props.defaultValue as string,
|
||||
}) as Delta;
|
||||
|
||||
if (!loaded) {
|
||||
setValue(t);
|
||||
|
||||
console.log(t);
|
||||
}
|
||||
|
||||
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());
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-editor">
|
||||
<ReactQuill
|
||||
value={value}
|
||||
ref={editor}
|
||||
modules={modules}
|
||||
|
||||
|
||||
onChange={changeHandler}
|
||||
|
||||
|
||||
theme="snow"
|
||||
placeholder="Type your thoughts here..."
|
||||
/>
|
||||
|
||||
@ -1,67 +1,15 @@
|
||||
import { Button, Card, ChevronDownIcon, Text } from "@radix-ui/themes";
|
||||
import * as NavigationMenu from "@radix-ui/react-navigation-menu";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import CustomNavigationMenu from "./NavigationMenu/NavigationMenu";
|
||||
import RightButtonBar from "./RightButtonBar/RightButtonBar";
|
||||
import SearchField from "./SearchField/SearchField";
|
||||
|
||||
export default function NavBar() {
|
||||
return (
|
||||
<nav className="pt-2">
|
||||
<NavigationMenu.Root
|
||||
orientation="horizontal"
|
||||
className="flex justify-center"
|
||||
>
|
||||
<NavigationMenu.List className="flex justify-center gap-2">
|
||||
<NavItem text="Cringer" to="/" />
|
||||
<nav className="flex justify-center pt-2 pb-2 ml-4 mr-4 flex-[1] max-h-fit">
|
||||
<CustomNavigationMenu />
|
||||
|
||||
<NavItem text="C-Cringer" to="/c" />
|
||||
<SearchField />
|
||||
|
||||
<NavigationMenu.Item className="text-center">
|
||||
<NavigationMenu.Trigger className="flex items-center">
|
||||
<Button
|
||||
asChild
|
||||
className="w-fit pr-2 h-fit rounded-full m-0 p-0 pl-2 mt-2 mb-2 duration-[50ms]"
|
||||
variant="ghost"
|
||||
highContrast
|
||||
>
|
||||
<Text
|
||||
size={"3"}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
Cringer 123 <ChevronDownIcon />
|
||||
</Text>
|
||||
</Button>
|
||||
</NavigationMenu.Trigger>
|
||||
|
||||
<NavigationMenu.Content className="absolute data-[motion=from-start]:scale-150">
|
||||
<Card>asd</Card>
|
||||
</NavigationMenu.Content>
|
||||
</NavigationMenu.Item>
|
||||
</NavigationMenu.List>
|
||||
</NavigationMenu.Root>
|
||||
<RightButtonBar />
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
type TNavItem = {
|
||||
text: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
function NavItem(props: TNavItem) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<NavigationMenu.Item>
|
||||
<NavigationMenu.Link>
|
||||
<Button
|
||||
className="w-fit h-fit rounded-full m-0 p-0 pr-2 pl-2 mt-2 mb-2 duration-[50ms]"
|
||||
highContrast
|
||||
variant={location.pathname === props.to ? "solid" : "ghost"}
|
||||
onClick={() => navigate(props.to)}
|
||||
>
|
||||
<Text size={"3"}>{props.text}</Text>
|
||||
</Button>
|
||||
</NavigationMenu.Link>
|
||||
</NavigationMenu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import * as NavigationMenu from "@radix-ui/react-navigation-menu";
|
||||
import { Button, Heading, useThemeContext } from "@radix-ui/themes";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
export default function CustomNavigationMenu() {
|
||||
|
||||
const {t} = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<NavigationMenu.Root orientation="horizontal">
|
||||
<NavigationMenu.List className="flex items-center justify-start gap-8">
|
||||
<NavItem text={t("home")} to="/" />
|
||||
|
||||
<NavItem text={t("following")} to="/c" />
|
||||
</NavigationMenu.List>
|
||||
</NavigationMenu.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type TNavItem = {
|
||||
text: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
function NavItem(props: TNavItem) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const theme = useThemeContext();
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col">
|
||||
<NavigationMenu.Item>
|
||||
<NavigationMenu.Link>
|
||||
<div>
|
||||
<Button
|
||||
className="w-fit border-0 border-b-[0px] border-solid"
|
||||
highContrast
|
||||
variant="ghost"
|
||||
onClick={() => navigate(props.to)}
|
||||
>
|
||||
<Heading weight={"medium"} size={"3"}>
|
||||
{props.text}
|
||||
</Heading>
|
||||
</Button>
|
||||
</div>
|
||||
</NavigationMenu.Link>
|
||||
</NavigationMenu.Item>
|
||||
{location.pathname == props.to ? (
|
||||
<div
|
||||
className={`absolute animate-widthOut bottom-[-0.35rem]
|
||||
w-full h-[2px] z-[999] rounded-full`}
|
||||
style={{
|
||||
background: `var(--${theme.accentColor}-10)`,
|
||||
}}
|
||||
></div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { PlusIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Text } from "@radix-ui/themes";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export default function CreatePostButton() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Link to={"/create"}>
|
||||
<Button variant="ghost" className="h-full">
|
||||
<PlusIcon />
|
||||
<Text>{t("createPost")}</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import CreatePostButton from "./CreatePostButton/CreatePostButton";
|
||||
import UserButton from "./UserButton/UserButton";
|
||||
|
||||
|
||||
export default function RightButtonBar() {
|
||||
return (
|
||||
<div className='flex flex-row justify-end flex-1 gap-4'>
|
||||
<CreatePostButton />
|
||||
<UserButton />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
import {
|
||||
EnterIcon,
|
||||
ExitIcon,
|
||||
LaptopIcon,
|
||||
PersonIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { DropdownMenu, Flex, IconButton, Text } from "@radix-ui/themes";
|
||||
import { Icon } from "@radix-ui/themes/dist/esm/components/callout.js";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { userAtom } from "../../../../AtomStore/AtomStore";
|
||||
|
||||
export default function UserButton() {
|
||||
const user = useAtomValue(userAtom);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<IconButton>
|
||||
<PersonIcon />
|
||||
</IconButton>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content className="w-fit">
|
||||
<DropdownMenu.Item>
|
||||
<Link to={"/user/:user-id/profile"}>
|
||||
<Flex className="justify-between gap-2">
|
||||
<Icon>
|
||||
<PersonIcon />
|
||||
</Icon>
|
||||
|
||||
<Text>{t("profile")}</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item>
|
||||
<Link to={"/user/blogs"}>
|
||||
<Flex className="justify-between gap-2">
|
||||
<Icon>
|
||||
<LaptopIcon />
|
||||
</Icon>
|
||||
<Text>{t("yourBlogs")}</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Separator />
|
||||
|
||||
<DropdownMenu.Item color={user ? "red" : "green"}>
|
||||
{user ? (
|
||||
<Flex className="justify-between gap-2">
|
||||
<Icon>
|
||||
<ExitIcon />
|
||||
</Icon>
|
||||
<Text>{t("signOut")}</Text>
|
||||
</Flex>
|
||||
) : (
|
||||
<Link to={"/login"}>
|
||||
<Flex className="justify-between gap-2">
|
||||
<Icon>
|
||||
<EnterIcon />
|
||||
</Icon>
|
||||
<Text>{t("signIn")}</Text>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
enshi/src/Components/NavBar/SearchField/SearchField.tsx
Normal file
20
enshi/src/Components/NavBar/SearchField/SearchField.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { MagnifyingGlassIcon } from "@radix-ui/react-icons";
|
||||
import { TextField } from "@radix-ui/themes";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function SearchField() {
|
||||
const {t} = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="flex justify-center flex-1">
|
||||
<TextField.Root
|
||||
className="w-2/3 rounded-lg"
|
||||
placeholder={t("search")}
|
||||
>
|
||||
<TextField.Slot>
|
||||
<MagnifyingGlassIcon />
|
||||
</TextField.Slot>
|
||||
</TextField.Root>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
enshi/src/Components/ToastProvider/ToastProvider.tsx
Normal file
44
enshi/src/Components/ToastProvider/ToastProvider.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { Cross1Icon } from "@radix-ui/react-icons";
|
||||
import * as Toast from "@radix-ui/react-toast";
|
||||
import { Card, Text } from "@radix-ui/themes";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React from "react";
|
||||
import { toastAtom } from "../../AtomStore/AtomStore";
|
||||
|
||||
export default function ToastProvider(props: React.PropsWithChildren) {
|
||||
const toastsToRender = useAtomValue(toastAtom);
|
||||
|
||||
return (
|
||||
<Toast.Provider swipeDirection="right">
|
||||
{props.children}
|
||||
|
||||
{toastsToRender.map((toast) => {
|
||||
return (
|
||||
<Toast.Root
|
||||
key={toast.id}
|
||||
className="mt-2 mr-2 data-[state=open]:animate-slideFromRight data-[state=closed]:animate-fadeOut"
|
||||
open={toast.open}
|
||||
onOpenChange={toast.resetFunc}
|
||||
color={"red"}
|
||||
>
|
||||
<Card className="relative w-60">
|
||||
<Toast.Title>
|
||||
<Text size={"4"} weight={"bold"}>
|
||||
{toast.title}
|
||||
</Text>
|
||||
</Toast.Title>
|
||||
<Toast.Description className="overflow-hidden w-50 text-ellipsis">
|
||||
<Text className="text-pretty line-clamp-2">{toast.description}</Text>
|
||||
</Toast.Description>
|
||||
<Toast.Close className="absolute top-2 right-2">
|
||||
<Cross1Icon color="red" />
|
||||
</Toast.Close>
|
||||
</Card>
|
||||
</Toast.Root>
|
||||
);
|
||||
})}
|
||||
|
||||
<Toast.Viewport className="fixed right-0 bottom-2" />
|
||||
</Toast.Provider>
|
||||
);
|
||||
}
|
||||
33
enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx
Normal file
33
enshi/src/Components/UserNicknameLink/UserNicknameLink.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import { Skeleton, Text } from "@radix-ui/themes";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "react-router-dom";
|
||||
import { axiosLocalhost } from "../../api/axios/axios";
|
||||
|
||||
type TUserNicknameLink = {
|
||||
userId: string;
|
||||
};
|
||||
|
||||
export default function UserNicknameLink(props: TUserNicknameLink) {
|
||||
const { data, isPending } = useQuery({
|
||||
queryKey: [`userLink${props.userId}`],
|
||||
queryFn: async () => {
|
||||
const response = await axiosLocalhost.get(
|
||||
`/user/${props.userId || 0}`
|
||||
);
|
||||
return response.data as string;
|
||||
},
|
||||
});
|
||||
|
||||
if (isPending)
|
||||
return (
|
||||
<Skeleton>
|
||||
<Text>@Nickname</Text>
|
||||
</Skeleton>
|
||||
);
|
||||
|
||||
return (
|
||||
<Link to={`/users/${data}`}>
|
||||
<Text>@{data}</Text>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
22
enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx
Normal file
22
enshi/src/Pages/AuthPageWrapper/AuthPageWrapper.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { Container, Text } from "@radix-ui/themes";
|
||||
import { t } from "i18next";
|
||||
import { useAtomValue } from "jotai";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { userAtom } from "../../AtomStore/AtomStore";
|
||||
|
||||
export default function AuthPageWrapper(props: React.PropsWithChildren) {
|
||||
const user = useAtomValue(userAtom);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!user) {
|
||||
navigate("/login");
|
||||
return (
|
||||
<Container size={"4"} className="mt-4">
|
||||
<Text size={"7"}>{t("errors.unauthorized")}</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
return props.children;
|
||||
}
|
||||
13
enshi/src/Pages/BlogPage/BlogPage.tsx
Normal file
13
enshi/src/Pages/BlogPage/BlogPage.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Box } from '@radix-ui/themes'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
|
||||
export default function BlogPage() {
|
||||
const queryParams = useParams()
|
||||
|
||||
return (
|
||||
<Box>
|
||||
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
182
enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx
Normal file
182
enshi/src/Pages/LoginRegisterPage/LoginPage/LoginPage.tsx
Normal file
@ -0,0 +1,182 @@
|
||||
import * as Form from "@radix-ui/react-form";
|
||||
import { CrossCircledIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Card, Heading, Text, TextField } from "@radix-ui/themes";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { t } from "i18next";
|
||||
import { useAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { axiosLocalhost } from "../../../api/axios/axios";
|
||||
import { userAtom } from "../../../AtomStore/AtomStore";
|
||||
import UseCapsLock from "../../../hooks/useCapsLock";
|
||||
import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton";
|
||||
|
||||
type TLoginData = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export default function LoginPage() {
|
||||
const [userAtomValue, setUserAtom] = useAtom(userAtom);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { isCapsLockOn } = UseCapsLock();
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const logInMutation = useMutation({
|
||||
mutationFn: async (data: TLoginData) => {
|
||||
let response = await axiosLocalhost.post(
|
||||
"/login",
|
||||
JSON.stringify(data)
|
||||
);
|
||||
setUserAtom({
|
||||
username: response.data.username,
|
||||
isAdmin: false,
|
||||
id: response.data.id,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (error, _variables, _context) => {
|
||||
console.log(error);
|
||||
setIsError(true);
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
let isAdminFunc = async () => {
|
||||
let response = await axiosLocalhost.get("/admin/check");
|
||||
if (response.status === 200) {
|
||||
setUserAtom({
|
||||
username: userAtomValue?.username || "",
|
||||
isAdmin: true,
|
||||
id: userAtomValue?.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
isAdminFunc();
|
||||
|
||||
navigate("/");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card
|
||||
size={"2"}
|
||||
className="absolute w-1/4
|
||||
left-[50%] top-[50%]
|
||||
translate-x-[-50%] translate-y-[-50%]"
|
||||
>
|
||||
<Heading weight={"medium"} className="mb-4 text-center">
|
||||
{t("loginForm")}
|
||||
</Heading>
|
||||
<Form.Root
|
||||
className="flex flex-col gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
let formData = new FormData(
|
||||
document.querySelector("form") as HTMLFormElement
|
||||
);
|
||||
|
||||
let loginData: TLoginData = {
|
||||
password: (formData.get("password") as string) || "",
|
||||
username: (formData.get("username") as string) || "",
|
||||
};
|
||||
|
||||
logInMutation.mutate(loginData);
|
||||
}}
|
||||
>
|
||||
<Form.Field className="gap-0.5 grid" name="username">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<Form.Label>
|
||||
<Text size={"3"}>{t("username")}</Text>
|
||||
</Form.Label>
|
||||
<Form.Message match="valueMissing">
|
||||
<Text color="red">{t("errors.enterUsername")}</Text>
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<TextField.Root type="text" required>
|
||||
<Form.ValidityState>
|
||||
{(validity) => (
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
color="red"
|
||||
className={
|
||||
validity
|
||||
? validity.valid
|
||||
? "hidden"
|
||||
: "mr-0.5"
|
||||
: "hidden"
|
||||
}
|
||||
>
|
||||
<CrossCircledIcon />
|
||||
</TextField.Slot>
|
||||
)}
|
||||
</Form.ValidityState>
|
||||
</TextField.Root>
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field className="mb-2.5 gap-0.5 grid" name="password">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<Form.Label>
|
||||
<Text size={"3"}>{t("password")}</Text>
|
||||
</Form.Label>
|
||||
<Form.Message match="valueMissing">
|
||||
<Text color="red">{t("errors.enterPassword")}</Text>
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<TextField.Root
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
autoComplete="on"
|
||||
>
|
||||
<Form.ValidityState>
|
||||
{(validity) => (
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
color={
|
||||
validity
|
||||
? validity.valid
|
||||
? undefined
|
||||
: "red"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ShowPasswordButton
|
||||
isShown={showPassword}
|
||||
setIsShown={setShowPassword}
|
||||
/>
|
||||
</TextField.Slot>
|
||||
)}
|
||||
</Form.ValidityState>
|
||||
</TextField.Root>
|
||||
</Form.Control>
|
||||
<Text size={"1"} hidden={!isCapsLockOn}>
|
||||
{t("capsLogWarning")}
|
||||
</Text>
|
||||
</Form.Field>
|
||||
|
||||
<Text color="red" hidden={!isError}>
|
||||
{t("errors.invalidLoginData")}
|
||||
</Text>
|
||||
|
||||
<Form.Submit className="flex justify-center" asChild>
|
||||
<Button type="submit" className="w-full p-4 m-auto">
|
||||
<Text size={"3"}>{t("submit")}</Text>
|
||||
</Button>
|
||||
</Form.Submit>
|
||||
|
||||
<Text size={"1"} color="gray" className="block w-full text-center">
|
||||
{t("suggestRegister")}{" "}
|
||||
<Link to="/register">
|
||||
<Text className="underline" weight={"bold"}>{t("register")}</Text>
|
||||
</Link>{" "}
|
||||
{t("now")}
|
||||
</Text>
|
||||
</Form.Root>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
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 SubmitChangesButton from "./SubmitChangesButton/SubmitChangesButton";
|
||||
|
||||
export default function PostRedactor() {
|
||||
const [contentValue, setContentValue] = useState("");
|
||||
const [titleValue, setTitleValue] = useState("");
|
||||
|
||||
const queryParams = useParams();
|
||||
|
||||
const { isPending } = useQuery({
|
||||
queryKey: ["changePostKey", queryParams.postId],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await axiosLocalhost.get(
|
||||
`/posts/${queryParams.postId}`
|
||||
);
|
||||
|
||||
setTitleValue(response.data["title"]);
|
||||
setContentValue(response.data["content"]);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
|
||||
return error;
|
||||
}
|
||||
},
|
||||
gcTime: 0,
|
||||
refetchOnMount: true
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box className="flex flex-col flex-1">
|
||||
<Flex gap={"4"} direction={"column"} className="flex-[1]">
|
||||
<Container className="flex-[1]">
|
||||
<input
|
||||
disabled={isPending}
|
||||
placeholder={"Post title"}
|
||||
className="mb-2 border-0 border-b-[1px]
|
||||
outline-none w-full border-b-gray-400
|
||||
text-[60px] pl-4 pr-4 font-times"
|
||||
onChange={(e) => {
|
||||
setTitleValue(e.target.value);
|
||||
}}
|
||||
value={titleValue}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
<Container className="overflow-y-auto flex-grow-[100]">
|
||||
{isPending ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Editor
|
||||
defaultValue={contentValue}
|
||||
onChange={setContentValue}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
<Box className="flex justify-center flex-[1] mb-4">
|
||||
<SubmitChangesButton
|
||||
contentValue={contentValue}
|
||||
titleValue={titleValue}
|
||||
className="text-2xl rounded-full w-52" />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { Button } from "@radix-ui/themes";
|
||||
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";
|
||||
|
||||
type TSubmitChangesButton = {
|
||||
className: string;
|
||||
titleValue: string;
|
||||
contentValue: string;
|
||||
};
|
||||
|
||||
export default function SubmitChangesButton(props: TSubmitChangesButton) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
const createToast = useToast();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const queryParams = useParams();
|
||||
|
||||
const postMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!props.titleValue) throw new Error("no title provided");
|
||||
if (!props.contentValue || props.contentValue === "<p><br></p>")
|
||||
throw new Error("no content provided");
|
||||
|
||||
axiosLocalhost.put(`/posts/${queryParams["postId"]}`, {
|
||||
title: props.titleValue,
|
||||
content: props.contentValue,
|
||||
});
|
||||
},
|
||||
onMutate: () => {
|
||||
setIsDisabled(true);
|
||||
},
|
||||
onError: () => {
|
||||
setIsDisabled(false);
|
||||
},
|
||||
onSuccess: () => {
|
||||
createToast({title: "Post has been changed!"})
|
||||
navigate("/");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
postMutation.mutate();
|
||||
}}
|
||||
className={props.className}
|
||||
variant="soft"
|
||||
size={"4"}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{t("updatePost")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
265
enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx
Normal file
265
enshi/src/Pages/LoginRegisterPage/RegisterPage/RegisterPage.tsx
Normal file
@ -0,0 +1,265 @@
|
||||
import * as Form from "@radix-ui/react-form";
|
||||
import { CrossCircledIcon } from "@radix-ui/react-icons";
|
||||
import { Button, Card, Heading, Text, TextField } from "@radix-ui/themes";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { axiosLocalhost } from "../../../api/axios/axios";
|
||||
import { userAtom } from "../../../AtomStore/AtomStore";
|
||||
import UseCapsLock from "../../../hooks/useCapsLock";
|
||||
import ShowPasswordButton from "../ShowPasswordButton/ShowPasswordButton";
|
||||
|
||||
type TRegisterData = {
|
||||
username: string;
|
||||
password: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export default function RegisterPage() {
|
||||
const setUserAtom = useSetAtom(userAtom)
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfPassword, setShowConfPassword] = useState(false);
|
||||
const { isCapsLockOn } = UseCapsLock();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isError, setIsError] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async (data: TRegisterData) => {
|
||||
let response = await axiosLocalhost.post("/users", JSON.stringify(data));
|
||||
setUserAtom({
|
||||
username: response.data.username,
|
||||
isAdmin: false,
|
||||
id: response.data.id,
|
||||
})
|
||||
},
|
||||
|
||||
onError: (error, _variables, _context) => {
|
||||
console.log(error);
|
||||
setIsError(true);
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
navigate("/");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Card
|
||||
size={"2"}
|
||||
className="absolute w-[25rem] min-w-[20rem]
|
||||
left-[50%] top-[50%]
|
||||
translate-x-[-50%] translate-y-[-50%]"
|
||||
>
|
||||
<Heading weight={"medium"} className="mb-4 text-center">
|
||||
{t("registerForm")}
|
||||
</Heading>
|
||||
<Form.Root
|
||||
className="flex flex-col gap-2"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
let formData = new FormData(
|
||||
document.querySelector("form") as HTMLFormElement
|
||||
);
|
||||
|
||||
let registerData: TRegisterData = {
|
||||
password: (formData.get("password") as string) || "",
|
||||
username: (formData.get("username") as string) || "",
|
||||
email: (formData.get("email") as string) || "",
|
||||
};
|
||||
|
||||
registerMutation.mutate(registerData);
|
||||
}}
|
||||
>
|
||||
<Form.Field className="gap-0.5 grid" name="username">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<Form.Label>
|
||||
<Text size={"4"}>{t("username")}</Text>
|
||||
</Form.Label>
|
||||
<Form.Message match="valueMissing">
|
||||
<Text color="red">{t("errors.enterUsername")}</Text>
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<TextField.Root type="text" required>
|
||||
<Form.ValidityState>
|
||||
{(validity) => (
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
color="red"
|
||||
className={
|
||||
validity
|
||||
? validity.valid
|
||||
? "hidden"
|
||||
: "mr-0.5"
|
||||
: "hidden"
|
||||
}
|
||||
>
|
||||
<CrossCircledIcon />
|
||||
</TextField.Slot>
|
||||
)}
|
||||
</Form.ValidityState>
|
||||
</TextField.Root>
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field className="gap-0.5 grid" name="email">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<Form.Label>
|
||||
<Text size={"4"}>{t("email")}</Text>
|
||||
</Form.Label>
|
||||
<Form.Message match="valueMissing">
|
||||
<Text color="red">{t("errors.enterEmail")}</Text>
|
||||
</Form.Message>
|
||||
<Form.Message match="typeMismatch">
|
||||
<Text color="red">{t("errors.invalidEmail")}</Text>
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<TextField.Root type="email" required>
|
||||
<Form.ValidityState>
|
||||
{(validity) => (
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
color="red"
|
||||
className={
|
||||
validity
|
||||
? validity.valid
|
||||
? "hidden"
|
||||
: "mr-0.5"
|
||||
: "hidden"
|
||||
}
|
||||
>
|
||||
<CrossCircledIcon />
|
||||
</TextField.Slot>
|
||||
)}
|
||||
</Form.ValidityState>
|
||||
</TextField.Root>
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field className="gap-0.5 grid" name="password">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<Form.Label>
|
||||
<Text size={"4"}>{t("password")}</Text>
|
||||
</Form.Label>
|
||||
<Form.Message match="valueMissing">
|
||||
<Text color="red">{t("errors.enterPassword")}</Text>
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<TextField.Root
|
||||
type={showPassword ? "text" : "password"}
|
||||
required
|
||||
>
|
||||
<Form.ValidityState>
|
||||
{(validity) => (
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
color={
|
||||
validity
|
||||
? validity.valid
|
||||
? undefined
|
||||
: "red"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ShowPasswordButton
|
||||
isShown={showPassword}
|
||||
setIsShown={setShowPassword}
|
||||
/>
|
||||
</TextField.Slot>
|
||||
)}
|
||||
</Form.ValidityState>
|
||||
</TextField.Root>
|
||||
</Form.Control>
|
||||
<Text size={"1"} hidden={!isCapsLockOn}>
|
||||
{t("capsLogWarning")}
|
||||
</Text>
|
||||
</Form.Field>
|
||||
|
||||
<Form.Field
|
||||
className="gap-0.5 grid"
|
||||
name="conf-password"
|
||||
>
|
||||
<div className="flex items-baseline justify-between">
|
||||
<Form.Label>
|
||||
<Text size={"4"}>{t("confirmPassword")}</Text>
|
||||
</Form.Label>
|
||||
<Form.Message match="valueMissing">
|
||||
<Text color="red">{t("errors.enterPassword")}</Text>
|
||||
</Form.Message>
|
||||
<Form.Message
|
||||
match={(value, formData) =>
|
||||
value !== formData.get("password")
|
||||
}
|
||||
>
|
||||
<Text color="red">
|
||||
{t("errors.passwordsMismatch")}
|
||||
</Text>
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<TextField.Root
|
||||
type={showConfPassword ? "text" : "password"}
|
||||
required
|
||||
>
|
||||
<Form.ValidityState>
|
||||
{(validity) => (
|
||||
<TextField.Slot
|
||||
side="right"
|
||||
color={
|
||||
validity
|
||||
? validity.valid
|
||||
? undefined
|
||||
: "red"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<ShowPasswordButton
|
||||
isShown={showConfPassword}
|
||||
setIsShown={setShowConfPassword}
|
||||
/>
|
||||
</TextField.Slot>
|
||||
)}
|
||||
</Form.ValidityState>
|
||||
</TextField.Root>
|
||||
</Form.Control>
|
||||
<Text size={"1"} hidden={!isCapsLockOn}>
|
||||
{t("capsLogWarning")}
|
||||
</Text>
|
||||
</Form.Field>
|
||||
|
||||
<Text color="red" hidden={!isError}>
|
||||
{t("errors.invalidRegisterData")}
|
||||
</Text>
|
||||
|
||||
<Form.Submit className="flex justify-center mt-2" asChild>
|
||||
<Button type="submit" className="w-full m-auto">
|
||||
<Text size={"3"}>{t("submit")}</Text>
|
||||
</Button>
|
||||
</Form.Submit>
|
||||
|
||||
<Text size={"1"} color="gray" className="block w-full text-center">
|
||||
{t("alreadyRegistered")}{" "}
|
||||
<Link to="/login">
|
||||
<Text className="underline" weight={"bold"}>{t("logIn")}</Text>
|
||||
</Link>{" "}
|
||||
{t("now")}
|
||||
</Text>
|
||||
|
||||
<Text size={"1"} color="gray" className="block w-full text-center">
|
||||
{t("byPressingTheButton")}{" "}
|
||||
<Link to="/register">
|
||||
<Text className="underline" weight={"bold"}>{t("termsOfService")}</Text>.
|
||||
</Link>
|
||||
</Text>
|
||||
</Form.Root>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import { EyeClosedIcon, EyeOpenIcon } from "@radix-ui/react-icons";
|
||||
import { IconButton, Tooltip } from "@radix-ui/themes";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
type TShowPasswordButton = {
|
||||
isShown: boolean;
|
||||
setIsShown: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export default function ShowPasswordButton({ isShown, setIsShown }: TShowPasswordButton) {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip content="Show password">
|
||||
{isShown ? (
|
||||
<IconButton
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsShown(!isShown);
|
||||
}}
|
||||
size={"1"}
|
||||
className="rounded-full"
|
||||
variant="soft"
|
||||
>
|
||||
<EyeClosedIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton
|
||||
type="button"
|
||||
onClick={() => setIsShown(!isShown)}
|
||||
size={"1"}
|
||||
className="rounded-full"
|
||||
variant="soft"
|
||||
>
|
||||
<EyeOpenIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import NavBar from '../../Components/NavBar/NavBar'
|
||||
import { axiosLocalhost } from '../../api/axios/axios'
|
||||
|
||||
export default function MainPage() {
|
||||
return (
|
||||
<>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
<button
|
||||
onClick={
|
||||
async () => {
|
||||
let d = await axiosLocalhost.get("getCookie")
|
||||
console.log(d.data);
|
||||
|
||||
}
|
||||
}>
|
||||
qwpofjqwifhqwuif
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
43
enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx
Normal file
43
enshi/src/Pages/PostCreatorPage/PostCreatorPage.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Box, Container, Flex } from "@radix-ui/themes";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import {
|
||||
postCreationAtom,
|
||||
postCreationTitleAtom
|
||||
} from "../../AtomStore/AtomStore";
|
||||
import Editor from "../../Components/Editor/Editor";
|
||||
import SubmitPostButton from "./SubmitPostButton/SubmitPostButton";
|
||||
|
||||
export default function PostCreatorPage() {
|
||||
const [titleValue, setTitleValue] = useAtom(postCreationTitleAtom);
|
||||
const setContentValue = useSetAtom(postCreationAtom);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Box className="flex flex-col flex-1">
|
||||
<Flex gap={"4"} direction={"column"} className="flex-[1]">
|
||||
<Container className="flex-[1]">
|
||||
<input
|
||||
placeholder={"Post title"}
|
||||
className="mb-2 border-0 border-b-[1px]
|
||||
outline-none w-full border-b-gray-400
|
||||
text-[60px] pl-4 pr-4 font-times"
|
||||
onChange={(e) => {
|
||||
setTitleValue(e.target.value);
|
||||
}}
|
||||
value={titleValue}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
<Container className="overflow-y-auto flex-grow-[100]">
|
||||
<Editor onChange={setContentValue} />
|
||||
</Container>
|
||||
|
||||
<Box className="flex justify-center flex-[1] mb-4">
|
||||
<SubmitPostButton className="text-2xl rounded-full w-52" />
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import { Button } from "@radix-ui/themes";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { axiosLocalhost } from "../../../api/axios/axios";
|
||||
import {
|
||||
postCreationAtom,
|
||||
postCreationTitleAtom,
|
||||
} from "../../../AtomStore/AtomStore";
|
||||
|
||||
type TSubmitPostButton = {
|
||||
className: string;
|
||||
};
|
||||
|
||||
export default function SubmitPostButton(props: TSubmitPostButton) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isDisabled, setIsDisabled] = useState(false);
|
||||
|
||||
const [contentValue, setContentValue] = useAtom(postCreationAtom);
|
||||
const [titleValue, setTitleValue] = useAtom(postCreationTitleAtom);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const postMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!titleValue) throw new Error("no title provided");
|
||||
if (!contentValue || contentValue === "<p><br></p>")
|
||||
throw new Error("no content provided");
|
||||
|
||||
axiosLocalhost.post("/posts", {
|
||||
title: titleValue,
|
||||
content: contentValue,
|
||||
});
|
||||
},
|
||||
onMutate: () => {
|
||||
setIsDisabled(true);
|
||||
},
|
||||
onError: () => {
|
||||
setIsDisabled(false);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setContentValue("");
|
||||
setTitleValue("");
|
||||
navigate("/");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
postMutation.mutate();
|
||||
}}
|
||||
className={props.className}
|
||||
variant="soft"
|
||||
size={"4"}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{t("submit")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
30
enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx
Normal file
30
enshi/src/Pages/RandomPostsPage/PostCard/PostCard.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import { ImageIcon } from "@radix-ui/react-icons";
|
||||
import { Box, Card, Heading } from "@radix-ui/themes";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { GetRandomPostsRow } from "../../../@types/PostTypes";
|
||||
|
||||
type TPostCard = {
|
||||
post: GetRandomPostsRow;
|
||||
};
|
||||
|
||||
export default function PostCard({ post }: TPostCard) {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const clickHandler = () => {
|
||||
navigate(`/posts/${post.post_id.toString()}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-32 mb-4" onClick={clickHandler}>
|
||||
<Box className="flex size-full">
|
||||
<Box>
|
||||
<ImageIcon className="w-full h-full" />
|
||||
</Box>
|
||||
|
||||
<Box className="px-4 pt-2">
|
||||
<Heading>{post.title}</Heading>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
67
enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx
Normal file
67
enshi/src/Pages/RandomPostsPage/RandomPostsPage.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
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 { useTranslation } from "react-i18next";
|
||||
import { GetRandomPostsRow } from "../../@types/PostTypes";
|
||||
import { axiosLocalhost } from "../../api/axios/axios";
|
||||
import PostCard from "./PostCard/PostCard";
|
||||
|
||||
const LIMIT = 10;
|
||||
|
||||
export default function RandomPostsPage() {
|
||||
const {t} = useTranslation()
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ["random_posts_key"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await axiosLocalhost.get(
|
||||
`/posts/random?limit=${LIMIT}`
|
||||
);
|
||||
|
||||
return response.data as GetRandomPostsRow[];
|
||||
} catch (error) {
|
||||
console.log(`Something went wrong`);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex direction={"column"} className="mx-auto">
|
||||
<Heading size={"9"} weight={"regular"} className="text-center">
|
||||
{t("discover")}
|
||||
</Heading>
|
||||
|
||||
<Separator size={"4"} className="my-8" />
|
||||
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
import { Box, Skeleton } from "@radix-ui/themes";
|
||||
|
||||
export default function SkeletonBoxes() {
|
||||
return (
|
||||
<>
|
||||
<Skeleton>
|
||||
<Box className="w-full h-20 mb-2 rounded-lg"></Box>
|
||||
</Skeleton>
|
||||
<Skeleton>
|
||||
<Box className="w-full h-20 mb-2 rounded-lg"></Box>
|
||||
</Skeleton>
|
||||
<Skeleton>
|
||||
<Box className="w-full h-20 mb-2 rounded-lg"></Box>
|
||||
</Skeleton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
126
enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx
Normal file
126
enshi/src/Pages/UserBlogsPage/UserBlogsPage.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon, PlusIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Container,
|
||||
Flex,
|
||||
Separator,
|
||||
Text,
|
||||
} from "@radix-ui/themes";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { axiosLocalhost } from "../../api/axios/axios";
|
||||
import BlogBox from "../../Components/BlogBox/BlogBox";
|
||||
import { JSONWithInt64 } from "../../utils/idnex";
|
||||
import SkeletonBoxes from "./SkeletonBoxes/SkeletonBoxes";
|
||||
|
||||
export default function UserBlogsPage() {
|
||||
const { data, isPending, isFetching } = useQuery({
|
||||
queryKey: ["userBlogs"],
|
||||
queryFn: async () => {
|
||||
const response = await axiosLocalhost.get("/user/blogs", {
|
||||
transformResponse: [(data) => data],
|
||||
});
|
||||
|
||||
let temp = JSONWithInt64(response.data);
|
||||
|
||||
return temp as any[];
|
||||
},
|
||||
});
|
||||
|
||||
if (isPending)
|
||||
return (
|
||||
<Container size={"1"}>
|
||||
<SkeletonBoxes />
|
||||
</Container>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="size-full">
|
||||
<Container size={"1"}>
|
||||
<Flex direction={"column"} gap={"2"}>
|
||||
<Text size={"9"} className="text-center">
|
||||
Your blogs
|
||||
</Text>
|
||||
|
||||
<Separator size={"4"} className="my-2" />
|
||||
|
||||
{data
|
||||
? data?.map((blog: any, b) => {
|
||||
return (
|
||||
<>
|
||||
<BlogBox
|
||||
key={b}
|
||||
title={blog.title}
|
||||
blogId={blog.blog_id}
|
||||
userId={blog.user_id}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger asChild>
|
||||
<Button onClick={() => {}}>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-blackA6 data-[state=open]:animate-overlayShow" />
|
||||
<Dialog.Content className="fixed left-1/2 top-1/2 max-h-[85vh] w-[90vw] max-w-[450px] -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none data-[state=open]:animate-contentShow">
|
||||
<Dialog.Title className="m-0 text-[17px] font-medium text-mauve12">
|
||||
Create blog
|
||||
</Dialog.Title>
|
||||
<Dialog.Description className="mb-5 mt-2.5 text-[15px] leading-normal text-mauve11">
|
||||
Create your new blog.
|
||||
</Dialog.Description>
|
||||
<fieldset className="mb-[15px] flex items-center gap-5">
|
||||
<label
|
||||
className="w-[90px] text-right text-[15px] text-violet11"
|
||||
htmlFor="title"
|
||||
>
|
||||
Blog title
|
||||
</label>
|
||||
<input
|
||||
className="inline-flex h-[35px] w-full flex-1 items-center justify-center rounded px-2.5 text-[15px] leading-none text-violet11 shadow-[0_0_0_1px] shadow-violet7 outline-none focus:shadow-[0_0_0_2px] focus:shadow-violet8"
|
||||
id="title"
|
||||
defaultValue="My blog"
|
||||
/>
|
||||
</fieldset>
|
||||
<fieldset className="mb-[15px] flex items-center gap-5">
|
||||
<label
|
||||
className="w-[90px] text-right text-[15px] text-violet11"
|
||||
htmlFor="Description"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
className="pt-2 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded px-2.5 text-[15px] leading-none text-violet11 shadow-[0_0_0_1px] shadow-violet7 outline-none focus:shadow-[0_0_0_2px] focus:shadow-violet8"
|
||||
id="Description"
|
||||
placeholder="Your description..."
|
||||
/>
|
||||
</fieldset>
|
||||
<div className="mt-[25px] flex justify-end">
|
||||
<Dialog.Close asChild>
|
||||
<Button>
|
||||
Create blog
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</div>
|
||||
<Dialog.Close asChild>
|
||||
<button
|
||||
className="absolute right-2.5 top-2.5 inline-flex size-[25px] appearance-none items-center justify-center rounded-full text-violet11 hover:bg-violet4 focus:shadow-[0_0_0_2px] focus:shadow-violet7 focus:outline-none"
|
||||
aria-label="Close"
|
||||
>
|
||||
<Cross2Icon />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</Flex>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -1,8 +1,12 @@
|
||||
import axios from "axios";
|
||||
|
||||
const environment = import.meta.env.VITE_ENV || 'development';
|
||||
// const environment = "docker"
|
||||
const baseURL = environment === "docker" ? "https://localhost/api/v1/" : "http://127.0.0.1:9876/";
|
||||
|
||||
export const axiosLocalhost = axios.create(
|
||||
{
|
||||
baseURL: `http://localhost:9876/`,
|
||||
baseURL: baseURL,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
|
||||
|
||||
12
enshi/src/constants/textForSkeleton.ts
Normal file
12
enshi/src/constants/textForSkeleton.ts
Normal file
@ -0,0 +1,12 @@
|
||||
export const pText = `The goal of typography is to relate font size, line
|
||||
height, and line width in a proportional way that
|
||||
maximizes beauty and makes reading easier and more
|
||||
pleasant. The question is: What proportion(s) will give
|
||||
us the best results? The golden ratio is often observed
|
||||
in nature where beauty and utility intersect; perhaps we
|
||||
can use this “divine” proportion to enhance these
|
||||
attributes in our typography.`;
|
||||
|
||||
export const headerLong = `THUS SHU SHU HDFQIUWKHFQWHF KJQWHqwfiqfquwdhqwjdk`;
|
||||
|
||||
export const headerShort = `THUS SHU SHU HDFQIUWKHFQWHF`;
|
||||
27
enshi/src/hooks/useCapsLock.tsx
Normal file
27
enshi/src/hooks/useCapsLock.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function UseCapsLock() {
|
||||
|
||||
const [isCapsLockOn, setIsCapsLockOn] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const f = (e: KeyboardEvent) => {
|
||||
if (e.getModifierState("CapsLock")) {
|
||||
setIsCapsLockOn(true);
|
||||
} else {
|
||||
setIsCapsLockOn(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", f);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", f);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return {
|
||||
isCapsLockOn
|
||||
}
|
||||
}
|
||||
7
enshi/src/hooks/useToast.tsx
Normal file
7
enshi/src/hooks/useToast.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import { useSetAtom } from "jotai";
|
||||
import { setToastAtom } from "../AtomStore/AtomStore";
|
||||
|
||||
export default function useToast() {
|
||||
const createToast = useSetAtom(setToastAtom);
|
||||
return createToast;
|
||||
}
|
||||
@ -1,10 +1,19 @@
|
||||
@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');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.center-of-parent {
|
||||
@apply absolute top-1/2 left-1/2 translate-x-[-50%] translate-y-[-50%];
|
||||
}
|
||||
}
|
||||
|
||||
.radix-themes {
|
||||
--default-font-family:
|
||||
--heading-font-family:
|
||||
--default-font-family: "Times New Roman"; ;
|
||||
|
||||
--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 */
|
||||
|
||||
74
enshi/src/layout/MainPage/MainPage.tsx
Normal file
74
enshi/src/layout/MainPage/MainPage.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import { Box, Flex, Spinner } from "@radix-ui/themes";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { axiosLocalhost } from "../../api/axios/axios";
|
||||
import { userAtom } from "../../AtomStore/AtomStore";
|
||||
import NavBar from "../../Components/NavBar/NavBar";
|
||||
|
||||
const REFETCH_INTERVAL_IN_MINUTES = 5;
|
||||
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);
|
||||
|
||||
const { isPending } = useQuery({
|
||||
queryKey: ["authKey"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const response = await axiosLocalhost.get("/auth/check");
|
||||
|
||||
setUserData({
|
||||
isAdmin: response.data["is_admin"],
|
||||
username: response.data["username"],
|
||||
id: response.data["id"],
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
setUserData(undefined);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
refetchInterval:
|
||||
REFETCH_INTERVAL_IN_MINUTES * SECONDS_IN_MINUTE * MILLS_IN_SECOND,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
gcTime: 10,
|
||||
|
||||
retry: 3,
|
||||
retryDelay: (attempt) =>
|
||||
attempt * RETRY_INTERVAL_IN_SECONDS * MILLS_IN_SECOND,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPending ? (
|
||||
<div
|
||||
className="absolute top-1/2 left-1/2
|
||||
translate-x-[-50%] translate-y-[-50%]"
|
||||
>
|
||||
<Spinner size={"3"} />
|
||||
</div>
|
||||
) : (
|
||||
<Flex
|
||||
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-">
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,53 @@
|
||||
const en = {
|
||||
hello: "hello!"
|
||||
}
|
||||
hello: "hello!",
|
||||
search: "Search...",
|
||||
username: "Username",
|
||||
email: "Email",
|
||||
password: "Password",
|
||||
confirmPassword: "Confirm password",
|
||||
submit: "Submit",
|
||||
|
||||
createPost: "Write post",
|
||||
|
||||
profile: "Profile",
|
||||
yourBlogs: "Your blogs",
|
||||
|
||||
signIn: "Log in",
|
||||
signOut: "Sign out",
|
||||
|
||||
capsLogWarning: "CapsLock is on",
|
||||
|
||||
registerForm: "Register",
|
||||
loginForm: "Log in",
|
||||
|
||||
alreadyRegistered: "Already registered?",
|
||||
|
||||
suggestRegister: "Don't have an account?",
|
||||
register: "Register",
|
||||
now: "now!",
|
||||
|
||||
logIn: "Log in",
|
||||
|
||||
updatePost: "Update",
|
||||
|
||||
byPressingTheButton: "By pressing the submit button you agree with our",
|
||||
termsOfService: "Terms Of Service",
|
||||
|
||||
discover: "Discover something new...",
|
||||
|
||||
home: "Home",
|
||||
following: "Following",
|
||||
|
||||
errors: {
|
||||
enterUsername: "Please enter your username",
|
||||
enterEmail: "Please enter your email",
|
||||
invalidEmail: "Please enter correct email",
|
||||
enterPassword: "Please enter your password",
|
||||
passwordsMismatch: "Passwords must be the same",
|
||||
invalidLoginData: "Invalid username or password",
|
||||
invalidRegisterData: "Invalid register data",
|
||||
unauthorized: "You need to be authorized to do that",
|
||||
},
|
||||
};
|
||||
|
||||
export default en;
|
||||
@ -1,5 +1,56 @@
|
||||
|
||||
const ru = {
|
||||
hello: "Привет!"
|
||||
}
|
||||
hello: "Привет!",
|
||||
search: "Поиск...",
|
||||
username: "Имя пользователя",
|
||||
email: "Электронная почта",
|
||||
password: "Пароль",
|
||||
confirmPassword: "Подтвердите пароль",
|
||||
submit: "Подтвердить",
|
||||
|
||||
createPost: "Написать пост",
|
||||
|
||||
profile: "Профиль",
|
||||
yourBlogs: "Ваши блоги",
|
||||
|
||||
signIn: "Войти",
|
||||
signOut: "Выйти",
|
||||
|
||||
capsLogWarning: "Включён CapsLock",
|
||||
|
||||
registerForm: "Регистрация",
|
||||
loginForm: "Вход",
|
||||
|
||||
alreadyRegistered: "Уже есть аккаунт?",
|
||||
|
||||
suggestRegister: "Не зарегистрированы?",
|
||||
register: "Создайте аккаунт",
|
||||
now: "сейчас!",
|
||||
|
||||
logIn: "Войдите",
|
||||
|
||||
byPressingTheButton: "Нажимая `Подтвердить`, вы соглашаетесь с нашими",
|
||||
termsOfService: "Условиями предоставления услуг.",
|
||||
|
||||
updatePost: "Изменить",
|
||||
|
||||
discover: "Найдите что-то новое",
|
||||
|
||||
home: "Главная",
|
||||
following: "Отслеживаемые",
|
||||
|
||||
|
||||
errors: {
|
||||
enterUsername: "Это обязательное поле",
|
||||
enterEmail: "Это обязательное поле",
|
||||
invalidEmail: "Некорректный адрес электронной почты",
|
||||
enterPassword: "Пожалуйста, введите пароль",
|
||||
passwordsMismatch: "Пароли должны быть одинаковыми",
|
||||
invalidLoginData: "Неверное имя пользователя или пароль",
|
||||
invalidRegisterData:
|
||||
"Пользователь с таким адресом электронной почты или именем пользователя уже существует",
|
||||
unauthorized: "Вы должны быть авторизованы, чтобы сделать это",
|
||||
},
|
||||
};
|
||||
|
||||
export default ru;
|
||||
@ -1,7 +1,20 @@
|
||||
import { createRoutesFromElements, Route, useRouteError } from "react-router-dom"
|
||||
import MainPage from "../Pages/MainPage/MainPage"
|
||||
import { Text } from "@radix-ui/themes";
|
||||
|
||||
import {
|
||||
createRoutesFromElements,
|
||||
Outlet,
|
||||
Route,
|
||||
useRouteError,
|
||||
} from "react-router-dom";
|
||||
import ArticleViewer from "../Components/ArticleViewer/ArticleViewer";
|
||||
import MainPage from "../layout/MainPage/MainPage";
|
||||
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 RandomPostsPage from "../Pages/RandomPostsPage/RandomPostsPage";
|
||||
import UserBlogsPage from "../Pages/UserBlogsPage/UserBlogsPage";
|
||||
|
||||
function ErrorBoundary() {
|
||||
let error = useRouteError();
|
||||
@ -12,16 +25,54 @@ function ErrorBoundary() {
|
||||
|
||||
export const routes = createRoutesFromElements(
|
||||
<>
|
||||
<Route path="/" errorElement={<ErrorBoundary />} element={<MainPage />}>
|
||||
<Route index element={<RandomPostsPage />} />
|
||||
|
||||
<Route
|
||||
path="/"
|
||||
errorElement={<ErrorBoundary />}
|
||||
element={<MainPage />}
|
||||
>
|
||||
<Route index element={<Text>Cringer path</Text>} />
|
||||
path="a?/c"
|
||||
element={
|
||||
<Text weight={"regular"}>
|
||||
This page is yet to be created
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/a?/c"
|
||||
element={<Text>Cringer path, but this a</Text>}
|
||||
></Route>
|
||||
path="create"
|
||||
element={
|
||||
<AuthPageWrapper>
|
||||
<PostCreatorPage />
|
||||
</AuthPageWrapper>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="blogs/:blogId" element={<BlogPage />} />
|
||||
|
||||
<Route path="user" element={<Outlet />}>
|
||||
<Route
|
||||
path="blogs"
|
||||
element={
|
||||
<AuthPageWrapper>
|
||||
<UserBlogsPage />
|
||||
</AuthPageWrapper>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="posts/:postId" element={<ArticleViewer />} />
|
||||
<Route path="posts/change/:postId" element={<PostRedactor />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path="/login"
|
||||
errorElement={<ErrorBoundary />}
|
||||
element={<LoginPage />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/register"
|
||||
errorElement={<ErrorBoundary />}
|
||||
element={<RegisterPage />}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
||||
19
enshi/src/utils/idnex.ts
Normal file
19
enshi/src/utils/idnex.ts
Normal file
@ -0,0 +1,19 @@
|
||||
const isBigNumber = (num: any) => !Number.isSafeInteger(+num);
|
||||
|
||||
const enquoteBigNumber = (jsonString: any, bigNumChecker: any) =>
|
||||
jsonString.replaceAll(
|
||||
/([:\s\[,]*)(\d+)([\s,\]]*)/g,
|
||||
(matchingSubstr: any, prefix: any, bigNum: any, suffix: any) =>
|
||||
bigNumChecker(bigNum)
|
||||
? `${prefix}"${bigNum}"${suffix}`
|
||||
: matchingSubstr
|
||||
);
|
||||
|
||||
const parseWithBigInt = (jsonString: any, bigNumChecker: any) =>
|
||||
JSON.parse(enquoteBigNumber(jsonString, bigNumChecker), (_key, value) =>
|
||||
!isNaN(value) && bigNumChecker(value) ? BigInt(value).toString() : value
|
||||
);
|
||||
|
||||
export const JSONWithInt64 = (jsonString: any) => {
|
||||
return parseWithBigInt(jsonString, isBigNumber);
|
||||
};
|
||||
@ -1,21 +1,53 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
"primary-color": "var(--primary-color)",
|
||||
"secondary-color": "var(--secondary-color)",
|
||||
},
|
||||
fontFamily: {
|
||||
'times': "Times New Roman"
|
||||
},
|
||||
animation: {
|
||||
'appear': 'appear 0.25s'
|
||||
appear: "appear 0.25s",
|
||||
widthOut: "widthOut cubic-bezier(0.4, 0, 0.6, 1) 0.4s",
|
||||
slideFromRight: "slideFromRight cubic-bezier(0.4, 0, 0.6, 1) 0.2s",
|
||||
fadeOut: "fadeOut 0.2s ease-in",
|
||||
},
|
||||
keyframes: {
|
||||
fadeOut: {
|
||||
from: {
|
||||
opacity: "1",
|
||||
},
|
||||
to: {
|
||||
opacity: "0",
|
||||
}
|
||||
},
|
||||
slideFromRight: {
|
||||
"0%": {
|
||||
transform: "translateX(110%)"
|
||||
},
|
||||
"100%": {
|
||||
transform: "translateX(0%)"
|
||||
}
|
||||
},
|
||||
appear: {
|
||||
'100%': {opacity: '1'}
|
||||
}
|
||||
}
|
||||
"100%": { opacity: "1" },
|
||||
},
|
||||
widthOut: {
|
||||
"0%": {
|
||||
width: "0%",
|
||||
left: "50%",
|
||||
},
|
||||
"100%": {
|
||||
width: "100%",
|
||||
left: "0%",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
||||
8
enshi_back/.dockerignore
Normal file
8
enshi_back/.dockerignore
Normal file
@ -0,0 +1,8 @@
|
||||
main
|
||||
*.log
|
||||
*.swp
|
||||
*.tmp
|
||||
*.out
|
||||
node_modules
|
||||
.idea
|
||||
.vscode
|
||||
17
enshi_back/ABAC/AdminPolicies/AdminPolicy.go
Normal file
17
enshi_back/ABAC/AdminPolicies/AdminPolicy.go
Normal file
@ -0,0 +1,17 @@
|
||||
package adminpolicies
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AdminPolicies(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
globalrules.IsAdminRule,
|
||||
}
|
||||
|
||||
return rules.CheckRules(c, rulesToCheck, rules.ALL_RULES_MUST_BE_COMPLETED)
|
||||
}
|
||||
36
enshi_back/ABAC/BookmarkPolicies/bookmarkPolicies.go
Normal file
36
enshi_back/ABAC/BookmarkPolicies/bookmarkPolicies.go
Normal file
@ -0,0 +1,36 @@
|
||||
package bookmarkspolicies
|
||||
|
||||
import (
|
||||
bookmarksrules "enshi/ABAC/BookmarkPolicies/bookmarkRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
DELETE_BOOKMARK = "delete_bookmark"
|
||||
CREATE_BOOKMARK = "create_bookmark"
|
||||
READ_BOOKMARK = "read_bookmark"
|
||||
)
|
||||
|
||||
func BlogPolicies(c *gin.Context) (bool, []error) {
|
||||
target, exists := c.Get("target")
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Permit if one permit
|
||||
switch target {
|
||||
case DELETE_BOOKMARK:
|
||||
return rules.CheckRule(c, bookmarksrules.BookmarkDeleteRule)
|
||||
|
||||
case CREATE_BOOKMARK:
|
||||
return rules.CheckRule(c, bookmarksrules.BookmarkCreateRule)
|
||||
|
||||
case READ_BOOKMARK:
|
||||
return rules.CheckRule(c, bookmarksrules.BookmarkReadRule)
|
||||
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
22
enshi_back/ABAC/BookmarkPolicies/bookmarkRules/createRule.go
Normal file
22
enshi_back/ABAC/BookmarkPolicies/bookmarkRules/createRule.go
Normal file
@ -0,0 +1,22 @@
|
||||
package bookmarksrules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BookmarkCreateRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
22
enshi_back/ABAC/BookmarkPolicies/bookmarkRules/deleteRule.go
Normal file
22
enshi_back/ABAC/BookmarkPolicies/bookmarkRules/deleteRule.go
Normal file
@ -0,0 +1,22 @@
|
||||
package bookmarksrules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BookmarkDeleteRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
22
enshi_back/ABAC/BookmarkPolicies/bookmarkRules/readRule.go
Normal file
22
enshi_back/ABAC/BookmarkPolicies/bookmarkRules/readRule.go
Normal file
@ -0,0 +1,22 @@
|
||||
package bookmarksrules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BookmarkReadRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
30
enshi_back/ABAC/GlobalRules/AuthorizedRule.go
Normal file
30
enshi_back/ABAC/GlobalRules/AuthorizedRule.go
Normal file
@ -0,0 +1,30 @@
|
||||
package globalrules
|
||||
|
||||
import (
|
||||
"enshi/auth"
|
||||
"enshi/global"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func AuthorizedRule(c *gin.Context) (bool, []error) {
|
||||
cookies := c.Request.CookiesNamed("auth_cookie")
|
||||
if len(cookies) == 0 {
|
||||
return false, []error{fmt.Errorf("no cookies provided")}
|
||||
}
|
||||
|
||||
tokenFromCookies := cookies[0].Value
|
||||
cookieClimes, err := auth.ValidateToken(tokenFromCookies)
|
||||
if err != nil {
|
||||
c.IndentedJSON(http.StatusUnauthorized, gin.H{"error auth": err.Error()})
|
||||
c.Abort()
|
||||
return false, []error{err}
|
||||
} else {
|
||||
c.Set(global.ContextUserId, cookieClimes["id"])
|
||||
c.Set(global.ContextTokenData, cookieClimes)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
33
enshi_back/ABAC/GlobalRules/IsAdminRule.go
Normal file
33
enshi_back/ABAC/GlobalRules/IsAdminRule.go
Normal file
@ -0,0 +1,33 @@
|
||||
package globalrules
|
||||
|
||||
import (
|
||||
"context"
|
||||
db_repo "enshi/db/go_queries"
|
||||
"enshi/db_connection"
|
||||
"enshi/middleware/getters"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func IsAdminRule(c *gin.Context) (bool, []error) {
|
||||
contextUserId, err := getters.GetUserIdFromContext(c)
|
||||
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
|
||||
user, err :=
|
||||
db_repo.New(db_connection.Dbx).
|
||||
GetUserById(context.Background(), contextUserId)
|
||||
|
||||
if err != nil || user.UserID == 0 {
|
||||
return false, []error{err}
|
||||
}
|
||||
|
||||
if !user.IsAdmin {
|
||||
return false, []error{fmt.Errorf("not admin")}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
39
enshi_back/ABAC/GlobalRules/IsOwnerOfTheBlogRule.go
Normal file
39
enshi_back/ABAC/GlobalRules/IsOwnerOfTheBlogRule.go
Normal file
@ -0,0 +1,39 @@
|
||||
package globalrules
|
||||
|
||||
import (
|
||||
"context"
|
||||
db_repo "enshi/db/go_queries"
|
||||
"enshi/db_connection"
|
||||
"enshi/middleware/getters"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func IsOwnerOfTheBlogRule(c *gin.Context) (bool, []error) {
|
||||
blogId, err := getters.GetInt64Param(c, "blog-id")
|
||||
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
|
||||
contextUserId, err := getters.GetUserIdFromContext(c)
|
||||
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
|
||||
blog, err :=
|
||||
db_repo.New(db_connection.Dbx).
|
||||
GetBlogByBlogId(context.Background(), blogId)
|
||||
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
|
||||
if blog.UserID != contextUserId {
|
||||
return false, []error{fmt.Errorf("now owner of the blog")}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
39
enshi_back/ABAC/GlobalRules/IsOwnerOfThePostRule.go
Normal file
39
enshi_back/ABAC/GlobalRules/IsOwnerOfThePostRule.go
Normal file
@ -0,0 +1,39 @@
|
||||
package globalrules
|
||||
|
||||
import (
|
||||
"context"
|
||||
db_repo "enshi/db/go_queries"
|
||||
"enshi/db_connection"
|
||||
"enshi/middleware/getters"
|
||||
"fmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func IsOwnerOfThePostRule(c *gin.Context) (bool, []error) {
|
||||
postId, err := getters.GetInt64Param(c, "post-id")
|
||||
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
|
||||
contextUserId, err := getters.GetUserIdFromContext(c)
|
||||
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
|
||||
post, err :=
|
||||
db_repo.New(db_connection.Dbx).
|
||||
GetPostsByPostId(context.Background(), postId)
|
||||
|
||||
if err != nil {
|
||||
return false, []error{err}
|
||||
}
|
||||
|
||||
if post.UserID != contextUserId {
|
||||
return false, []error{fmt.Errorf("now owner of the post")}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
38
enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go
Normal file
38
enshi_back/ABAC/PostVotesPolicies/PostVotePolicies.go
Normal file
@ -0,0 +1,38 @@
|
||||
package postvotespolicies
|
||||
|
||||
import (
|
||||
postvoterules "enshi/ABAC/PostVotesPolicies/PostVoteRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
DELETE_VOTE = "delete_vote"
|
||||
CREATE_VOTE = "create_vote"
|
||||
READ_VOTE = "read_vote"
|
||||
)
|
||||
|
||||
func PostVotePolicies(c *gin.Context) (bool, []error) {
|
||||
target, exists := c.Get("target")
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Permit if one permit
|
||||
switch target {
|
||||
case DELETE_VOTE:
|
||||
return rules.CheckRule(c, postvoterules.PostVoteDeleteRule)
|
||||
|
||||
case CREATE_VOTE:
|
||||
return rules.CheckRule(c, postvoterules.PostVoteCreateRule)
|
||||
|
||||
case READ_VOTE:
|
||||
return rules.CheckRule(c, postvoterules.PostVoteReadRule)
|
||||
|
||||
default:
|
||||
return rules.CheckRule(c, postvoterules.PostVotesReadRule)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package postvoterules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func PostVoteCreateRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package postvoterules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func PostVoteDeleteRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
22
enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readRule.go
Normal file
22
enshi_back/ABAC/PostVotesPolicies/PostVoteRules/readRule.go
Normal file
@ -0,0 +1,22 @@
|
||||
package postvoterules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func PostVoteReadRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package postvoterules
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func PostVotesReadRule(c *gin.Context) (bool, []error) {
|
||||
// rulesToCheck := []rules.RuleFunction{}
|
||||
|
||||
// isAllowed, errors := rules.CheckRules(
|
||||
// c,
|
||||
// rulesToCheck,
|
||||
// rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
// )
|
||||
|
||||
return true, nil
|
||||
}
|
||||
48
enshi_back/ABAC/PostsPolicies/postPolicy.go
Normal file
48
enshi_back/ABAC/PostsPolicies/postPolicy.go
Normal file
@ -0,0 +1,48 @@
|
||||
package postspolicies
|
||||
|
||||
import (
|
||||
"enshi/ABAC/PostsPolicies/postRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
DELETE_POST = "delete_post"
|
||||
DELETE_POST_BLOG = "delete_post_blog"
|
||||
UPDATE_POST = "update_post"
|
||||
UPDATE_POST_BLOG = "update_post_blog"
|
||||
CREATE_POST = "create_post"
|
||||
GET_POST = "get_post"
|
||||
)
|
||||
|
||||
func PostsPolicies(c *gin.Context) (bool, []error) {
|
||||
target, exists := c.Get("target")
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Permit if one permit
|
||||
switch target {
|
||||
case DELETE_POST:
|
||||
return rules.CheckRule(c, postRules.DeleteRule)
|
||||
|
||||
case DELETE_POST_BLOG:
|
||||
return rules.CheckRule(c, postRules.DeletePostFromBlogRule)
|
||||
|
||||
case UPDATE_POST:
|
||||
return rules.CheckRule(c, postRules.PostUpdateRule)
|
||||
|
||||
case UPDATE_POST_BLOG:
|
||||
return rules.CheckRule(c, postRules.UpdatePostBlogRule)
|
||||
|
||||
case GET_POST:
|
||||
return rules.CheckRule(c, postRules.PostReadRule)
|
||||
|
||||
case CREATE_POST:
|
||||
return rules.CheckRule(c, postRules.PostCreateRule)
|
||||
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
23
enshi_back/ABAC/PostsPolicies/postRules/createRule.go
Normal file
23
enshi_back/ABAC/PostsPolicies/postRules/createRule.go
Normal file
@ -0,0 +1,23 @@
|
||||
package postRules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Only owner of the post can change it
|
||||
func PostCreateRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package postRules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func DeletePostFromBlogRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
globalrules.IsOwnerOfThePostRule,
|
||||
globalrules.IsOwnerOfTheBlogRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
RULES_NUMBER_TO_COMPLETE,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
27
enshi_back/ABAC/PostsPolicies/postRules/deleteRule.go
Normal file
27
enshi_back/ABAC/PostsPolicies/postRules/deleteRule.go
Normal file
@ -0,0 +1,27 @@
|
||||
package postRules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const RULES_NUMBER_TO_COMPLETE = 2
|
||||
|
||||
// Only owner or admin can delete post
|
||||
func DeleteRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
globalrules.IsOwnerOfThePostRule,
|
||||
globalrules.IsAdminRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
RULES_NUMBER_TO_COMPLETE,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
10
enshi_back/ABAC/PostsPolicies/postRules/readRule.go
Normal file
10
enshi_back/ABAC/PostsPolicies/postRules/readRule.go
Normal file
@ -0,0 +1,10 @@
|
||||
package postRules
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Only owner of the post can change it
|
||||
func PostReadRule(c *gin.Context) (bool, []error) {
|
||||
return true, nil
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package postRules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Only user that own target post and blog can do that
|
||||
func UpdatePostBlogRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
globalrules.IsOwnerOfThePostRule,
|
||||
globalrules.IsOwnerOfTheBlogRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
24
enshi_back/ABAC/PostsPolicies/postRules/updateRule.go
Normal file
24
enshi_back/ABAC/PostsPolicies/postRules/updateRule.go
Normal file
@ -0,0 +1,24 @@
|
||||
package postRules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Only owner of the post can change it
|
||||
func PostUpdateRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
globalrules.IsOwnerOfThePostRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
31
enshi_back/ABAC/ProfilePolicies/ProfilePolicies.go
Normal file
31
enshi_back/ABAC/ProfilePolicies/ProfilePolicies.go
Normal file
@ -0,0 +1,31 @@
|
||||
package profilepolicies
|
||||
|
||||
import (
|
||||
profilesrules "enshi/ABAC/ProfilePolicies/ProfilesRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
RESET_PROFILE = "reset_profile"
|
||||
UPDATE_PROFILE = "update_profile"
|
||||
CREATE_PROFILE = "create_profile"
|
||||
GET_PROFILE = "get_profile"
|
||||
)
|
||||
|
||||
func ProfilePolicies(c *gin.Context) (bool, []error) {
|
||||
target, exists := c.Get("target")
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Permit if one permit
|
||||
switch target {
|
||||
case UPDATE_PROFILE:
|
||||
return rules.CheckRule(c, profilesrules.UpdateProfileRule)
|
||||
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
22
enshi_back/ABAC/ProfilePolicies/ProfilesRules/UpdateRule.go
Normal file
22
enshi_back/ABAC/ProfilePolicies/ProfilesRules/UpdateRule.go
Normal file
@ -0,0 +1,22 @@
|
||||
package profilesrules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func UpdateProfileRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
40
enshi_back/ABAC/blogsPolicies/blogPolicies.go
Normal file
40
enshi_back/ABAC/blogsPolicies/blogPolicies.go
Normal file
@ -0,0 +1,40 @@
|
||||
package blogspolicies
|
||||
|
||||
import (
|
||||
blogrules "enshi/ABAC/blogsPolicies/blogRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
DELETE_BLOG = "delete_blog"
|
||||
UPDATE_BLOG = "update_blog"
|
||||
CREATE_BLOG = "create_blog"
|
||||
GET_BLOG = "get_blog"
|
||||
)
|
||||
|
||||
func BlogPolicies(c *gin.Context) (bool, []error) {
|
||||
target, exists := c.Get("target")
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Permit if one permit
|
||||
switch target {
|
||||
case DELETE_BLOG:
|
||||
return rules.CheckRule(c, blogrules.BlogDeleteRule)
|
||||
|
||||
case UPDATE_BLOG:
|
||||
return rules.CheckRule(c, blogrules.BlogUpdateRule)
|
||||
|
||||
case GET_BLOG:
|
||||
return rules.CheckRule(c, blogrules.BlogReadRule)
|
||||
|
||||
case CREATE_BLOG:
|
||||
return rules.CheckRule(c, blogrules.BlogCreateRule)
|
||||
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
22
enshi_back/ABAC/blogsPolicies/blogRules/createRule.go
Normal file
22
enshi_back/ABAC/blogsPolicies/blogRules/createRule.go
Normal file
@ -0,0 +1,22 @@
|
||||
package blogrules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BlogCreateRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
24
enshi_back/ABAC/blogsPolicies/blogRules/deleteRule.go
Normal file
24
enshi_back/ABAC/blogsPolicies/blogRules/deleteRule.go
Normal file
@ -0,0 +1,24 @@
|
||||
package blogrules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BlogDeleteRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
globalrules.IsOwnerOfTheBlogRule,
|
||||
globalrules.IsAdminRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
2,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
19
enshi_back/ABAC/blogsPolicies/blogRules/readRule.go
Normal file
19
enshi_back/ABAC/blogsPolicies/blogRules/readRule.go
Normal file
@ -0,0 +1,19 @@
|
||||
package blogrules
|
||||
|
||||
import (
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BlogReadRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
23
enshi_back/ABAC/blogsPolicies/blogRules/updateRule.go
Normal file
23
enshi_back/ABAC/blogsPolicies/blogRules/updateRule.go
Normal file
@ -0,0 +1,23 @@
|
||||
package blogrules
|
||||
|
||||
import (
|
||||
globalrules "enshi/ABAC/GlobalRules"
|
||||
"enshi/ABAC/rules"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BlogUpdateRule(c *gin.Context) (bool, []error) {
|
||||
rulesToCheck := []rules.RuleFunction{
|
||||
globalrules.AuthorizedRule,
|
||||
globalrules.IsOwnerOfTheBlogRule,
|
||||
}
|
||||
|
||||
isAllowed, errors := rules.CheckRules(
|
||||
c,
|
||||
rulesToCheck,
|
||||
rules.ALL_RULES_MUST_BE_COMPLETED,
|
||||
)
|
||||
|
||||
return isAllowed, errors
|
||||
}
|
||||
72
enshi_back/ABAC/rules/CheckRule.go
Normal file
72
enshi_back/ABAC/rules/CheckRule.go
Normal file
@ -0,0 +1,72 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type RuleFunction func(*gin.Context) (bool, []error)
|
||||
|
||||
const (
|
||||
ALL_RULES_MUST_BE_COMPLETED = iota
|
||||
)
|
||||
|
||||
func CheckRule(
|
||||
c *gin.Context,
|
||||
ruleChecker RuleFunction,
|
||||
) (bool, []error) {
|
||||
IsAllowed, err := ruleChecker(c)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return IsAllowed, nil
|
||||
}
|
||||
|
||||
func CheckRules(
|
||||
c *gin.Context,
|
||||
rules []RuleFunction,
|
||||
completedRulesCount int,
|
||||
) (bool, []error) {
|
||||
var allowancesIndexes []int
|
||||
var errors []error
|
||||
|
||||
if len(rules) < completedRulesCount {
|
||||
return false, []error{fmt.Errorf("there is less rules, that should be completed")}
|
||||
}
|
||||
|
||||
for i, rule := range rules {
|
||||
if isAllowed, err := CheckRule(c, rule); err != nil {
|
||||
errors = append(
|
||||
errors,
|
||||
err...,
|
||||
)
|
||||
} else if !isAllowed {
|
||||
errors = append(
|
||||
errors,
|
||||
fmt.Errorf("rule "+
|
||||
strconv.Itoa(i)+
|
||||
" was rejected"),
|
||||
)
|
||||
} else {
|
||||
allowancesIndexes = append(allowancesIndexes, i)
|
||||
}
|
||||
}
|
||||
|
||||
switch completedRulesCount {
|
||||
case ALL_RULES_MUST_BE_COMPLETED:
|
||||
if len(allowancesIndexes) == len(rules) {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, errors
|
||||
}
|
||||
default:
|
||||
if len(allowancesIndexes) >= completedRulesCount {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, errors
|
||||
}
|
||||
}
|
||||
}
|
||||
28
enshi_back/ABAC/rules/ShouldAbortRequest.go
Normal file
28
enshi_back/ABAC/rules/ShouldAbortRequest.go
Normal file
@ -0,0 +1,28 @@
|
||||
package rules
|
||||
|
||||
import (
|
||||
rest_api_stuff "enshi/REST_API_stuff"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ShouldAbortRequest(c *gin.Context, isAllowed bool, errors []error) bool {
|
||||
var errorsMap = map[int]string{}
|
||||
for i, error := range errors {
|
||||
errorsMap[i] = error.Error()
|
||||
}
|
||||
|
||||
if errors != nil {
|
||||
c.IndentedJSON(http.StatusUnauthorized, errorsMap)
|
||||
return true
|
||||
}
|
||||
|
||||
if !isAllowed {
|
||||
rest_api_stuff.UnauthorizedAnswer(c, fmt.Errorf("you have no permission"))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@ -21,7 +21,7 @@ RETURNING blog_id, user_id, title, description, category_id, created_at
|
||||
type CreateBlogByUserIdParams struct {
|
||||
BlogID int64 `json:"blog_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
Title pgtype.Text `json:"title" validate:"required"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
CategoryID pgtype.Int4 `json:"category_id"`
|
||||
}
|
||||
@ -56,6 +56,26 @@ func (q *Queries) DeleteBlogByBlogId(ctx context.Context, blogID int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const getBlogByBlogId = `-- name: GetBlogByBlogId :one
|
||||
SELECT blog_id, user_id, title, description, category_id, created_at
|
||||
FROM public.blogs
|
||||
WHERE blog_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetBlogByBlogId(ctx context.Context, blogID int64) (Blog, error) {
|
||||
row := q.db.QueryRow(ctx, getBlogByBlogId, blogID)
|
||||
var i Blog
|
||||
err := row.Scan(
|
||||
&i.BlogID,
|
||||
&i.UserID,
|
||||
&i.Title,
|
||||
&i.Description,
|
||||
&i.CategoryID,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getBlogsByUserId = `-- name: GetBlogsByUserId :many
|
||||
SELECT blog_id, user_id, title, description, category_id, created_at
|
||||
FROM public.blogs
|
||||
@ -97,7 +117,7 @@ RETURNING blog_id, user_id, title, description, category_id, created_at
|
||||
`
|
||||
|
||||
type UpdateBlogInfoByBlogIdParams struct {
|
||||
Title pgtype.Text `json:"title"`
|
||||
Title pgtype.Text `json:"title" validate:"required"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
CategoryID pgtype.Int4 `json:"category_id"`
|
||||
BlogID int64 `json:"blog_id"`
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
type Blog struct {
|
||||
BlogID int64 `json:"blog_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
Title pgtype.Text `json:"title" validate:"required"`
|
||||
Description pgtype.Text `json:"description"`
|
||||
CategoryID pgtype.Int4 `json:"category_id"`
|
||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||
|
||||
@ -13,6 +13,9 @@ const createPostVote = `-- name: CreatePostVote :one
|
||||
INSERT INTO public.post_votes
|
||||
(post_id, user_id, vote)
|
||||
VALUES($1, $2, $3)
|
||||
ON CONFLICT (user_id, post_id)
|
||||
DO UPDATE SET
|
||||
vote = $3
|
||||
RETURNING post_id, user_id, vote
|
||||
`
|
||||
|
||||
@ -62,6 +65,25 @@ func (q *Queries) GetPostVote(ctx context.Context, arg GetPostVoteParams) (bool,
|
||||
return vote, err
|
||||
}
|
||||
|
||||
const getPostVotes = `-- name: GetPostVotes :one
|
||||
SELECT count (*) FILTER (WHERE vote = TRUE) as upvotes,
|
||||
count (*) FILTER (WHERE vote = FALSE) as downvotes
|
||||
FROM public.post_votes
|
||||
WHERE post_id = $1
|
||||
`
|
||||
|
||||
type GetPostVotesRow struct {
|
||||
Upvotes int64 `json:"upvotes"`
|
||||
Downvotes int64 `json:"downvotes"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetPostVotes(ctx context.Context, postID int64) (GetPostVotesRow, error) {
|
||||
row := q.db.QueryRow(ctx, getPostVotes, postID)
|
||||
var i GetPostVotesRow
|
||||
err := row.Scan(&i.Upvotes, &i.Downvotes)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const updateVote = `-- name: UpdateVote :one
|
||||
UPDATE public.post_votes
|
||||
SET vote=$1
|
||||
|
||||
@ -146,29 +146,79 @@ 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
|
||||
`
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const updatePostBlogId = `-- name: UpdatePostBlogId :exec
|
||||
UPDATE public.posts
|
||||
SET blog_id=$2, updated_at=CURRENT_TIMESTAMP
|
||||
WHERE post_id = $1
|
||||
RETURNING post_id, blog_id, user_id, title, content, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdatePostBlogIdParams struct {
|
||||
PostID int64 `json:"post_id"`
|
||||
BlogID pgtype.Int8 `json:"blog_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdatePostBlogId(ctx context.Context, arg UpdatePostBlogIdParams) error {
|
||||
_, err := q.db.Exec(ctx, updatePostBlogId, arg.PostID, arg.BlogID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updatePostByPostId = `-- name: UpdatePostByPostId :one
|
||||
UPDATE public.posts
|
||||
SET blog_id=$1, user_id=$2, title=$3, "content"=$4, updated_at=CURRENT_TIMESTAMP
|
||||
WHERE post_id = $5
|
||||
SET title=$1, "content"=$2, updated_at=CURRENT_TIMESTAMP
|
||||
WHERE post_id = $3
|
||||
RETURNING post_id, blog_id, user_id, title, content, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdatePostByPostIdParams struct {
|
||||
BlogID pgtype.Int8 `json:"blog_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Title pgtype.Text `json:"title"`
|
||||
Content pgtype.Text `json:"content"`
|
||||
PostID int64 `json:"post_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdatePostByPostId(ctx context.Context, arg UpdatePostByPostIdParams) (Post, error) {
|
||||
row := q.db.QueryRow(ctx, updatePostByPostId,
|
||||
arg.BlogID,
|
||||
arg.UserID,
|
||||
arg.Title,
|
||||
arg.Content,
|
||||
arg.PostID,
|
||||
)
|
||||
row := q.db.QueryRow(ctx, updatePostByPostId, arg.Title, arg.Content, arg.PostID)
|
||||
var i Post
|
||||
err := row.Scan(
|
||||
&i.PostID,
|
||||
|
||||
@ -152,6 +152,17 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getUserUsernameById = `-- name: GetUserUsernameById :one
|
||||
SELECT username FROM users WHERE user_id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetUserUsernameById(ctx context.Context, userID int64) (string, error) {
|
||||
row := q.db.QueryRow(ctx, getUserUsernameById, userID)
|
||||
var username string
|
||||
err := row.Scan(&username)
|
||||
return username, err
|
||||
}
|
||||
|
||||
const updateUserPasswordHash = `-- name: UpdateUserPasswordHash :one
|
||||
UPDATE public.users
|
||||
SET "password"=$1
|
||||
|
||||
@ -15,6 +15,11 @@ SELECT *
|
||||
FROM public.blogs
|
||||
WHERE user_id = $1;
|
||||
|
||||
-- name: GetBlogByBlogId :one
|
||||
SELECT *
|
||||
FROM public.blogs
|
||||
WHERE blog_id = $1;
|
||||
|
||||
-- name: DeleteBlogByBlogId :exec
|
||||
DELETE FROM public.blogs
|
||||
WHERE blog_id=$1;
|
||||
@ -2,6 +2,9 @@
|
||||
INSERT INTO public.post_votes
|
||||
(post_id, user_id, vote)
|
||||
VALUES($1, $2, $3)
|
||||
ON CONFLICT (user_id, post_id)
|
||||
DO UPDATE SET
|
||||
vote = $3
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeletePostVote :exec
|
||||
@ -18,3 +21,9 @@ RETURNING *;
|
||||
SELECT vote
|
||||
FROM public.post_votes p_v
|
||||
WHERE p_v.user_id = $1 and p_v.post_id = $2;
|
||||
|
||||
-- name: GetPostVotes :one
|
||||
SELECT count (*) FILTER (WHERE vote = TRUE) as upvotes,
|
||||
count (*) FILTER (WHERE vote = FALSE) as downvotes
|
||||
FROM public.post_votes
|
||||
WHERE post_id = $1;
|
||||
|
||||
@ -21,10 +21,22 @@ RETURNING *;
|
||||
|
||||
-- name: UpdatePostByPostId :one
|
||||
UPDATE public.posts
|
||||
SET blog_id=$1, user_id=$2, title=$3, "content"=$4, updated_at=CURRENT_TIMESTAMP
|
||||
WHERE post_id = $5
|
||||
SET title=$1, "content"=$2, updated_at=CURRENT_TIMESTAMP
|
||||
WHERE post_id = $3
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeletePostByPostId :exec
|
||||
DELETE FROM public.posts
|
||||
WHERE post_id=$1;
|
||||
|
||||
-- name: UpdatePostBlogId :exec
|
||||
UPDATE public.posts
|
||||
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;
|
||||
@ -4,6 +4,9 @@ SELECT * FROM users;
|
||||
-- name: GetUserById :one
|
||||
SELECT * FROM users WHERE user_id = $1;
|
||||
|
||||
-- name: GetUserUsernameById :one
|
||||
SELECT username FROM users WHERE user_id = $1;
|
||||
|
||||
-- name: GetUserByUsername :one
|
||||
SELECT * FROM users WHERE username = $1;
|
||||
|
||||
|
||||
@ -18,6 +18,9 @@ sql:
|
||||
- column: users.email
|
||||
go_struct_tag: validate:"required,email"
|
||||
|
||||
- column: blogs.title
|
||||
go_struct_tag: validate:"required"
|
||||
|
||||
- db_type: "uuid"
|
||||
go_type:
|
||||
import: 'github.com/google/uuid'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user