Merge pull request 'dev' (#2) from dev into production
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 0s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 0s
Reviewed-on: #2
This commit is contained in:
commit
286735b8a5
10
.gitea/workflows/demo.yml
Normal file
10
.gitea/workflows/demo.yml
Normal file
@ -0,0 +1,10 @@
|
||||
name: Gitea Actions Demo
|
||||
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
Explore-Gitea-Actions:
|
||||
runs-on: linux
|
||||
steps:
|
||||
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
||||
- run: echo "🎉 Test job completed! 🚀"
|
||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
||||
FROM node:18-alpine
|
||||
|
||||
RUN npm install -g pnpm
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pnpm build
|
||||
|
||||
CMD ["pnpm", "start"]
|
||||
15
deploy.sh
Normal file
15
deploy.sh
Normal file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <project_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROJECT_PATH=$1
|
||||
|
||||
echo "Deploying project..."
|
||||
cd "$PROJECT_PATH" || { echo "Directory not found: $PROJECT_PATH"; exit 1; }
|
||||
git pull origin main
|
||||
docker compose up -d --no-deps --build nextapp
|
||||
echo "Deployment finished!"
|
||||
42
docker-compose.yml
Normal file
42
docker-compose.yml
Normal file
@ -0,0 +1,42 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
nextapp:
|
||||
build: .
|
||||
deploy:
|
||||
update_config:
|
||||
parallelism: 1
|
||||
delay: 5s
|
||||
order: start-first
|
||||
container_name: logipedia
|
||||
restart: always
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.logipedia.rule=Host(`logipedia.shortman.me`)"
|
||||
- "traefik.http.routers.logipedia.entrypoints=websecure"
|
||||
- "traefik.http.services.logipedia.loadbalancer.server.port=3000"
|
||||
- "traefik.http.routers.logipedia.tls.certresolver=myresolver"
|
||||
expose:
|
||||
- "3000"
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
networks:
|
||||
- webproxy
|
||||
db:
|
||||
image: postgres:latest
|
||||
container_name: logipedia-db
|
||||
restart: always
|
||||
shm_size: 128mb
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
webproxy:
|
||||
external: true
|
||||
@ -6,6 +6,9 @@ import "./src/env.js";
|
||||
|
||||
/** @type {import("next").NextConfig} */
|
||||
const config = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
experimental: {
|
||||
serverActions: {
|
||||
bodySizeLimit: "2mb",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "wiki-antifa",
|
||||
"name": "logipedia",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@ -46,6 +46,7 @@
|
||||
"@trpc/client": "^11.0.0-rc.446",
|
||||
"@trpc/react-query": "^11.0.0-rc.446",
|
||||
"@trpc/server": "^11.0.0-rc.446",
|
||||
"argon2": "^0.41.1",
|
||||
"cheerio": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@ -67,7 +68,9 @@
|
||||
"superjson": "^2.2.1",
|
||||
"tailwind-merge": "^3.0.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiptap-extension-resize-image": "^1.2.1",
|
||||
"use-debounce": "^10.0.4",
|
||||
"winston": "^3.17.0",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
226
pnpm-lock.yaml
generated
226
pnpm-lock.yaml
generated
@ -86,6 +86,9 @@ importers:
|
||||
'@trpc/server':
|
||||
specifier: ^11.0.0-rc.446
|
||||
version: 11.0.0-rc.824(typescript@5.8.2)
|
||||
argon2:
|
||||
specifier: ^0.41.1
|
||||
version: 0.41.1
|
||||
cheerio:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0
|
||||
@ -149,9 +152,15 @@ importers:
|
||||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.23)(typescript@5.8.2)))
|
||||
tiptap-extension-resize-image:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-image@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))(@tiptap/pm@2.11.5)
|
||||
use-debounce:
|
||||
specifier: ^10.0.4
|
||||
version: 10.0.4(react@18.3.1)
|
||||
winston:
|
||||
specifier: ^3.17.0
|
||||
version: 3.17.0
|
||||
zod:
|
||||
specifier: ^3.24.2
|
||||
version: 3.24.2
|
||||
@ -255,10 +264,17 @@ packages:
|
||||
'@cfcs/core@0.0.6':
|
||||
resolution: {integrity: sha512-FxfJMwoLB8MEMConeXUCqtMGqxdtePQxRBOiGip9ULcYYam3WfCgoY6xdnMaSkYvRvmosp5iuG+TiPofm65+Pw==}
|
||||
|
||||
'@colors/colors@1.6.0':
|
||||
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
|
||||
engines: {node: '>=0.1.90'}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@dabh/diagnostics@2.0.3':
|
||||
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||
|
||||
'@daybrush/utils@1.13.0':
|
||||
resolution: {integrity: sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ==}
|
||||
|
||||
@ -969,6 +985,10 @@ packages:
|
||||
'@paralleldrive/cuid2@2.2.2':
|
||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||
|
||||
'@phc/format@1.0.0':
|
||||
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@ -1930,6 +1950,9 @@ packages:
|
||||
'@types/react@18.3.18':
|
||||
resolution: {integrity: sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==}
|
||||
|
||||
'@types/triple-beam@1.3.5':
|
||||
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
@ -2035,6 +2058,10 @@ packages:
|
||||
arg@5.0.2:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
|
||||
argon2@0.41.1:
|
||||
resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
@ -2085,6 +2112,9 @@ packages:
|
||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2202,20 +2232,32 @@ packages:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
color-convert@1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.3:
|
||||
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
|
||||
color@3.2.1:
|
||||
resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
|
||||
|
||||
color@4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
|
||||
colorspace@1.1.4:
|
||||
resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
|
||||
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
@ -2470,6 +2512,9 @@ packages:
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
enabled@2.0.0:
|
||||
resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==}
|
||||
|
||||
encoding-sniffer@0.2.0:
|
||||
resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==}
|
||||
|
||||
@ -2688,6 +2733,9 @@ packages:
|
||||
picomatch:
|
||||
optional: true
|
||||
|
||||
fecha@4.2.3:
|
||||
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
|
||||
|
||||
file-entry-cache@6.0.1:
|
||||
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
@ -2707,6 +2755,9 @@ packages:
|
||||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
|
||||
fn.name@1.1.0:
|
||||
resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
|
||||
|
||||
for-each@0.3.5:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -2976,6 +3027,10 @@ packages:
|
||||
resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-string@1.1.1:
|
||||
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3076,6 +3131,9 @@ packages:
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
kuler@2.0.0:
|
||||
resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==}
|
||||
|
||||
language-subtag-registry@0.3.23:
|
||||
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
|
||||
|
||||
@ -3107,6 +3165,10 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
logform@2.7.0:
|
||||
resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
@ -3305,6 +3367,14 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
node-addon-api@8.3.1:
|
||||
resolution: {integrity: sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-gyp-build@4.8.4:
|
||||
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
|
||||
hasBin: true
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -3359,6 +3429,9 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
one-time@1.0.0:
|
||||
resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@ -3737,6 +3810,10 @@ packages:
|
||||
read-cache@1.0.0:
|
||||
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
|
||||
|
||||
readable-stream@3.6.2:
|
||||
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@ -3793,6 +3870,9 @@ packages:
|
||||
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -3801,6 +3881,10 @@ packages:
|
||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
safe-stable-stringify@2.5.0:
|
||||
resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
@ -3892,6 +3976,9 @@ packages:
|
||||
stable-hash@0.0.4:
|
||||
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
|
||||
|
||||
stack-trace@0.0.10:
|
||||
resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==}
|
||||
|
||||
streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@ -3927,6 +4014,9 @@ packages:
|
||||
resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
@ -4004,6 +4094,9 @@ packages:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
text-hex@1.0.0:
|
||||
resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==}
|
||||
|
||||
text-table@0.2.0:
|
||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||
|
||||
@ -4024,6 +4117,13 @@ packages:
|
||||
tiptap-extension-global-drag-handle@0.1.18:
|
||||
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
|
||||
|
||||
tiptap-extension-resize-image@1.2.1:
|
||||
resolution: {integrity: sha512-SLMAujDa+0LN/6Iv2HtU4Uk0BL6LMh4b/r85frpdnjFDW2i6pIOfTVG8jzJQ8T1EgYHNn2YG1U2HoVAGuwLc3Q==}
|
||||
peerDependencies:
|
||||
'@tiptap/core': ^2.0.0
|
||||
'@tiptap/extension-image': ^2.0.0
|
||||
'@tiptap/pm': ^2.0.0
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@ -4031,6 +4131,10 @@ packages:
|
||||
trim-lines@3.0.1:
|
||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||
|
||||
triple-beam@1.4.1:
|
||||
resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==}
|
||||
engines: {node: '>= 14.0.0'}
|
||||
|
||||
trough@2.2.0:
|
||||
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
||||
|
||||
@ -4240,6 +4344,14 @@ packages:
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
winston-transport@4.9.0:
|
||||
resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
winston@3.17.0:
|
||||
resolution: {integrity: sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
word-wrap@1.2.5:
|
||||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -4327,10 +4439,18 @@ snapshots:
|
||||
dependencies:
|
||||
'@egjs/component': 3.0.5
|
||||
|
||||
'@colors/colors@1.6.0': {}
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@dabh/diagnostics@2.0.3':
|
||||
dependencies:
|
||||
colorspace: 1.1.4
|
||||
enabled: 2.0.0
|
||||
kuler: 2.0.0
|
||||
|
||||
'@daybrush/utils@1.13.0': {}
|
||||
|
||||
'@drizzle-team/brocli@0.10.2': {}
|
||||
@ -4785,6 +4905,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@noble/hashes': 1.7.1
|
||||
|
||||
'@phc/format@1.0.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
@ -5737,6 +5859,8 @@ snapshots:
|
||||
'@types/prop-types': 15.7.14
|
||||
csstype: 3.1.3
|
||||
|
||||
'@types/triple-beam@1.3.5': {}
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
@ -5860,6 +5984,12 @@ snapshots:
|
||||
|
||||
arg@5.0.2: {}
|
||||
|
||||
argon2@0.41.1:
|
||||
dependencies:
|
||||
'@phc/format': 1.0.0
|
||||
node-addon-api: 8.3.1
|
||||
node-gyp-build: 4.8.4
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
aria-hidden@1.2.4:
|
||||
@ -5936,6 +6066,8 @@ snapshots:
|
||||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
available-typed-arrays@1.0.7:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
@ -6074,17 +6206,27 @@ snapshots:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
color-convert@1.9.3:
|
||||
dependencies:
|
||||
color-name: 1.1.3
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.3: {}
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
color-string@1.9.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
optional: true
|
||||
|
||||
color@3.2.1:
|
||||
dependencies:
|
||||
color-convert: 1.9.3
|
||||
color-string: 1.9.1
|
||||
|
||||
color@4.2.3:
|
||||
dependencies:
|
||||
@ -6092,6 +6234,11 @@ snapshots:
|
||||
color-string: 1.9.1
|
||||
optional: true
|
||||
|
||||
colorspace@1.1.4:
|
||||
dependencies:
|
||||
color: 3.2.1
|
||||
text-hex: 1.0.0
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
@ -6257,6 +6404,8 @@ snapshots:
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
enabled@2.0.0: {}
|
||||
|
||||
encoding-sniffer@0.2.0:
|
||||
dependencies:
|
||||
iconv-lite: 0.6.3
|
||||
@ -6691,6 +6840,8 @@ snapshots:
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.2
|
||||
|
||||
fecha@4.2.3: {}
|
||||
|
||||
file-entry-cache@6.0.1:
|
||||
dependencies:
|
||||
flat-cache: 3.2.0
|
||||
@ -6712,6 +6863,8 @@ snapshots:
|
||||
|
||||
flatted@3.3.3: {}
|
||||
|
||||
fn.name@1.1.0: {}
|
||||
|
||||
for-each@0.3.5:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
@ -6919,8 +7072,7 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
is-arrayish@0.3.2:
|
||||
optional: true
|
||||
is-arrayish@0.3.2: {}
|
||||
|
||||
is-async-function@2.1.1:
|
||||
dependencies:
|
||||
@ -7013,6 +7165,8 @@ snapshots:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
|
||||
is-stream@2.0.1: {}
|
||||
|
||||
is-string@1.1.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@ -7111,6 +7265,8 @@ snapshots:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
kuler@2.0.0: {}
|
||||
|
||||
language-subtag-registry@0.3.23: {}
|
||||
|
||||
language-tags@1.0.9:
|
||||
@ -7138,6 +7294,15 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
logform@2.7.0:
|
||||
dependencies:
|
||||
'@colors/colors': 1.6.0
|
||||
'@types/triple-beam': 1.3.5
|
||||
fecha: 4.2.3
|
||||
ms: 2.1.3
|
||||
safe-stable-stringify: 2.5.0
|
||||
triple-beam: 1.4.1
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
@ -7460,6 +7625,10 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-addon-api@8.3.1: {}
|
||||
|
||||
node-gyp-build@4.8.4: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
novel@1.0.2(@tiptap/extension-code-block@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5))(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
@ -7557,6 +7726,10 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
one-time@1.0.0:
|
||||
dependencies:
|
||||
fn.name: 1.1.0
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@ -7944,6 +8117,12 @@ snapshots:
|
||||
dependencies:
|
||||
pify: 2.3.0
|
||||
|
||||
readable-stream@3.6.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
string_decoder: 1.3.0
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
@ -8023,6 +8202,8 @@ snapshots:
|
||||
has-symbols: 1.1.0
|
||||
isarray: 2.0.5
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@ -8034,6 +8215,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-regex: 1.2.1
|
||||
|
||||
safe-stable-stringify@2.5.0: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scheduler@0.23.2:
|
||||
@ -8147,7 +8330,6 @@ snapshots:
|
||||
simple-swizzle@0.2.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
optional: true
|
||||
|
||||
sonner@2.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
@ -8167,6 +8349,8 @@ snapshots:
|
||||
|
||||
stable-hash@0.0.4: {}
|
||||
|
||||
stack-trace@0.0.10: {}
|
||||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
@ -8231,6 +8415,10 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
string_decoder@1.3.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
dependencies:
|
||||
character-entities-html4: 2.1.0
|
||||
@ -8322,6 +8510,8 @@ snapshots:
|
||||
|
||||
tapable@2.2.1: {}
|
||||
|
||||
text-hex@1.0.0: {}
|
||||
|
||||
text-table@0.2.0: {}
|
||||
|
||||
thenify-all@1.6.0:
|
||||
@ -8343,12 +8533,20 @@ snapshots:
|
||||
|
||||
tiptap-extension-global-drag-handle@0.1.18: {}
|
||||
|
||||
tiptap-extension-resize-image@1.2.1(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-image@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))(@tiptap/pm@2.11.5):
|
||||
dependencies:
|
||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||
'@tiptap/extension-image': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
|
||||
'@tiptap/pm': 2.11.5
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
trim-lines@3.0.1: {}
|
||||
|
||||
triple-beam@1.4.1: {}
|
||||
|
||||
trough@2.2.0: {}
|
||||
|
||||
ts-api-utils@2.0.1(typescript@5.8.2):
|
||||
@ -8600,6 +8798,26 @@ snapshots:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
winston-transport@4.9.0:
|
||||
dependencies:
|
||||
logform: 2.7.0
|
||||
readable-stream: 3.6.2
|
||||
triple-beam: 1.4.1
|
||||
|
||||
winston@3.17.0:
|
||||
dependencies:
|
||||
'@colors/colors': 1.6.0
|
||||
'@dabh/diagnostics': 2.0.3
|
||||
async: 3.2.6
|
||||
is-stream: 2.0.1
|
||||
logform: 2.7.0
|
||||
one-time: 1.0.0
|
||||
readable-stream: 3.6.2
|
||||
safe-stable-stringify: 2.5.0
|
||||
stack-trace: 0.0.10
|
||||
triple-beam: 1.4.1
|
||||
winston-transport: 4.9.0
|
||||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
|
||||
1
public/placeholder.svg
Normal file
1
public/placeholder.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
BIN
public/uploads/upload-1742077219929-person-3.jpg.png
Normal file
BIN
public/uploads/upload-1742077219929-person-3.jpg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
@ -1,8 +1,9 @@
|
||||
import "dotenv/config";
|
||||
import { env } from "@/env";
|
||||
import { db } from "../src/server/db";
|
||||
import { articles, categories, users } from "../src/server/db/schema";
|
||||
|
||||
async function seed() {
|
||||
async function developmentSeed() {
|
||||
const usersData = Array.from({ length: 100 }).map((_, i) => ({
|
||||
name: `User ${i + 1}`,
|
||||
email: `user${i + 1}@example.com`,
|
||||
@ -37,9 +38,29 @@ async function seed() {
|
||||
console.log("Seeded " + a.length + " articles");
|
||||
}
|
||||
|
||||
async function productionSeed() {
|
||||
const user = await db.query.users.findFirst();
|
||||
if (user) {
|
||||
console.log("Skipped seeding, user already exists");
|
||||
return;
|
||||
}
|
||||
const initialUser = {
|
||||
name: "Admin",
|
||||
email: "payblot@gmail.com",
|
||||
role: 7,
|
||||
};
|
||||
await db.insert(users).values(initialUser);
|
||||
console.log("Seeded user");
|
||||
}
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
await seed();
|
||||
if (env.NODE_ENV === "development") await developmentSeed();
|
||||
else if (env.NODE_ENV === "production") await productionSeed();
|
||||
else
|
||||
console.log(
|
||||
"Skipped seeding, NODE_ENV is not set to development or production",
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import RenderArticle from "@/components/article/render-article";
|
||||
import BreadNavigator from "@/components/bread-navigator";
|
||||
import RenderContent from "@/components/editor/render-content";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { appRoutes } from "@/config";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
@ -21,8 +22,9 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="w-full">
|
||||
<div className="flex w-full items-center gap-4">
|
||||
<BreadNavigator
|
||||
className="w-full"
|
||||
links={[
|
||||
...(article?.category
|
||||
? [
|
||||
@ -38,7 +40,14 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
/>
|
||||
</div>
|
||||
{isEditor && (
|
||||
<div className="flex w-full justify-end space-x-2">
|
||||
<div className="flex w-full items-center justify-end space-x-2">
|
||||
<Badge
|
||||
className="size-max"
|
||||
variant={article.published ? "outline" : "destructive"}
|
||||
>
|
||||
{article.published ? "Veröffentlicht" : "Draft"}
|
||||
</Badge>
|
||||
|
||||
<Button asChild variant={"outline"}>
|
||||
<Link href={appRoutes.editArticle(article.slug)}>
|
||||
<Edit className="size-4" />
|
||||
@ -52,10 +61,7 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold">{article.title}</h1>
|
||||
|
||||
{article?.content?.length ? (
|
||||
<RenderArticle content={article.content} />
|
||||
) : null}
|
||||
{article.content && <RenderContent content={article.content} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,21 +14,21 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { z } from "zod";
|
||||
import { userProfileSchema } from "@/lib/validation/zod/user";
|
||||
import { userSchema } from "@/lib/validation/zod/user";
|
||||
import { User } from "next-auth";
|
||||
import { updateUserProfile } from "@/server/actions/user";
|
||||
import { toast } from "sonner";
|
||||
|
||||
function UserForm({ server_user, cb }: { server_user: User; cb?: () => void }) {
|
||||
const form = useForm<z.infer<typeof userProfileSchema>>({
|
||||
resolver: zodResolver(userProfileSchema),
|
||||
const form = useForm<z.infer<typeof userSchema>>({
|
||||
resolver: zodResolver(userSchema),
|
||||
defaultValues: {
|
||||
name: server_user?.name ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
// 2. Define a submit handler.
|
||||
async function onSubmit(values: z.infer<typeof userProfileSchema>) {
|
||||
async function onSubmit(values: z.infer<typeof userSchema>) {
|
||||
// Do something with the form values.
|
||||
// ✅ This will be type-safe and validated.
|
||||
const { success } = await updateUserProfile(values);
|
||||
|
||||
@ -10,8 +10,8 @@ import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
const categories = await api.category.getAll({ limit: 6 });
|
||||
const articles = await api.article.getAll({ limit: 6 });
|
||||
const categories = await api.category.getMany({ limit: 6 });
|
||||
const articles = await api.article.getMany({ limit: 6 });
|
||||
return (
|
||||
<>
|
||||
<Alert>
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import * as cheerio from "cheerio";
|
||||
import { NextApiRequest } from "next";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(req: NextApiRequest) {
|
||||
export async function GET(req: Request) {
|
||||
if (req.method !== "GET") {
|
||||
return NextResponse.json({ error: "Method not allowed" });
|
||||
}
|
||||
|
||||
@ -6,10 +6,11 @@ import { type Metadata } from "next";
|
||||
import { TRPCReactProvider } from "@/trpc/react";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { appConfig } from "@/config";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create T3 App",
|
||||
description: "Generated by create-t3-app",
|
||||
title: appConfig.name,
|
||||
description: appConfig.description,
|
||||
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||
};
|
||||
|
||||
|
||||
@ -13,14 +13,17 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import Avatar from "../avatar";
|
||||
import { Icons } from "../icons";
|
||||
import { Badge } from "../ui/badge";
|
||||
|
||||
function ArticleCard({
|
||||
title,
|
||||
slug,
|
||||
author,
|
||||
published,
|
||||
createdAt,
|
||||
}: Pick<Article, "title" | "slug" | "createdAt" | "author">) {
|
||||
}: Pick<Article, "title" | "slug" | "createdAt" | "author" | "published">) {
|
||||
const authorName = author?.name ?? `${appConfig.name} Team`;
|
||||
|
||||
return (
|
||||
<Link href={appRoutes.article(slug)}>
|
||||
<Card className="group flex h-full flex-col justify-between">
|
||||
@ -37,11 +40,21 @@ function ArticleCard({
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">{authorName}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{createdAt.toLocaleDateString("de-DE", {
|
||||
dateStyle: "long",
|
||||
})}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
{typeof published === "boolean" && (
|
||||
<Badge
|
||||
className="size-max px-2 py-px text-xs"
|
||||
variant={published ? "outline" : "destructive"}
|
||||
>
|
||||
{published ? "Veröffentlicht" : "Draft"}
|
||||
</Badge>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{createdAt.toLocaleDateString("de-DE", {
|
||||
dateStyle: "long",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Link>
|
||||
|
||||
@ -85,6 +85,7 @@ function ArticleFilterBar({
|
||||
|
||||
<CategorySelect
|
||||
className="w-full"
|
||||
initialValue={filter.category}
|
||||
onSelect={(category) => {
|
||||
onFilterChange({
|
||||
category: category?.length ? category : undefined,
|
||||
|
||||
@ -25,7 +25,9 @@ import CategorySelect from "@/components/category/category-select";
|
||||
import { CheckCircle, XCircle } from "lucide-react";
|
||||
import PublishArticleAlertDialog from "./publish-article-alert-dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import Editor from "../editor";
|
||||
import dynamic from "next/dynamic";
|
||||
import { appRoutes } from "@/config";
|
||||
const Editor = dynamic(() => import("../editor"), { ssr: false });
|
||||
|
||||
export default ({ server_article }: { server_article: Article }) => {
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
@ -77,7 +79,7 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
<FormControl>
|
||||
<TextareaAutosize
|
||||
cols={1}
|
||||
className="h-max w-full resize-none text-4xl font-bold focus-visible:outline-none"
|
||||
className="h-max w-full resize-none bg-transparent text-4xl font-bold focus-visible:outline-none"
|
||||
value={field.value}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
@ -116,7 +118,7 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
// loading && "border-t-blue-600",
|
||||
)}
|
||||
>
|
||||
<div className="relative w-full space-y-4 overflow-hidden rounded-md border-t-2 bg-muted p-4">
|
||||
<div className="relative w-full space-y-4 overflow-hidden rounded-md border bg-background p-4">
|
||||
<div
|
||||
className={cn(
|
||||
"saving absolute left-0 right-0 top-0 h-0.5 rounded-full bg-primary",
|
||||
@ -151,11 +153,11 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
href={"/editoren-hilfe"}
|
||||
href={appRoutes.article(server_article.slug)}
|
||||
target="_blank"
|
||||
className="size-max scale-90 p-0 text-xs text-muted-foreground"
|
||||
>
|
||||
<span>? Hilfe</span>
|
||||
<span>Ansehen</span>
|
||||
</Link>
|
||||
</div>
|
||||
<FormField
|
||||
@ -187,6 +189,7 @@ export default ({ server_article }: { server_article: Article }) => {
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<CategorySelect
|
||||
className="w-full"
|
||||
initialValue={field.value}
|
||||
onSelect={(categoryId) => {
|
||||
field.onChange(categoryId);
|
||||
|
||||
@ -7,6 +7,7 @@ import ArticleCard from "../article-card";
|
||||
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar";
|
||||
import { Article } from "@/server/db/schema";
|
||||
|
||||
function InfiniteArticlesGrid() {
|
||||
const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
|
||||
@ -19,6 +20,9 @@ function InfiniteArticlesGrid() {
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
// staleTime: 60 * 4 * 1000, // 4 minutes stale time
|
||||
refetchOnMount: false, // Prevents unnecessary refetching
|
||||
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
|
||||
},
|
||||
);
|
||||
// Calculate all visible items across all loaded pages
|
||||
@ -43,7 +47,7 @@ function InfiniteArticlesGrid() {
|
||||
{data?.pages?.length
|
||||
? allItems.map((article, idx) => (
|
||||
<li key={`article-${idx}`}>
|
||||
<ArticleCard {...article} />
|
||||
<ArticleCard {...(article as Article)} />
|
||||
</li>
|
||||
))
|
||||
: null}
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
function RenderArticle({ content }: { content: string }) {
|
||||
return "render article: in work"; //<Editor readOnly editorProviderProps={{ content: content }} />;
|
||||
}
|
||||
|
||||
export default RenderArticle;
|
||||
@ -5,7 +5,7 @@ import { Combobox, ComboboxProps } from "../combobox";
|
||||
import { Icons } from "../icons";
|
||||
|
||||
function CategorySelect(props: Partial<ComboboxProps>) {
|
||||
const { data: categories } = api.category.getAll.useQuery();
|
||||
const { data: categories } = api.category.getMany.useQuery();
|
||||
return (
|
||||
<Combobox
|
||||
{...(props as ComboboxProps)}
|
||||
@ -16,7 +16,8 @@ function CategorySelect(props: Partial<ComboboxProps>) {
|
||||
empty: "Keine Kategorien gefunden",
|
||||
}}
|
||||
data={
|
||||
categories?.map(({ name, id }) => ({ label: name!, value: id! })) ?? []
|
||||
categories?.map(({ name, slug }) => ({ label: name, value: slug })) ??
|
||||
[]
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -9,8 +9,8 @@ export const CATEGORY_GRID_CLASS =
|
||||
function CategoryGrid({ categories }: { categories: Category[] }) {
|
||||
return (
|
||||
<menu className={CATEGORY_GRID_CLASS}>
|
||||
{categories.map((category) => (
|
||||
<li key={category.id}>
|
||||
{categories.map((category, idx) => (
|
||||
<li key={idx}>
|
||||
<CategoryCard {...category} />
|
||||
</li>
|
||||
))}
|
||||
|
||||
@ -18,6 +18,9 @@ export default function InfiniteCategoryGrid() {
|
||||
},
|
||||
{
|
||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||
staleTime: 60 * 4 * 1000, // 4 minutes stale time
|
||||
refetchOnMount: false, // Prevents unnecessary refetching
|
||||
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
|
||||
},
|
||||
);
|
||||
// Calculate all visible items across all loaded pages
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import ColorPicker, { type ColorPickerProps } from ".";
|
||||
import { Button } from "../ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||
|
||||
export default function ColorPickerPopover(props: ColorPickerProps) {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button style={{ backgroundColor: props.initialColor }} className="">
|
||||
color
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<ColorPicker {...props} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
.color-picker .react-colorful {
|
||||
width: 100%;
|
||||
height: 240px;
|
||||
}
|
||||
.color-picker .react-colorful__saturation {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
.color-picker .react-colorful__hue {
|
||||
height: 40px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
.color-picker .react-colorful__hue-pointer {
|
||||
width: 12px;
|
||||
height: inherit;
|
||||
border-radius: 0;
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
"use client";
|
||||
import "./color-picker.css";
|
||||
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { Input } from "../ui/input";
|
||||
import { debounce } from "@/lib/utils";
|
||||
|
||||
const STORAGE_KEY = "savedColors";
|
||||
const MAX_COLORS = 12;
|
||||
const SELECT_DEBOUNCE = 500;
|
||||
|
||||
export type ColorPickerProps = {
|
||||
onInput?: (color: string) => void;
|
||||
initialColor?: string;
|
||||
};
|
||||
|
||||
function ColorPicker({ onInput, initialColor }: ColorPickerProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [customColors, setCustomColors] = useState<string[]>([]);
|
||||
const [color, setColor] = useState(initialColor ?? "#ff0000");
|
||||
|
||||
// Load colors from localStorage on mount
|
||||
useEffect(() => {
|
||||
if (initialColor?.length) setColor(initialColor);
|
||||
const storageColors = localStorage.getItem(STORAGE_KEY);
|
||||
const storedColors = storageColors ? JSON.parse(storageColors) : [];
|
||||
if (storedColors.length) {
|
||||
setCustomColors(storedColors);
|
||||
if (!initialColor?.length) setColor(storedColors[0]);
|
||||
setMounted(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const persistColor = (newColor: string) => {
|
||||
if (newColor.length < 2) return;
|
||||
if (customColors[0] === newColor) return; // Prevent duplicate consecutive colors
|
||||
|
||||
const updatedColors = [
|
||||
newColor,
|
||||
...customColors.filter((c) => c !== newColor),
|
||||
].slice(0, MAX_COLORS);
|
||||
console.log(updatedColors);
|
||||
|
||||
setCustomColors(updatedColors);
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedColors));
|
||||
};
|
||||
|
||||
const selectColor = (newColor: string) => {
|
||||
persistColor(newColor);
|
||||
onInput?.(newColor);
|
||||
};
|
||||
|
||||
const selectColorDebounced = debounce((newColor: string) => {
|
||||
selectColor(newColor);
|
||||
}, SELECT_DEBOUNCE);
|
||||
|
||||
const handleColorChange = (newColor: string, skipDebounce = false) => {
|
||||
setColor(newColor);
|
||||
if (skipDebounce)
|
||||
selectColor(newColor); // Delayed save
|
||||
else selectColorDebounced(newColor);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="color-picker flex flex-col items-center gap-4">
|
||||
{/* React Colorful Picker */}
|
||||
<HexColorPicker
|
||||
color={color}
|
||||
onChange={handleColorChange}
|
||||
className="w-full"
|
||||
/>
|
||||
|
||||
{/* Default Input (Native Color Picker) */}
|
||||
<div className="flex w-full gap-2">
|
||||
<div
|
||||
className="size-8 rounded-md border"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<Input
|
||||
className="h-8"
|
||||
value={color}
|
||||
onInput={(e) => handleColorChange(e.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Display Recent Colors */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{customColors.map((col) => (
|
||||
<button
|
||||
key={col}
|
||||
className="size-8 rounded border"
|
||||
style={{ backgroundColor: col }}
|
||||
onClick={() => handleColorChange(col, true)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ColorPicker;
|
||||
@ -50,6 +50,7 @@ export function Combobox({
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [value, setValue] = React.useState(initialValue ?? "");
|
||||
const selectedItem = data.find((item) => item.value === initialValue)!;
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
|
||||
@ -36,7 +36,6 @@ export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
"use no memo";
|
||||
const [pagination, setPagination] = React.useState<PaginationState>({
|
||||
pageSize: 25,
|
||||
pageIndex: 0,
|
||||
|
||||
@ -18,15 +18,15 @@ import {
|
||||
Youtube,
|
||||
} from "novel";
|
||||
import LinkPreview from "./link-preview";
|
||||
import { Heading } from "@tiptap/extension-heading";
|
||||
import { cx } from "class-variance-authority";
|
||||
import { slashCommand } from "./slash-commands";
|
||||
import ImageResize from "tiptap-extension-resize-image";
|
||||
|
||||
const placeholder = Placeholder;
|
||||
const tiptapLink = TiptapLink.configure({
|
||||
HTMLAttributes: {
|
||||
class: cx(
|
||||
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
|
||||
"underline text-foreground underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
|
||||
),
|
||||
},
|
||||
});
|
||||
@ -46,6 +46,12 @@ const tiptapImage = TiptapImage.extend({
|
||||
},
|
||||
});
|
||||
|
||||
const resizeImage = ImageResize.configure({
|
||||
HTMLAttributes: {
|
||||
class: cx("rounded-lg border border-muted"),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedImage = UpdatedImage.configure({
|
||||
HTMLAttributes: {
|
||||
class: cx("rounded-lg border border-muted"),
|
||||
@ -108,6 +114,7 @@ export const defaultExtensions = [
|
||||
tiptapLink,
|
||||
tiptapImage,
|
||||
updatedImage,
|
||||
resizeImage,
|
||||
taskList,
|
||||
taskItem,
|
||||
horizontalRule,
|
||||
|
||||
@ -28,7 +28,7 @@ const InputLink = ({ onSubmit }: { onSubmit: (link: string) => void }) => {
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex gap-2 " autoFocus>
|
||||
<form onSubmit={handleSubmit} className="flex gap-2" autoFocus>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
autoFocus
|
||||
@ -36,7 +36,7 @@ const InputLink = ({ onSubmit }: { onSubmit: (link: string) => void }) => {
|
||||
placeholder="Enter a link"
|
||||
value={link}
|
||||
onChange={(e) => setLink(e.target.value)}
|
||||
className="flex-1 focus-visible:ring-transparent border-0"
|
||||
className="flex-1 border-0 focus-visible:ring-transparent"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
@ -60,29 +60,31 @@ const Preview = ({
|
||||
_description?.length > 100
|
||||
? `${_description?.slice(0, 150)}...`
|
||||
: _description;
|
||||
|
||||
|
||||
return (
|
||||
<a href={href} target="_blank">
|
||||
<div className=" flex gap-4 flex-col-reverse md:flex-row">
|
||||
<div className="w-full space-y-2">
|
||||
<h2
|
||||
className="text-xl"
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "1.5rem",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-sm mt-2">{description}</p>
|
||||
<span className="text-xs text-muted-foreground ">{href}</span>
|
||||
<div className="flex flex-col-reverse gap-4 md:flex-row">
|
||||
<div className="flex w-full flex-col justify-between">
|
||||
<div className="w-full space-y-2">
|
||||
<h2
|
||||
className="text-xl"
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: "1.5rem",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-2 text-sm">{description}</p>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">{href}</span>
|
||||
</div>
|
||||
|
||||
{image?.length ? (
|
||||
<img
|
||||
src={image}
|
||||
alt={title}
|
||||
className="w-full max-w-40 rounded-md object-cover "
|
||||
className="w-full max-w-40 rounded-md object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-20 rounded-md bg-muted" />
|
||||
@ -98,13 +100,13 @@ export const LinkPreviewComponent: React.FC<NodeViewProps> = ({
|
||||
extension,
|
||||
}) => {
|
||||
const [preview, setPreview] = React.useState<LinkPreviewData | undefined>(
|
||||
(node.attrs?.href?.length && (node.attrs as LinkPreviewData)) ?? undefined
|
||||
(node.attrs?.href?.length && (node.attrs as LinkPreviewData)) ?? undefined,
|
||||
);
|
||||
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper as="div" className="p-4 rounded-md bg-background border ">
|
||||
<NodeViewWrapper as="div" className="rounded-md border bg-background p-4">
|
||||
{loading ? (
|
||||
<Skeleton className="h-8 w-full rounded" />
|
||||
) : preview ? (
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
|
||||
import { Command, renderItems, createSuggestionItems } from "novel";
|
||||
import { selectionItems } from "../../selector/selection-items";
|
||||
import { uploadFile } from "@/server/actions/image";
|
||||
|
||||
// const items = selectionItems.filter((item) => !item.inline);
|
||||
// const defaultSuggestionItems = items.map((item) => (
|
||||
@ -148,10 +149,19 @@ export const suggestionItems = createSuggestionItems([
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.onchange = async () => {
|
||||
if (input.files?.length) {
|
||||
if (input.files?.[0]) {
|
||||
const file = input.files[0];
|
||||
const pos = editor.view.state.selection.from;
|
||||
// uploadFn(file, editor.view, pos);
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const url = await uploadFile(formData);
|
||||
console.log("URL", url);
|
||||
if (!url) return;
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.setImage({ src: url, alt: file.name, title: file.name })
|
||||
.run();
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
|
||||
@ -14,23 +14,26 @@ import { MenuBar } from "./menu/menu-bar";
|
||||
const Editor = ({
|
||||
onContentChange,
|
||||
initialContent,
|
||||
readOnly,
|
||||
}: {
|
||||
initialContent: JSONContent | null;
|
||||
onContentChange: (content: JSONContent) => void;
|
||||
onContentChange?: (content: JSONContent) => void;
|
||||
readOnly?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<EditorRoot>
|
||||
<EditorContent
|
||||
slotBefore={<MenuBar />}
|
||||
slotBefore={!readOnly && <MenuBar />}
|
||||
extensions={defaultExtensions}
|
||||
editorProps={{
|
||||
handleDOMEvents: {
|
||||
keydown: (_view, event) => handleCommandNavigation(event),
|
||||
},
|
||||
}}
|
||||
editable={!readOnly}
|
||||
initialContent={initialContent ?? { type: "doc" }}
|
||||
onUpdate={({ editor }) => {
|
||||
onContentChange(editor.getJSON());
|
||||
onContentChange?.(editor.getJSON());
|
||||
}}
|
||||
>
|
||||
<SlashCommandComponent />
|
||||
|
||||
11
src/components/editor/render-content.tsx
Normal file
11
src/components/editor/render-content.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
import dynamic from "next/dynamic";
|
||||
import { JSONContent } from "novel";
|
||||
import React from "react";
|
||||
const Editor = dynamic(() => import("."), { ssr: false });
|
||||
|
||||
function RenderContent({ content }: { content: JSONContent }) {
|
||||
return <Editor readOnly initialContent={content} />;
|
||||
}
|
||||
|
||||
export default RenderContent;
|
||||
@ -15,80 +15,43 @@ export interface BubbleColorMenuItem {
|
||||
const TEXT_COLORS: BubbleColorMenuItem[] = [
|
||||
{
|
||||
name: "Default",
|
||||
color: "var(--novel-black)",
|
||||
color: "var(--foreground)",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
color: "#9333EA",
|
||||
color: "var(--color-purple-600)",
|
||||
},
|
||||
{
|
||||
name: "Red",
|
||||
color: "#E00000",
|
||||
color: "var(--color-red-600)",
|
||||
},
|
||||
{
|
||||
name: "Yellow",
|
||||
color: "#EAB308",
|
||||
color: "var(--color-yellow-600)",
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
color: "#2563EB",
|
||||
color: "var(--color-blue-600)",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
color: "#008A00",
|
||||
color: "var(--color-emerald-600)",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
color: "#FFA500",
|
||||
color: "var(--color-orange-600)",
|
||||
},
|
||||
{
|
||||
name: "Pink",
|
||||
color: "#BA4081",
|
||||
color: "var(--color-pink-600)",
|
||||
},
|
||||
{
|
||||
name: "Gray",
|
||||
color: "#A8A29E",
|
||||
color: "var(--color-gray-600)",
|
||||
},
|
||||
];
|
||||
|
||||
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [
|
||||
{
|
||||
name: "Default",
|
||||
color: "var(--novel-highlight-default)",
|
||||
},
|
||||
{
|
||||
name: "Purple",
|
||||
color: "var(--novel-highlight-purple)",
|
||||
},
|
||||
{
|
||||
name: "Red",
|
||||
color: "var(--novel-highlight-red)",
|
||||
},
|
||||
{
|
||||
name: "Yellow",
|
||||
color: "var(--novel-highlight-yellow)",
|
||||
},
|
||||
{
|
||||
name: "Blue",
|
||||
color: "var(--novel-highlight-blue)",
|
||||
},
|
||||
{
|
||||
name: "Green",
|
||||
color: "var(--novel-highlight-green)",
|
||||
},
|
||||
{
|
||||
name: "Orange",
|
||||
color: "var(--novel-highlight-orange)",
|
||||
},
|
||||
{
|
||||
name: "Pink",
|
||||
color: "var(--novel-highlight-pink)",
|
||||
},
|
||||
{
|
||||
name: "Gray",
|
||||
color: "var(--novel-highlight-gray)",
|
||||
},
|
||||
];
|
||||
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = TEXT_COLORS;
|
||||
|
||||
interface ColorSelectorProps {
|
||||
open: boolean;
|
||||
@ -100,11 +63,11 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {
|
||||
|
||||
if (!editor) return null;
|
||||
const activeColorItem = TEXT_COLORS.find(({ color }) =>
|
||||
editor.isActive("textStyle", { color })
|
||||
editor.isActive("textStyle", { color }),
|
||||
);
|
||||
|
||||
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
|
||||
editor.isActive("highlight", { color })
|
||||
editor.isActive("highlight", { color }),
|
||||
);
|
||||
|
||||
return (
|
||||
@ -126,7 +89,7 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {
|
||||
|
||||
<PopoverContent
|
||||
sideOffset={5}
|
||||
className="my-1 flex max-h-80 w-48 flex-col overflow-hidden overflow-y-auto rounded border p-1 shadow-xl "
|
||||
className="my-1 flex max-h-80 w-48 flex-col overflow-hidden overflow-y-auto rounded border p-1 shadow-xl"
|
||||
align="start"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
|
||||
@ -9,12 +9,11 @@ import {
|
||||
Heading3,
|
||||
Heading4,
|
||||
ListOrdered,
|
||||
TextIcon,
|
||||
TextQuote,
|
||||
SeparatorHorizontalIcon,
|
||||
Heading5,
|
||||
Heading6,
|
||||
QuoteIcon,
|
||||
TypeIcon,
|
||||
} from "lucide-react";
|
||||
import { EditorInstance } from "novel";
|
||||
|
||||
@ -64,7 +63,7 @@ export const selectionItems: SelectorItem[] = [
|
||||
// blocks
|
||||
{
|
||||
name: "Text",
|
||||
icon: TextIcon,
|
||||
icon: TypeIcon,
|
||||
command: (editor) => editor.chain().focus().clearNodes().run(),
|
||||
isActive: (editor) => editor.isActive("paragraph"), //node?.type?.name === "paragraph",
|
||||
},
|
||||
|
||||
@ -8,9 +8,6 @@ const items = selectionItems.filter((item) => item.inline);
|
||||
export const TextButtons = () => {
|
||||
const { editor } = useEditor();
|
||||
if (!editor) return null;
|
||||
editor.on("selectionUpdate", () => {
|
||||
editor.view.dispatch(editor.state.tr);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
|
||||
@ -72,36 +72,40 @@
|
||||
}
|
||||
|
||||
.tiptap hr {
|
||||
@apply border-border;
|
||||
/* border-color: var(--border); */
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #5abbf7;
|
||||
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.menu-bar button {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
.menu-bar .is-active {
|
||||
border-color: var(--primary-color);
|
||||
img {
|
||||
@apply rounded-md;
|
||||
}
|
||||
|
||||
/* Task List */
|
||||
ul[data-type="taskList"] li {
|
||||
@apply m-0 my-3 flex items-center;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li p {
|
||||
@apply m-0 my-0 flex items-center;
|
||||
}
|
||||
|
||||
ul[data-type="taskList"] li > label input[type="checkbox"] {
|
||||
@apply size-4 border border-border bg-muted;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
border: 2px solid var(--border);
|
||||
margin-right: 0.3rem;
|
||||
|
||||
display: grid;
|
||||
border-radius: 0.25rem;
|
||||
place-content: center;
|
||||
@ -126,7 +130,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:checked::before {
|
||||
transform: scale(1);
|
||||
}
|
||||
ul[data-type="taskList"] li[data-checked="true"] > div > p {
|
||||
color: var(--muted-foreground);
|
||||
@apply text-foreground/75;
|
||||
text-decoration: line-through;
|
||||
text-decoration-thickness: 2px;
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export type AppConfig = {
|
||||
name: string;
|
||||
description: string;
|
||||
socials: {
|
||||
discord: string;
|
||||
};
|
||||
@ -7,6 +8,7 @@ export type AppConfig = {
|
||||
|
||||
export const appConfig: AppConfig = {
|
||||
name: "Logipedia",
|
||||
description: "Logipedia ist Wissen",
|
||||
|
||||
socials: {
|
||||
discord: "https://discord.com",
|
||||
|
||||
@ -22,6 +22,7 @@ export type AppRoutes = {
|
||||
|
||||
// Auth routes
|
||||
signin: string;
|
||||
signup: string;
|
||||
signout: string;
|
||||
profile: string;
|
||||
};
|
||||
@ -43,6 +44,7 @@ export const appRoutes: AppRoutes = {
|
||||
|
||||
// auth
|
||||
signin: "/api/auth/signin",
|
||||
signup: "/api/auth/signin",
|
||||
signout: "/api/auth/signout",
|
||||
profile: "/me",
|
||||
};
|
||||
|
||||
@ -13,6 +13,8 @@ export const env = createEnv({
|
||||
: z.string().optional(),
|
||||
AUTH_DISCORD_ID: z.string(),
|
||||
AUTH_DISCORD_SECRET: z.string(),
|
||||
AUTH_GOOGLE_ID: z.string(),
|
||||
AUTH_GOOGLE_SECRET: z.string(),
|
||||
DATABASE_URL: z.string().url(),
|
||||
NODE_ENV: z
|
||||
.enum(["development", "test", "production"])
|
||||
@ -36,6 +38,8 @@ export const env = createEnv({
|
||||
AUTH_SECRET: process.env.AUTH_SECRET,
|
||||
AUTH_DISCORD_ID: process.env.AUTH_DISCORD_ID,
|
||||
AUTH_DISCORD_SECRET: process.env.AUTH_DISCORD_SECRET,
|
||||
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
|
||||
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
},
|
||||
|
||||
27
src/lib/logger.ts
Normal file
27
src/lib/logger.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import winston from "winston";
|
||||
const customLevels = {
|
||||
levels: {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
sql: 3,
|
||||
debug: 4,
|
||||
},
|
||||
colors: {
|
||||
error: "red",
|
||||
warn: "yellow",
|
||||
info: "green",
|
||||
sql: "magenta",
|
||||
debug: "blue",
|
||||
},
|
||||
};
|
||||
winston.addColors(customLevels.colors);
|
||||
|
||||
export const logger = winston.createLogger({
|
||||
levels: customLevels.levels,
|
||||
format: winston.format.json(),
|
||||
transports: [
|
||||
new winston.transports.File({ filename: "db_queries.log", level: "sql" }),
|
||||
new winston.transports.File({ filename: "errors.log", level: "error" }),
|
||||
],
|
||||
});
|
||||
@ -1,7 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const userProfileSchema = z.object({
|
||||
export const userSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
// image: z.string().optional(),
|
||||
// email: z.string().email(),
|
||||
email: z.string().email(),
|
||||
image: z.string().optional(),
|
||||
});
|
||||
|
||||
export const passwordSchema = z.string().min(8, {
|
||||
message: "Passwort muss mindestens 8 Zeichen lang sein",
|
||||
});
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export const registerSchema = z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
password: passwordSchema,
|
||||
confirmPassword: z.string(),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwörter stimmen nicht überein",
|
||||
path: ["confirmPassword"],
|
||||
});
|
||||
|
||||
@ -28,6 +28,8 @@ export async function updateArticle(
|
||||
article: { ...article, content: JSON.parse(article.content) },
|
||||
articleId,
|
||||
});
|
||||
revalidatePath("/");
|
||||
|
||||
// if (!result[0]?.id?.length) return false;
|
||||
// return revalidatePath(`/artikel/${result[0]?.slug}/edit`);
|
||||
}
|
||||
@ -35,5 +37,7 @@ export async function deleteArticle(articleId: string) {
|
||||
const result = await api.article.delete({
|
||||
articleId,
|
||||
});
|
||||
revalidatePath("/");
|
||||
|
||||
// if (!result[0]?.id?.length) return false;
|
||||
}
|
||||
|
||||
7
src/server/actions/auth.ts
Normal file
7
src/server/actions/auth.ts
Normal file
@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { signIn } from "@/server/auth";
|
||||
|
||||
export async function loginOAuth(provider: string) {
|
||||
return await signIn(provider);
|
||||
}
|
||||
@ -1,17 +1,30 @@
|
||||
"use server";
|
||||
import fs from "node:fs/promises";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { auth } from "../auth";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
|
||||
export async function uploadFile(formData: FormData) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session || !hasPermission(session.user.role, Role.EDITOR)) return false;
|
||||
console.log("Starting upload");
|
||||
|
||||
const file = formData.get("file") as File;
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const buffer = new Uint8Array(arrayBuffer);
|
||||
const filename = `upload-${Date.now()}-${file.name}.png`;
|
||||
console.log("Filename", filename);
|
||||
|
||||
try {
|
||||
await fs.writeFile(`./public/uploads/${filename}`, buffer);
|
||||
console.log("File uploaded successfully");
|
||||
|
||||
revalidatePath("/");
|
||||
|
||||
return `/uploads/${filename}`;
|
||||
} catch (e) {
|
||||
console.error("Error uploading file:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
"use server";
|
||||
|
||||
import { userProfileSchema } from "@/lib/validation/zod/user";
|
||||
import { userSchema } from "@/lib/validation/zod/user";
|
||||
import { api } from "@/trpc/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
|
||||
export async function updateUserProfile(
|
||||
profile: z.infer<typeof userProfileSchema>,
|
||||
) {
|
||||
export async function updateUserProfile(profile: z.infer<typeof userSchema>) {
|
||||
const [result] = await api.users.updateProfile({ profile });
|
||||
if (!result?.id) return { success: false };
|
||||
revalidatePath("/me");
|
||||
|
||||
@ -4,6 +4,7 @@ import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||
import { usersRouter } from "./routers/users";
|
||||
import { authorRouter } from "./routers/author";
|
||||
import { appRouter as globalRouter } from "./routers/app";
|
||||
import { authRouter } from "./routers/auth";
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
*
|
||||
@ -15,6 +16,7 @@ export const appRouter = createTRPCRouter({
|
||||
users: usersRouter,
|
||||
author: authorRouter,
|
||||
app: globalRouter,
|
||||
auth: authRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@ -1,29 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
import { desc, ilike, like } from "drizzle-orm";
|
||||
import {
|
||||
articles as articlesTable,
|
||||
categories as categoriesTable,
|
||||
lower,
|
||||
} from "@/server/db/schema";
|
||||
import { articles as articlesTable } from "@/server/db/schema";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
searchContent: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
query: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const articles = await ctx.db.query.articles.findMany({
|
||||
where: ilike(articlesTable.title, "%" + input.query + "%"),
|
||||
});
|
||||
const categories = await ctx.db.query.categories.findMany({
|
||||
where: like(categoriesTable.name, "%" + input.query + "%"),
|
||||
});
|
||||
return { articles, categories };
|
||||
}),
|
||||
|
||||
getSidebarMain: publicProcedure.query(async ({ ctx }) => {
|
||||
const categories = await ctx.db.query.categories.findMany({
|
||||
limit: 3,
|
||||
@ -34,6 +13,8 @@ export const appRouter = createTRPCRouter({
|
||||
});
|
||||
const articles = await ctx.db.query.articles.findMany({
|
||||
limit: 3,
|
||||
where: eq(articlesTable.published, true),
|
||||
orderBy: desc(articlesTable.createdAt),
|
||||
columns: {
|
||||
slug: true,
|
||||
title: true,
|
||||
|
||||
@ -71,10 +71,13 @@ export const articleRouter = createTRPCRouter({
|
||||
get: publicProcedure
|
||||
.input(z.object({ slug: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await ctx.db.query.articles.findFirst({
|
||||
where: eq(articles.slug, input.slug),
|
||||
const user = ctx?.session?.user;
|
||||
const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
|
||||
const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
|
||||
return (await ctx.db.query.articles.findFirst({
|
||||
where: and(eq(articles.slug, input.slug), publishedArg),
|
||||
with: { category: true },
|
||||
});
|
||||
})) as Article;
|
||||
}),
|
||||
getByCursor: publicProcedure
|
||||
.input(
|
||||
@ -112,26 +115,24 @@ export const articleRouter = createTRPCRouter({
|
||||
sortConfig,
|
||||
cursorObj,
|
||||
);
|
||||
const user = ctx?.session?.user;
|
||||
const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
|
||||
const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
|
||||
|
||||
const items = await ctx.db.query.articles.findMany({
|
||||
where: and(
|
||||
cursorArg,
|
||||
categoryArg,
|
||||
queryFilterArg,
|
||||
eq(articles.published, true),
|
||||
),
|
||||
where: and(cursorArg, categoryArg, queryFilterArg, publishedArg),
|
||||
limit: limit + 1,
|
||||
orderBy,
|
||||
columns: {
|
||||
title: true,
|
||||
slug: true,
|
||||
createdAt: true,
|
||||
published: isEditor ? true : false,
|
||||
},
|
||||
});
|
||||
|
||||
let nextCursor: string | undefined = undefined;
|
||||
if (items.length > limit) {
|
||||
console.log("Configure next cursor");
|
||||
const cursorItem = items.pop();
|
||||
// Create a cursor object with the relevant fields for sorting
|
||||
const cursorData: ArticleCursor = {
|
||||
@ -150,7 +151,7 @@ export const articleRouter = createTRPCRouter({
|
||||
nextCursor,
|
||||
};
|
||||
}),
|
||||
getAll: publicProcedure
|
||||
getMany: publicProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
@ -160,11 +161,12 @@ export const articleRouter = createTRPCRouter({
|
||||
.optional(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const limit = input?.limit ?? 50;
|
||||
return (await ctx.db.query.articles.findMany({
|
||||
where: input?.categoryId
|
||||
? eq(articles.categoryId, input.categoryId)
|
||||
: undefined,
|
||||
limit: input?.limit,
|
||||
limit: limit,
|
||||
columns: {
|
||||
title: true,
|
||||
slug: true,
|
||||
|
||||
54
src/server/api/routers/auth.ts
Normal file
54
src/server/api/routers/auth.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { passwordSchema, userSchema } from "@/lib/validation/zod/user";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
import { z } from "zod";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { users } from "@/server/db/schema";
|
||||
import argon from "argon2";
|
||||
|
||||
export const authRouter = createTRPCRouter({
|
||||
register: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
user: userSchema,
|
||||
password: passwordSchema,
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const {
|
||||
password,
|
||||
user: { email, name },
|
||||
} = input;
|
||||
// Check if user already exists
|
||||
try {
|
||||
const existingUser = await ctx.db.query.users.findFirst({
|
||||
where: eq(users.email, email),
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return { success: false, message: "User already exists" };
|
||||
}
|
||||
|
||||
// Hash the password (12 is a good cost factor)
|
||||
const hashedPassword = await argon.hash(password);
|
||||
|
||||
// Create user in database
|
||||
const [user] = await ctx.db
|
||||
.insert(users)
|
||||
.values({
|
||||
name,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
})
|
||||
.returning({ id: users.id });
|
||||
console.log(user);
|
||||
|
||||
if (user) {
|
||||
return { success: true, message: "User created successfully" };
|
||||
}
|
||||
return { success: false, message: "Error creating user" };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { success: false, message: "Error creating user" };
|
||||
}
|
||||
}),
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
|
||||
import { createTRPCRouter, publicProcedure } from "../trpc";
|
||||
|
||||
export const authorRouter = createTRPCRouter({
|
||||
getCount: publicProcedure.query(async ({ ctx }) => {
|
||||
|
||||
@ -12,11 +12,11 @@ import {
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
gt,
|
||||
gte,
|
||||
ilike,
|
||||
like,
|
||||
lte,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { hasPermission, Role } from "@/lib/validation/permissions";
|
||||
import {
|
||||
@ -60,13 +60,6 @@ const getCategorySorting = (sort: string, cursor?: CategoryCursor) => {
|
||||
};
|
||||
|
||||
export const categoryRouter = createTRPCRouter({
|
||||
search: publicProcedure
|
||||
.input(z.object({ query: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await ctx.db.query.categories.findMany({
|
||||
where: like(categories.name, "%" + input.query + "%"),
|
||||
});
|
||||
}),
|
||||
get: publicProcedure
|
||||
.input(z.object({ slug: z.string(), with: z.any() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
@ -76,7 +69,7 @@ export const categoryRouter = createTRPCRouter({
|
||||
})) as Category;
|
||||
}),
|
||||
|
||||
getAll: publicProcedure
|
||||
getMany: publicProcedure
|
||||
.input(
|
||||
z
|
||||
.object({
|
||||
@ -85,9 +78,15 @@ export const categoryRouter = createTRPCRouter({
|
||||
.optional(),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await ctx.db.query.categories.findMany({
|
||||
limit: input?.limit,
|
||||
});
|
||||
const limit = input?.limit ?? 50;
|
||||
return (await ctx.db.query.categories.findMany({
|
||||
limit: limit,
|
||||
columns: {
|
||||
name: true,
|
||||
slug: true,
|
||||
createdAt: true,
|
||||
},
|
||||
})) as Category[];
|
||||
}),
|
||||
|
||||
getByCursor: publicProcedure
|
||||
@ -135,7 +134,6 @@ export const categoryRouter = createTRPCRouter({
|
||||
|
||||
let nextCursor: string | undefined = undefined;
|
||||
if (items.length > limit) {
|
||||
console.log("Configure next cursor");
|
||||
const cursorItem = items.pop();
|
||||
// Create a cursor object with the relevant fields for sorting
|
||||
const cursorData: CategoryCursor = {
|
||||
@ -172,6 +170,12 @@ export const categoryRouter = createTRPCRouter({
|
||||
.values({ ...input.category, slug })
|
||||
.returning({
|
||||
slug: categories.slug,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: categories.slug,
|
||||
set: {
|
||||
slug: sql`${slug} || '-' || (SELECT COUNT(*) FROM ${categories} WHERE slug LIKE ${slug + "-%"})`,
|
||||
},
|
||||
});
|
||||
}),
|
||||
update: protectedProcedure
|
||||
|
||||
@ -3,17 +3,17 @@ import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import { z } from "zod";
|
||||
import { users } from "@/server/db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { userProfileSchema } from "@/lib/validation/zod/user";
|
||||
import { userSchema } from "@/lib/validation/zod/user";
|
||||
|
||||
export const usersRouter = createTRPCRouter({
|
||||
updateProfile: protectedProcedure
|
||||
.input(z.object({ profile: userProfileSchema }))
|
||||
.input(z.object({ profile: userSchema }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return await ctx.db
|
||||
.update(users)
|
||||
.set(input.profile)
|
||||
.where(eq(users.id, ctx.session.user.id))
|
||||
.returning();
|
||||
.returning({ id: users.id });
|
||||
}),
|
||||
|
||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
||||
@ -21,6 +21,12 @@ export const usersRouter = createTRPCRouter({
|
||||
if (!isAdmin) throw new Error("You are not allowed to get all users");
|
||||
return await ctx.db.query.users.findMany({
|
||||
orderBy: desc(users.role),
|
||||
columns: {
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
|
||||
@ -89,6 +89,7 @@ const timingMiddleware = t.middleware(async ({ next, path }) => {
|
||||
|
||||
if (t._config.isDev) {
|
||||
// artificial delay in dev
|
||||
// const waitMs = 1000 * 5; // 5 seconds
|
||||
const waitMs = Math.floor(Math.random() * 400) + 100;
|
||||
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
||||
import { type DefaultSession, type NextAuthConfig } from "next-auth";
|
||||
import DiscordProvider from "next-auth/providers/discord";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
|
||||
import { db } from "@/server/db";
|
||||
import {
|
||||
@ -37,9 +38,18 @@ declare module "next-auth" {
|
||||
*
|
||||
* @see https://next-auth.js.org/configuration/options
|
||||
*/
|
||||
|
||||
export const adapter = DrizzleAdapter(db, {
|
||||
usersTable: users,
|
||||
accountsTable: accounts,
|
||||
sessionsTable: sessions,
|
||||
verificationTokensTable: verificationTokens,
|
||||
}) as Adapter;
|
||||
|
||||
export const authConfig = {
|
||||
providers: [
|
||||
DiscordProvider,
|
||||
GoogleProvider,
|
||||
/**
|
||||
* ...add more providers here.
|
||||
*
|
||||
@ -50,19 +60,23 @@ export const authConfig = {
|
||||
* @see https://next-auth.js.org/providers/github
|
||||
*/
|
||||
],
|
||||
adapter: DrizzleAdapter(db, {
|
||||
usersTable: users,
|
||||
accountsTable: accounts,
|
||||
sessionsTable: sessions,
|
||||
verificationTokensTable: verificationTokens,
|
||||
}) as Adapter,
|
||||
// pages: {
|
||||
// signIn: appRoutes.signin, // Custom sign in page
|
||||
// },
|
||||
adapter,
|
||||
callbacks: {
|
||||
session: ({ session, user }) => ({
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
},
|
||||
}),
|
||||
session: ({ session, user }) => {
|
||||
return {
|
||||
...session,
|
||||
user: {
|
||||
...session.user,
|
||||
id: user.id,
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
session: {
|
||||
strategy: "database",
|
||||
maxAge: 60 * 60 * 24 * 7, // 7 days,
|
||||
},
|
||||
} satisfies NextAuthConfig;
|
||||
|
||||
436
src/server/auth/credentials-provider.ts
Normal file
436
src/server/auth/credentials-provider.ts
Normal file
@ -0,0 +1,436 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
|
||||
export function generateSessionToken() {
|
||||
return createId();
|
||||
}
|
||||
export const fromDate = (time: number, date = Date.now()) => {
|
||||
return new Date(date + time * 1000);
|
||||
};
|
||||
|
||||
// CredentialsProvider({
|
||||
// name: "Credentials",
|
||||
// credentials: {
|
||||
// email: { label: "Email", type: "email" },
|
||||
// password: { label: "Password", type: "password" },
|
||||
// },
|
||||
// authorize: async (credentials) => {
|
||||
// let user: Session["user"] | null = null;
|
||||
|
||||
// if (!credentials?.email || !credentials?.password) return null;
|
||||
|
||||
// if (
|
||||
// typeof credentials.password !== "string" ||
|
||||
// typeof credentials.email !== "string"
|
||||
// ) {
|
||||
// console.log("WARN: Password or Email is not a string.");
|
||||
// return null;
|
||||
// }
|
||||
// try {
|
||||
// // Add your own database logic here
|
||||
// const response = await db.query.users.findFirst({
|
||||
// where: eq(users.email, String(credentials.email)),
|
||||
// });
|
||||
|
||||
// // No user found
|
||||
// if (!response || !response.password) {
|
||||
// if (!response?.password) return null;
|
||||
// }
|
||||
// user = response;
|
||||
// // Check password - using timing-safe comparison via bcrypt
|
||||
// const isValidPassword = await argon.verify(
|
||||
// String(response.password),
|
||||
// String(credentials?.password),
|
||||
// );
|
||||
// if (!isValidPassword) return null;
|
||||
// console.log("User authenticated successfully:", user.id);
|
||||
|
||||
// return {
|
||||
// name: user.name,
|
||||
// email: user.email,
|
||||
// image: user.image,
|
||||
// id: user.id,
|
||||
// role: user.role,
|
||||
// } as User;
|
||||
// } catch (e) {
|
||||
// console.log("WARN: Error while validating credentials.");
|
||||
// return null;
|
||||
// }
|
||||
// },
|
||||
// }),
|
||||
|
||||
// callback
|
||||
// async signIn({ user, account, profile, email, credentials }) {
|
||||
// console.log("👉 SignIn callback triggered", user?.id);
|
||||
// console.log("👉 SignIn callback credentials", credentials);
|
||||
// if (credentials && user.id) await createSession(user.id!);
|
||||
// // Return true to allow sign-in
|
||||
// return true;
|
||||
// },
|
||||
|
||||
// server action
|
||||
// export async function login(values: z.infer<typeof loginSchema>) {
|
||||
// return await signIn("credentials", values);
|
||||
// }
|
||||
// export async function register(values: z.infer<typeof registerSchema>) {
|
||||
// try {
|
||||
// const { success } = await api.auth.register({
|
||||
// user: {
|
||||
// email: values.email,
|
||||
// name: values.name,
|
||||
// },
|
||||
// password: values.password,
|
||||
// });
|
||||
// await signIn("credentials", {
|
||||
// email: values.email,
|
||||
// password: values.password,
|
||||
// });
|
||||
// return success;
|
||||
// } catch (e) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// export async function createSession(userId: string) {
|
||||
// if (!adapter.createSession) throw new Error("Adapter not initialized");
|
||||
// const sessionToken = generateSessionToken();
|
||||
// const sessionExpiry = fromDate(authConfig.session.maxAge);
|
||||
// console.log("👉 createSession", sessionToken);
|
||||
// const session = await adapter.createSession({
|
||||
// sessionToken: sessionToken,
|
||||
// userId: userId,
|
||||
// expires: sessionExpiry,
|
||||
// });
|
||||
// console.log("👉 createSession session", session);
|
||||
// const cookieStore = await cookies();
|
||||
// cookieStore.set("authjs.session-token", sessionToken, {
|
||||
// expires: sessionExpiry,
|
||||
// });
|
||||
// }
|
||||
|
||||
// register page
|
||||
// "use client";
|
||||
// import { cn } from "@/lib/utils";
|
||||
// import { Button } from "@/components/ui/button";
|
||||
// import { Card, CardContent } from "@/components/ui/card";
|
||||
// import { Input } from "@/components/ui/input";
|
||||
// import { zodResolver } from "@hookform/resolvers/zod";
|
||||
// import { useForm } from "react-hook-form";
|
||||
// import { z } from "zod";
|
||||
|
||||
// import {
|
||||
// Form,
|
||||
// FormControl,
|
||||
// FormField,
|
||||
// FormItem,
|
||||
// FormLabel,
|
||||
// FormMessage,
|
||||
// } from "@/components/ui/form";
|
||||
|
||||
// import { registerSchema } from "@/lib/validation/zod/user";
|
||||
// import { appConfig, appRoutes } from "@/config";
|
||||
// import Link from "next/link";
|
||||
// import { login, register } from "@/server/actions/auth";
|
||||
// import { toast } from "sonner";
|
||||
// import { AuthProviderList, LeagalFooter } from ".";
|
||||
|
||||
// export function RegisterForm({
|
||||
// className,
|
||||
// ...props
|
||||
// }: React.ComponentProps<"div">) {
|
||||
// const form = useForm<z.infer<typeof registerSchema>>({
|
||||
// resolver: zodResolver(registerSchema),
|
||||
// defaultValues: {
|
||||
// name: "",
|
||||
// email: "",
|
||||
// password: "",
|
||||
// confirmPassword: "",
|
||||
// },
|
||||
// });
|
||||
|
||||
// // 2. Define a submit handler.
|
||||
// async function onSubmit(values: z.infer<typeof registerSchema>) {
|
||||
// const success = await register(values);
|
||||
// if (!success) toast.error("Registrierung fehlgeschlagen");
|
||||
// form.reset();
|
||||
// }
|
||||
// return (
|
||||
// <div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
// <Card className="overflow-hidden">
|
||||
// <CardContent className="grid p-0 md:grid-cols-2">
|
||||
// <Form {...form}>
|
||||
// <form onSubmit={form.handleSubmit(onSubmit)} className="p-6 md:p-8">
|
||||
// <div className="flex flex-col gap-6">
|
||||
// <div className="flex flex-col items-center text-center">
|
||||
// <h1 className="text-2xl font-bold">Wilkommen</h1>
|
||||
// <p className="text-balance text-muted-foreground">
|
||||
// Erstelle dein {appConfig.name} Konto
|
||||
// </p>
|
||||
// </div>
|
||||
|
||||
// <AuthProviderList />
|
||||
|
||||
// <div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
||||
// <span className="relative z-10 bg-background px-2 text-muted-foreground">
|
||||
// Oder mit
|
||||
// </span>
|
||||
// </div>
|
||||
|
||||
// <div className="space-y-4">
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="name"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Name</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// placeholder="Mustermax"
|
||||
// tabIndex={1}
|
||||
// {...field}
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="email"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Email</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// placeholder="email@beispiel.com"
|
||||
// tabIndex={2}
|
||||
// {...field}
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="password"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Passwort</FormLabel>
|
||||
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// type="password"
|
||||
// tabIndex={3}
|
||||
// placeholder="******"
|
||||
// {...field}
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="confirmPassword"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Passwort Wiederholen</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// type="password"
|
||||
// tabIndex={4}
|
||||
// placeholder="******"
|
||||
// {...field}
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// <Button type="submit">Login</Button>
|
||||
|
||||
// <div className="text-center text-sm">
|
||||
// Du hast bereits ein Konto?{" "}
|
||||
// <Link
|
||||
// href={appRoutes.signin}
|
||||
// className="underline underline-offset-4"
|
||||
// >
|
||||
// Anmelden
|
||||
// </Link>
|
||||
// </div>
|
||||
// </div>
|
||||
// </form>
|
||||
// </Form>
|
||||
|
||||
// <div className="relative hidden bg-muted md:block">
|
||||
// <img
|
||||
// src="/placeholder.svg"
|
||||
// alt="Image"
|
||||
// className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
// />
|
||||
// </div>
|
||||
// </CardContent>
|
||||
// </Card>
|
||||
// <LeagalFooter />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// Login page
|
||||
|
||||
// export function LoginForm({
|
||||
// className,
|
||||
// ...props
|
||||
// }: React.ComponentProps<"div">) {
|
||||
// const form = useForm<z.infer<typeof loginSchema>>({
|
||||
// resolver: zodResolver(loginSchema),
|
||||
// defaultValues: {
|
||||
// email: "",
|
||||
// password: "",
|
||||
// },
|
||||
// });
|
||||
|
||||
// // 2. Define a submit handler.
|
||||
// async function onSubmit(values: z.infer<typeof loginSchema>) {
|
||||
// const success = await login(values);
|
||||
// // if (!success) toast.error("Login fehlgeschlagen");
|
||||
// // form.reset();
|
||||
// }
|
||||
// return (
|
||||
// <div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||
// <Card className="overflow-hidden">
|
||||
// <CardContent className="grid p-0 md:grid-cols-2">
|
||||
// <Form {...form}>
|
||||
// <form onSubmit={form.handleSubmit(onSubmit)} className="p-6 md:p-8">
|
||||
// <div className="flex flex-col gap-6">
|
||||
// <div className="flex flex-col items-center text-center">
|
||||
// <h1 className="text-2xl font-bold">Wilkommen Zurück</h1>
|
||||
// <p className="text-balance text-muted-foreground">
|
||||
// Melde dich in deinem {appConfig.name} Konto an
|
||||
// </p>
|
||||
// </div>
|
||||
// <AuthProviderList />
|
||||
// <div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
||||
// <span className="relative z-10 bg-background px-2 text-muted-foreground">
|
||||
// Oder mit
|
||||
// </span>
|
||||
// </div>
|
||||
// <div className="space-y-4">
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="email"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <FormLabel>Email</FormLabel>
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// placeholder="email@beispiel.com"
|
||||
// tabIndex={1}
|
||||
// {...field}
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// <FormField
|
||||
// control={form.control}
|
||||
// name="password"
|
||||
// render={({ field }) => (
|
||||
// <FormItem>
|
||||
// <div className="flex items-center">
|
||||
// <FormLabel>Passwort</FormLabel>
|
||||
|
||||
// <a
|
||||
// href="#"
|
||||
// className="ml-auto text-sm underline-offset-2 hover:underline"
|
||||
// >
|
||||
// passwort vergessen?
|
||||
// </a>
|
||||
// </div>
|
||||
// <FormControl>
|
||||
// <Input
|
||||
// type="password"
|
||||
// tabIndex={2}
|
||||
// placeholder="******"
|
||||
// {...field}
|
||||
// />
|
||||
// </FormControl>
|
||||
// <FormMessage />
|
||||
// </FormItem>
|
||||
// )}
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// <Button type="submit">Login</Button>
|
||||
|
||||
// <div className="text-center text-sm">
|
||||
// Du hast noch kein Konto?{" "}
|
||||
// <Link
|
||||
// href={appRoutes.signup}
|
||||
// className="underline underline-offset-4"
|
||||
// >
|
||||
// Registrieren
|
||||
// </Link>
|
||||
// </div>
|
||||
// </div>
|
||||
// </form>
|
||||
// </Form>
|
||||
|
||||
// <div className="relative hidden bg-muted md:block">
|
||||
// <img
|
||||
// src="/placeholder.svg"
|
||||
// alt="Image"
|
||||
// className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
// />
|
||||
// </div>
|
||||
// </CardContent>
|
||||
// </Card>
|
||||
// <LeagalFooter />
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export function AuthProviderList() {
|
||||
// return (
|
||||
// <div className="grid grid-cols-3 gap-4">
|
||||
// <Button
|
||||
// variant="outline"
|
||||
// className="w-full"
|
||||
// onClick={() => loginOAuth("discord")}
|
||||
// >
|
||||
// <Icons.discord />
|
||||
// <span className="sr-only">Login with Discord</span>
|
||||
// </Button>
|
||||
// <Button variant="outline" className="w-full">
|
||||
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
// <path
|
||||
// d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"
|
||||
// fill="currentColor"
|
||||
// />
|
||||
// </svg>
|
||||
// <span className="sr-only">Login with Google</span>
|
||||
// </Button>
|
||||
// <Button variant="outline" className="w-full">
|
||||
// <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
// <path
|
||||
// d="M6.915 4.03c-1.968 0-3.683 1.28-4.871 3.113C.704 9.208 0 11.883 0 14.449c0 .706.07 1.369.21 1.973a6.624 6.624 0 0 0 .265.86 5.297 5.297 0 0 0 .371.761c.696 1.159 1.818 1.927 3.593 1.927 1.497 0 2.633-.671 3.965-2.444.76-1.012 1.144-1.626 2.663-4.32l.756-1.339.186-.325c.061.1.121.196.183.3l2.152 3.595c.724 1.21 1.665 2.556 2.47 3.314 1.046.987 1.992 1.22 3.06 1.22 1.075 0 1.876-.355 2.455-.843a3.743 3.743 0 0 0 .81-.973c.542-.939.861-2.127.861-3.745 0-2.72-.681-5.357-2.084-7.45-1.282-1.912-2.957-2.93-4.716-2.93-1.047 0-2.088.467-3.053 1.308-.652.57-1.257 1.29-1.82 2.05-.69-.875-1.335-1.547-1.958-2.056-1.182-.966-2.315-1.303-3.454-1.303zm10.16 2.053c1.147 0 2.188.758 2.992 1.999 1.132 1.748 1.647 4.195 1.647 6.4 0 1.548-.368 2.9-1.839 2.9-.58 0-1.027-.23-1.664-1.004-.496-.601-1.343-1.878-2.832-4.358l-.617-1.028a44.908 44.908 0 0 0-1.255-1.98c.07-.109.141-.224.211-.327 1.12-1.667 2.118-2.602 3.358-2.602zm-10.201.553c1.265 0 2.058.791 2.675 1.446.307.327.737.871 1.234 1.579l-1.02 1.566c-.757 1.163-1.882 3.017-2.837 4.338-1.191 1.649-1.81 1.817-2.486 1.817-.524 0-1.038-.237-1.383-.794-.263-.426-.464-1.13-.464-2.046 0-2.221.63-4.535 1.66-6.088.454-.687.964-1.226 1.533-1.533a2.264 2.264 0 0 1 1.088-.285z"
|
||||
// fill="currentColor"
|
||||
// />
|
||||
// </svg>
|
||||
// <span className="sr-only">Login with Meta</span>
|
||||
// </Button>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export function LeagalFooter() {
|
||||
// return (
|
||||
// <div className="text-balance text-center text-xs text-muted-foreground [&_a]:underline [&_a]:underline-offset-4 hover:[&_a]:text-primary">
|
||||
// By clicking continue, you agree to our <a href="#">Terms of Service</a>{" "}
|
||||
// and <a href="#">Privacy Policy</a>.
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
@ -15,6 +15,8 @@ const globalForDb = globalThis as unknown as {
|
||||
const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
|
||||
if (env.NODE_ENV !== "production") globalForDb.conn = conn;
|
||||
|
||||
export const db = drizzle(conn, { schema });
|
||||
export const db = drizzle(conn, {
|
||||
schema,
|
||||
});
|
||||
|
||||
export type DBType = typeof db;
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import { relations, SQL, sql } from "drizzle-orm";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
AnyPgColumn,
|
||||
boolean,
|
||||
index,
|
||||
integer,
|
||||
@ -16,11 +15,7 @@ import { User } from "next-auth";
|
||||
import { type AdapterAccount } from "next-auth/adapters";
|
||||
import { JSONContent } from "novel";
|
||||
|
||||
export function lower(value: AnyPgColumn): SQL {
|
||||
return sql`lower(${value})`;
|
||||
}
|
||||
|
||||
export const createTable = pgTableCreator((name) => `wiki-antifa_${name}`);
|
||||
export const createTable = pgTableCreator((name) => `logipedia_${name}`);
|
||||
|
||||
export const articles = createTable(
|
||||
"article",
|
||||
@ -43,7 +38,9 @@ export const articles = createTable(
|
||||
),
|
||||
},
|
||||
(example) => ({
|
||||
articleTitleIndex: index("article_title_idx").on(example.title),
|
||||
titleIndex: index("article_title_idx").on(example.title),
|
||||
slugIndex: index("article_slug_idx").on(example.slug),
|
||||
createdAtIndex: index("article_created_at_idx").on(example.createdAt),
|
||||
}),
|
||||
);
|
||||
export type Article = typeof articles.$inferSelect & {
|
||||
@ -81,7 +78,9 @@ export const categories = createTable(
|
||||
),
|
||||
},
|
||||
(example) => ({
|
||||
categoryNameIndex: index("category_name_idx").on(example.name),
|
||||
nameIndex: index("category_name_idx").on(example.name),
|
||||
slugameIndex: index("category_slug_idx").on(example.slug),
|
||||
createdAtIndex: index("category_created_at_idx").on(example.createdAt),
|
||||
}),
|
||||
);
|
||||
export type Category = typeof categories.$inferSelect & {
|
||||
@ -105,6 +104,7 @@ export const users = createTable("user", {
|
||||
withTimezone: true,
|
||||
}).default(sql`CURRENT_TIMESTAMP`),
|
||||
image: varchar("image", { length: 255 }),
|
||||
password: varchar("password", { length: 255 }),
|
||||
});
|
||||
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted: 240 3.7% 5.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
@ -96,7 +96,7 @@
|
||||
transform: scaleX(1); /* Slight overshoot for bounce effect */
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(0.2);
|
||||
transform: scaleX(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -69,5 +69,25 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
function ({ addBase, theme }: any) {
|
||||
function extractColorVars(colorObj: any, colorGroup = "") {
|
||||
return Object.keys(colorObj).reduce((vars: any, colorKey: any) => {
|
||||
const value = colorObj[colorKey];
|
||||
|
||||
const newVars: any =
|
||||
typeof value === "string"
|
||||
? { [`--color${colorGroup}-${colorKey}`]: value }
|
||||
: extractColorVars(value, `-${colorKey}`);
|
||||
|
||||
return { ...vars, ...newVars };
|
||||
}, {});
|
||||
}
|
||||
|
||||
addBase({
|
||||
":root": extractColorVars(theme("colors")),
|
||||
});
|
||||
},
|
||||
],
|
||||
} satisfies Config;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user